diff --git a/packages/plugin-nft-collections/src/constants/collections.ts b/packages/plugin-nft-collections/src/constants/collections.ts index f3700d25f3..c02654c98f 100644 --- a/packages/plugin-nft-collections/src/constants/collections.ts +++ b/packages/plugin-nft-collections/src/constants/collections.ts @@ -12,6 +12,23 @@ export const NFTCollectionSchema = z.object({ verified: z.boolean().default(true), featured: z.boolean().default(false), createdAt: z.string().optional(), + // Market data + floorPrice: z.number().optional(), + volume24h: z.number().optional(), + marketCap: z.number().optional(), + holders: z.number().optional(), + totalSupply: z.number().optional(), + // Social metrics + twitterFollowers: z.number().optional(), + discordMembers: z.number().optional(), + // Trading features + supportedMarketplaces: z.array(z.string()).optional(), + hasRoyalties: z.boolean().optional(), + royaltyPercentage: z.number().optional(), + // Metadata + traits: z.record(z.string(), z.array(z.string())).optional(), + categories: z.array(z.string()).optional(), + lastUpdate: z.string().optional(), }); export type NFTCollection = z.infer; diff --git a/packages/plugin-nft-collections/src/index.ts b/packages/plugin-nft-collections/src/index.ts index 640cc72974..405dd59d0a 100644 --- a/packages/plugin-nft-collections/src/index.ts +++ b/packages/plugin-nft-collections/src/index.ts @@ -7,10 +7,34 @@ import { import { ReservoirService } from "./services/reservoir"; import { MarketIntelligenceService } from "./services/market-intelligence"; import { SocialAnalyticsService } from "./services/social-analytics"; +import { CacheManager } from "./services/cache-manager"; +import { RateLimiter } from "./services/rate-limiter"; +import { SecurityManager } from "./services/security-manager"; import { nftCollectionProvider } from "./providers/nft-collections"; import { sweepFloorAction } from "./actions/sweep-floor"; import { listNFTAction } from "./actions/list-nft"; +interface NFTCollectionsPluginConfig { + caching?: { + enabled: boolean; + ttl?: number; + maxSize?: number; + }; + security?: { + rateLimit?: { + enabled: boolean; + maxRequests?: number; + windowMs?: number; + }; + }; +} + +interface ExtendedCharacter extends Character { + providers?: any[]; + actions?: any[]; + runtime?: IAgentRuntime; +} + export class NFTCollectionsPlugin implements Plugin { public readonly name = "nft-collections"; public readonly description = @@ -19,44 +43,54 @@ export class NFTCollectionsPlugin implements Plugin { private reservoirService?: ReservoirService; private marketIntelligenceService?: MarketIntelligenceService; private socialAnalyticsService?: SocialAnalyticsService; + private cacheManager?: CacheManager; + private rateLimiter?: RateLimiter; + private securityManager?: SecurityManager; - constructor() { - // No need for super() since we're implementing, not extending + constructor(private config: NFTCollectionsPluginConfig = {}) { + this.initializeServices(); } - async setup( - character: Character & { runtime?: IAgentRuntime } - ): Promise { + private initializeServices(): void { + // Initialize caching if enabled + if (this.config.caching?.enabled) { + this.cacheManager = new CacheManager({ + ttl: this.config.caching.ttl || 3600000, // 1 hour default + maxSize: this.config.caching.maxSize || 1000, + }); + } + + // Initialize rate limiter if enabled + if (this.config.security?.rateLimit?.enabled) { + this.rateLimiter = new RateLimiter({ + maxRequests: this.config.security.rateLimit.maxRequests || 100, + windowMs: this.config.security.rateLimit.windowMs || 60000, + }); + } + } + + async setup(character: ExtendedCharacter): Promise { if (!character.runtime) { throw new Error("Runtime not available in character"); } - console.log( - "Character settings:", - JSON.stringify(character.settings, null, 2) - ); - console.log( - "Environment RESERVOIR_API_KEY:", - process.env.RESERVOIR_API_KEY - ); - - // Try to get the API key from character settings or environment variable const reservoirApiKey = character.settings?.secrets?.RESERVOIR_API_KEY || process.env.RESERVOIR_API_KEY; - console.log("Final reservoirApiKey:", reservoirApiKey); if (!reservoirApiKey) { - throw new Error( - "RESERVOIR_API_KEY is required in either character settings or environment variables" - ); + throw new Error("RESERVOIR_API_KEY is required"); } - this.reservoirService = new ReservoirService(reservoirApiKey); + // Initialize Reservoir service + this.reservoirService = new ReservoirService(reservoirApiKey, { + cacheManager: this.cacheManager, + rateLimiter: this.rateLimiter, + }); await this.reservoirService.initialize(character.runtime); await character.runtime.registerService(this.reservoirService); - // Optional services + // Initialize optional services const marketApiKeys = { nansen: character.settings.secrets?.NANSEN_API_KEY, dune: character.settings.secrets?.DUNE_API_KEY, @@ -65,45 +99,50 @@ export class NFTCollectionsPlugin implements Plugin { nftscan: character.settings.secrets?.NFTSCAN_API_KEY, }; - const socialApiKeys = { - twitter: character.settings.secrets?.TWITTER_API_KEY, - discord: character.settings.secrets?.DISCORD_API_KEY, - telegram: character.settings.secrets?.TELEGRAM_API_KEY, - alchemy: character.settings.secrets?.ALCHEMY_API_KEY, - nftscan: character.settings.secrets?.NFTSCAN_API_KEY, - }; - - // Initialize optional services only if API keys are provided if (Object.values(marketApiKeys).some((key) => key)) { - this.marketIntelligenceService = new MarketIntelligenceService( - marketApiKeys - ); - await this.marketIntelligenceService.initialize(); + this.marketIntelligenceService = new MarketIntelligenceService({ + cacheManager: this.cacheManager, + rateLimiter: this.rateLimiter, + }); + await this.marketIntelligenceService.initialize(character.runtime); await character.runtime.registerService( this.marketIntelligenceService ); } + const socialApiKeys = { + twitter: character.settings.secrets?.TWITTER_API_KEY, + discord: character.settings.secrets?.DISCORD_API_KEY, + telegram: character.settings.secrets?.TELEGRAM_API_KEY, + }; + if (Object.values(socialApiKeys).some((key) => key)) { - this.socialAnalyticsService = new SocialAnalyticsService( - socialApiKeys - ); - await this.socialAnalyticsService.initialize(); + this.socialAnalyticsService = new SocialAnalyticsService({ + cacheManager: this.cacheManager, + rateLimiter: this.rateLimiter, + }); + await this.socialAnalyticsService.initialize(character.runtime); await character.runtime.registerService( this.socialAnalyticsService ); } // Register providers and actions - (character as any).providers = (character as any).providers || []; - (character as any).providers.push(nftCollectionProvider); + character.providers = character.providers || []; + character.providers.push(nftCollectionProvider); - (character as any).actions = (character as any).actions || []; - (character as any).actions.push(sweepFloorAction, listNFTAction); + character.actions = character.actions || []; + character.actions.push(sweepFloorAction, listNFTAction); } async teardown(): Promise { - // Cleanup if needed + // Cleanup resources + if (this.cacheManager) { + await this.cacheManager.clear(); + } + if (this.rateLimiter) { + await this.rateLimiter.cleanup(); + } } } diff --git a/packages/plugin-nft-collections/src/services/cache-manager.ts b/packages/plugin-nft-collections/src/services/cache-manager.ts new file mode 100644 index 0000000000..6ab1822d07 --- /dev/null +++ b/packages/plugin-nft-collections/src/services/cache-manager.ts @@ -0,0 +1,57 @@ +interface CacheConfig { + ttl: number; + maxSize: number; +} + +interface CacheEntry { + data: T; + timestamp: number; +} + +export class CacheManager { + private cache: Map>; + private config: CacheConfig; + + constructor(config: CacheConfig) { + this.config = config; + this.cache = new Map(); + } + + async get(key: string): Promise { + const entry = this.cache.get(key); + if (!entry) return null; + + // Check if entry has expired + if (Date.now() - entry.timestamp > this.config.ttl) { + this.cache.delete(key); + return null; + } + + return entry.data; + } + + async set(key: string, data: T): Promise { + // Implement LRU eviction if cache is full + if (this.cache.size >= this.config.maxSize) { + const oldestKey = this.cache.keys().next().value; + this.cache.delete(oldestKey); + } + + this.cache.set(key, { + data, + timestamp: Date.now(), + }); + } + + async delete(key: string): Promise { + this.cache.delete(key); + } + + async clear(): Promise { + this.cache.clear(); + } + + async has(key: string): Promise { + return this.cache.has(key); + } +} diff --git a/packages/plugin-nft-collections/src/services/market-intelligence.ts b/packages/plugin-nft-collections/src/services/market-intelligence.ts index ecf035d577..d3f894f050 100644 --- a/packages/plugin-nft-collections/src/services/market-intelligence.ts +++ b/packages/plugin-nft-collections/src/services/market-intelligence.ts @@ -1,199 +1,127 @@ -import { Service, ServiceType } from "@ai16z/eliza"; -import { MarketIntelligence, TraitAnalytics } from "../types"; +import axios from "axios"; +import { Service, ServiceType, IAgentRuntime } from "@ai16z/eliza"; +import type { CacheManager } from "./cache-manager"; +import type { RateLimiter } from "./rate-limiter"; + +interface MarketIntelligenceConfig { + cacheManager?: CacheManager; + rateLimiter?: RateLimiter; +} + +interface MarketData { + floorPrice: number; + volume24h: number; + marketCap: number; + holders: number; + whaleHolders: number; + washTradingScore: number; + liquidityScore: number; + priceHistory: Array<{ + timestamp: number; + price: number; + }>; +} export class MarketIntelligenceService extends Service { - private nansenApiKey: string; - private duneApiKey: string; - private alchemyApiKey: string; - private chainbaseApiKey: string; - private nftscanApiKey: string; - - constructor(apiKeys: { - nansen?: string; - dune?: string; - alchemy?: string; - chainbase?: string; - nftscan?: string; - }) { + private cacheManager?: CacheManager; + private rateLimiter?: RateLimiter; + protected runtime?: IAgentRuntime; + + constructor(config?: MarketIntelligenceConfig) { super(); - this.nansenApiKey = apiKeys.nansen || ""; - this.duneApiKey = apiKeys.dune || ""; - this.alchemyApiKey = apiKeys.alchemy || ""; - this.chainbaseApiKey = apiKeys.chainbase || ""; - this.nftscanApiKey = apiKeys.nftscan || ""; + this.cacheManager = config?.cacheManager; + this.rateLimiter = config?.rateLimiter; } - static get serviceType(): ServiceType { + static override get serviceType(): ServiceType { return "nft_market_intelligence" as ServiceType; } - async initialize(): Promise { - // Initialize API clients if needed + override async initialize(runtime: IAgentRuntime): Promise { + this.runtime = runtime; + // Initialize any required resources } - private async fetchNansenData(collectionAddress: string): Promise<{ - whaleActivity: any[]; - washTrading: any; - }> { - // TODO: Implement Nansen API calls - // GET /v1/nft/collection/{address}/whales - // GET /v1/nft/collection/{address}/wash-trading - return { - whaleActivity: [], - washTrading: { - suspiciousVolume24h: 0, - suspiciousTransactions24h: 0, - washTradingScore: 0, - }, - }; + private async makeRequest( + endpoint: string, + params: Record = {} + ): Promise { + const cacheKey = `market:${endpoint}:${JSON.stringify(params)}`; + + // Check cache first + if (this.cacheManager) { + const cached = await this.cacheManager.get(cacheKey); + if (cached) return cached; + } + + // Check rate limit + if (this.rateLimiter) { + await this.rateLimiter.checkLimit("market"); + } + + try { + const response = await axios.get(endpoint, { params }); + + // Cache the response + if (this.cacheManager) { + await this.cacheManager.set(cacheKey, response.data); + } + + return response.data; + } catch (error) { + console.error("Market Intelligence API error:", error); + throw error; + } } - private async fetchDuneAnalytics(collectionAddress: string): Promise<{ - priceHistory: any[]; - marketplaceActivity: any; - }> { - // TODO: Implement Dune Analytics API calls - // Execute custom SQL queries for analytics - return { - priceHistory: [], - marketplaceActivity: {}, - }; - } + async getMarketData(address: string): Promise { + // Combine data from multiple sources + const [priceData, holderData, tradingData] = await Promise.all([ + this.getPriceData(address), + this.getHolderData(address), + this.getTradingData(address), + ]); - private async fetchAlchemyData(collectionAddress: string): Promise<{ - traits: any; - rarity: any; - }> { - // TODO: Implement Alchemy NFT API calls - // GET /v2/{apiKey}/getNFTMetadata - // GET /v2/{apiKey}/computeRarity return { - traits: {}, - rarity: {}, + ...priceData, + ...holderData, + ...tradingData, }; } - private async fetchChainbaseData(collectionAddress: string): Promise<{ - holders: any[]; - transfers: any[]; - liquidity: any; - }> { - // TODO: Implement Chainbase API calls - // GET /v1/nft/collection/{address}/holders - // GET /v1/nft/collection/{address}/transfers - return { - holders: [], - transfers: [], - liquidity: { - depth: [], - bidAskSpread: 0, - bestBid: 0, - bestAsk: 0, - }, - }; + private async getPriceData(address: string) { + return this.makeRequest(`/api/price/${address}`); } - async getMarketIntelligence( - collectionAddress: string - ): Promise { - const [nansenData, duneData, chainbaseData] = await Promise.all([ - this.fetchNansenData(collectionAddress), - this.fetchDuneAnalytics(collectionAddress), - this.fetchChainbaseData(collectionAddress), - ]); + private async getHolderData(address: string) { + return this.makeRequest(`/api/holders/${address}`); + } - return { - priceHistory: duneData.priceHistory, - washTradingMetrics: nansenData.washTrading, - marketplaceActivity: duneData.marketplaceActivity, - whaleActivity: nansenData.whaleActivity, - liquidityMetrics: chainbaseData.liquidity, - }; + private async getTradingData(address: string) { + return this.makeRequest(`/api/trading/${address}`); } - async getTraitAnalytics( - collectionAddress: string - ): Promise { - const alchemyData = await this.fetchAlchemyData(collectionAddress); + async detectWashTrading(address: string) { + return this.makeRequest(`/api/wash-trading/${address}`); + } - return { - distribution: alchemyData.traits, - rarityScores: alchemyData.rarity, - combinations: { - total: Object.keys(alchemyData.traits).length, - unique: 0, // Calculate from traits data - rarest: [], // Extract from rarity data - }, - priceByRarity: [], // Combine with market data - }; + async trackWhaleActivity(address: string) { + return this.makeRequest(`/api/whale-activity/${address}`); } - async detectWashTrading(collectionAddress: string): Promise<{ - suspiciousAddresses: string[]; - suspiciousTransactions: Array<{ - hash: string; - from: string; - to: string; - price: number; - confidence: number; - }>; - }> { - const nansenData = await this.fetchNansenData(collectionAddress); - return { - suspiciousAddresses: [], // Extract from Nansen data - suspiciousTransactions: [], // Extract from Nansen data - }; + async analyzeLiquidity(address: string) { + return this.makeRequest(`/api/liquidity/${address}`); } - async getWhaleActivity(collectionAddress: string): Promise<{ - whales: Array<{ - address: string; - holdings: number; - avgHoldingTime: number; - tradingVolume: number; - lastTrade: number; - }>; - impact: { - priceImpact: number; - volumeShare: number; - holdingsShare: number; - }; - }> { - const [nansenData, chainbaseData] = await Promise.all([ - this.fetchNansenData(collectionAddress), - this.fetchChainbaseData(collectionAddress), - ]); + async predictPrices(address: string) { + return this.makeRequest(`/api/price-prediction/${address}`); + } - return { - whales: [], // Combine Nansen and Chainbase data - impact: { - priceImpact: 0, - volumeShare: 0, - holdingsShare: 0, - }, - }; + async getRarityAnalysis(address: string) { + return this.makeRequest(`/api/rarity/${address}`); } - async getLiquidityAnalysis(collectionAddress: string): Promise<{ - depth: Array<{ - price: number; - quantity: number; - totalValue: number; - }>; - metrics: { - totalLiquidity: number; - averageSpread: number; - volatility24h: number; - }; - }> { - const chainbaseData = await this.fetchChainbaseData(collectionAddress); - return { - depth: chainbaseData.liquidity.depth, - metrics: { - totalLiquidity: 0, // Calculate from depth data - averageSpread: chainbaseData.liquidity.bidAskSpread, - volatility24h: 0, // Calculate from price history - }, - }; + async getMarketplaceData(address: string) { + return this.makeRequest(`/api/marketplace/${address}`); } } diff --git a/packages/plugin-nft-collections/src/services/rate-limiter.ts b/packages/plugin-nft-collections/src/services/rate-limiter.ts new file mode 100644 index 0000000000..05dbef0f8d --- /dev/null +++ b/packages/plugin-nft-collections/src/services/rate-limiter.ts @@ -0,0 +1,76 @@ +interface RateLimitConfig { + maxRequests: number; + windowMs: number; +} + +interface RateLimitEntry { + count: number; + resetTime: number; +} + +export class RateLimiter { + private limits: Map; + private config: RateLimitConfig; + + constructor(config: RateLimitConfig) { + this.config = config; + this.limits = new Map(); + } + + async checkLimit(key: string): Promise { + const now = Date.now(); + let entry = this.limits.get(key); + + // If no entry exists or the window has expired, create a new one + if (!entry || now > entry.resetTime) { + entry = { + count: 0, + resetTime: now + this.config.windowMs, + }; + this.limits.set(key, entry); + } + + // Check if limit is exceeded + if (entry.count >= this.config.maxRequests) { + const waitTime = entry.resetTime - now; + throw new Error( + `Rate limit exceeded. Please wait ${Math.ceil( + waitTime / 1000 + )} seconds.` + ); + } + + // Increment the counter + entry.count++; + return true; + } + + async resetLimit(key: string): Promise { + this.limits.delete(key); + } + + async getRemainingRequests(key: string): Promise { + const entry = this.limits.get(key); + if (!entry || Date.now() > entry.resetTime) { + return this.config.maxRequests; + } + return Math.max(0, this.config.maxRequests - entry.count); + } + + async getResetTime(key: string): Promise { + const entry = this.limits.get(key); + if (!entry || Date.now() > entry.resetTime) { + return Date.now() + this.config.windowMs; + } + return entry.resetTime; + } + + async cleanup(): Promise { + const now = Date.now(); + for (const [key, entry] of this.limits.entries()) { + if (now > entry.resetTime) { + this.limits.delete(key); + } + } + } +} diff --git a/packages/plugin-nft-collections/src/services/reservoir.ts b/packages/plugin-nft-collections/src/services/reservoir.ts index 5aca96a490..554d107a72 100644 --- a/packages/plugin-nft-collections/src/services/reservoir.ts +++ b/packages/plugin-nft-collections/src/services/reservoir.ts @@ -1,390 +1,148 @@ -import { - Service, - ServiceType, - Action, - HandlerCallback, - IAgentRuntime, - Memory, - State, - ActionExample, -} from "@ai16z/eliza"; -import type { NFTCollection, MarketStats, NFTService } from "../types"; +import axios from "axios"; +import { Service, ServiceType, IAgentRuntime } from "@ai16z/eliza"; +import type { CacheManager } from "./cache-manager"; +import type { RateLimiter } from "./rate-limiter"; +import { NFTCollection } from "../constants/collections"; + +interface ReservoirConfig { + cacheManager?: CacheManager; + rateLimiter?: RateLimiter; +} -export class ReservoirService extends Service implements NFTService { +export class ReservoirService extends Service { private apiKey: string; private baseUrl = "https://api.reservoir.tools"; - protected runtime: IAgentRuntime | undefined; + private cacheManager?: CacheManager; + private rateLimiter?: RateLimiter; + protected runtime?: IAgentRuntime; - constructor(apiKey: string) { + constructor(apiKey: string, config?: ReservoirConfig) { super(); - if (!apiKey || typeof apiKey !== "string" || apiKey.trim() === "") { - throw new Error("Invalid Reservoir API key provided"); - } this.apiKey = apiKey; + this.cacheManager = config?.cacheManager; + this.rateLimiter = config?.rateLimiter; } - static get serviceType(): ServiceType { + static override get serviceType(): ServiceType { return "nft" as ServiceType; } - setRuntime(runtime: IAgentRuntime): void { - this.runtime = runtime; - } - - async initialize(runtime: IAgentRuntime): Promise { + override async initialize(runtime: IAgentRuntime): Promise { this.runtime = runtime; - - // Register NFT-related actions - const actions: Action[] = [ - { - name: "GET_TOP_COLLECTIONS", - description: "Get top NFT collections by volume", - similes: ["FETCH_TOP_COLLECTIONS", "LIST_TOP_COLLECTIONS"], - examples: [ - [ - { - user: "user", - content: { - text: "Show me the top NFT collections", - }, - }, - ], - ], - handler: async ( - _runtime: IAgentRuntime, - _message: Memory, - _state: State, - _options: any, - callback?: HandlerCallback - ) => { - try { - const collections = await this.getTopCollections(); - callback?.({ - text: JSON.stringify(collections, null, 2), - }); - return true; - } catch (error) { - callback?.({ - text: `Error fetching collections: ${error}`, - }); - return false; - } - }, - validate: async () => true, - }, - { - name: "GET_MARKET_STATS", - description: "Get NFT market statistics", - similes: ["FETCH_MARKET_STATS", "GET_NFT_STATS"], - examples: [ - [ - { - user: "user", - content: { - text: "What are the current NFT market statistics?", - }, - }, - ], - ], - handler: async ( - _runtime: IAgentRuntime, - _message: Memory, - _state: State, - _options: any, - callback?: HandlerCallback - ) => { - try { - const stats = await this.getMarketStats(); - callback?.({ text: JSON.stringify(stats, null, 2) }); - return true; - } catch (error) { - callback?.({ - text: `Error fetching market stats: ${error}`, - }); - return false; - } - }, - validate: async () => true, - }, - ]; - - // Register each action and log the registration - for (const action of actions) { - runtime.registerAction(action); - console.log(`✓ Registering NFT action: ${action.name}`); + // Initialize any required resources + if (!this.apiKey) { + throw new Error("Reservoir API key is required"); } } - private async fetchFromReservoir( + private async makeRequest( endpoint: string, params: Record = {} - ): Promise { - const queryString = new URLSearchParams(params).toString(); - const url = `${this.baseUrl}${endpoint}${queryString ? `?${queryString}` : ""}`; + ): Promise { + const cacheKey = `reservoir:${endpoint}:${JSON.stringify(params)}`; + + // Check cache first + if (this.cacheManager) { + const cached = await this.cacheManager.get(cacheKey); + if (cached) return cached; + } + + // Check rate limit + if (this.rateLimiter) { + await this.rateLimiter.checkLimit("reservoir"); + } try { - const response = await fetch(url, { + const response = await axios.get(`${this.baseUrl}${endpoint}`, { + params, headers: { - accept: "*/*", "x-api-key": this.apiKey, }, }); - if (!response.ok) { - const errorText = await response.text(); - if (response.status === 401 || response.status === 403) { - throw new Error("Invalid or expired Reservoir API key"); - } - throw new Error( - `Reservoir API error: ${response.status} - ${errorText}` - ); + // Cache the response + if (this.cacheManager) { + await this.cacheManager.set(cacheKey, response.data); } - return await response.json(); + return response.data; } catch (error) { - if (error instanceof Error) { - throw error; - } - throw new Error("Failed to fetch data from Reservoir API"); + console.error("Reservoir API error:", error); + throw error; } } - async getTopCollections(): Promise { - const data = await this.fetchFromReservoir("/collections/v7", { - limit: 20, - sortBy: "1DayVolume", - includeTopBid: true, - normalizeRoyalties: true, + async getCollection(address: string): Promise { + const data = await this.makeRequest(`/collections/v6`, { + contract: address, + }); + + return { + address: data.collections[0].id, + name: data.collections[0].name, + symbol: data.collections[0].symbol, + description: data.collections[0].description, + imageUrl: data.collections[0].image, + externalUrl: data.collections[0].externalUrl, + twitterUsername: data.collections[0].twitterUsername, + discordUrl: data.collections[0].discordUrl, + verified: + data.collections[0].openseaVerificationStatus === "verified", + floorPrice: + data.collections[0].floorAsk?.price?.amount?.native || 0, + volume24h: data.collections[0].volume24h || 0, + marketCap: data.collections[0].marketCap || 0, + totalSupply: data.collections[0].tokenCount || 0, + }; + } + + async getTopCollections(limit: number = 10): Promise { + const data = await this.makeRequest(`/collections/v6`, { + limit, + sortBy: "volume24h", }); return data.collections.map((collection: any) => ({ address: collection.id, name: collection.name, - symbol: collection.symbol || "", + symbol: collection.symbol, description: collection.description, imageUrl: collection.image, + externalUrl: collection.externalUrl, + twitterUsername: collection.twitterUsername, + discordUrl: collection.discordUrl, + verified: collection.openseaVerificationStatus === "verified", floorPrice: collection.floorAsk?.price?.amount?.native || 0, - volume24h: collection.volume["1day"] || 0, + volume24h: collection.volume24h || 0, marketCap: collection.marketCap || 0, - holders: collection.ownerCount || 0, + totalSupply: collection.tokenCount || 0, })); } - async getMarketStats(): Promise { - const data = await this.fetchFromReservoir("/collections/v7", { - limit: 500, - sortBy: "1DayVolume", + async getMarketStats(address: string) { + return this.makeRequest(`/collections/v6/stats`, { + contract: address, }); - - const stats = data.collections.reduce( - (acc: any, collection: any) => { - acc.totalVolume24h += collection.volume["1day"] || 0; - acc.totalMarketCap += collection.marketCap || 0; - acc.totalHolders += collection.ownerCount || 0; - acc.floorPrices.push( - collection.floorAsk?.price?.amount?.native || 0 - ); - return acc; - }, - { - totalVolume24h: 0, - totalMarketCap: 0, - totalHolders: 0, - floorPrices: [], - } - ); - - return { - totalVolume24h: stats.totalVolume24h, - totalMarketCap: stats.totalMarketCap, - totalCollections: data.collections.length, - totalHolders: stats.totalHolders, - averageFloorPrice: - stats.floorPrices.reduce((a: number, b: number) => a + b, 0) / - stats.floorPrices.length, - }; } - async getCollectionActivity(collectionAddress: string): Promise { - return await this.fetchFromReservoir(`/collections/activity/v6`, { - collection: collectionAddress, - limit: 100, - includeMetadata: true, + async getCollectionActivity(address: string, limit: number = 20) { + return this.makeRequest(`/collections/v6/activity`, { + contract: address, + limit, }); } - async getCollectionTokens(collectionAddress: string): Promise { - return await this.fetchFromReservoir(`/tokens/v7`, { - collection: collectionAddress, - limit: 100, - includeAttributes: true, - includeTopBid: true, + async getTokens(address: string, limit: number = 20) { + return this.makeRequest(`/tokens/v6`, { + contract: address, + limit, }); } - async getCollectionAttributes(collectionAddress: string): Promise { - return await this.fetchFromReservoir( - `/collections/${collectionAddress}/attributes/v3` - ); - } - - async createListing(options: { - tokenId: string; - collectionAddress: string; - price: number; - expirationTime?: number; - marketplace: "ikigailabs"; - currency?: string; - quantity?: number; - }): Promise<{ - listingId: string; - status: string; - transactionHash?: string; - marketplaceUrl: string; - }> { - // First, get the order kind and other details from Reservoir - const orderKind = await this.fetchFromReservoir(`/execute/list/v5`, { - maker: options.collectionAddress, - token: `${options.collectionAddress}:${options.tokenId}`, - weiPrice: options.price.toString(), - orderKind: "seaport-v1.5", - orderbook: "ikigailabs", - currency: options.currency || "ETH", - quantity: options.quantity || "1", - }); - - // Create the listing using the order data - const listingData = await this.fetchFromReservoir(`/order/v3`, { - kind: orderKind.kind, - data: { - ...orderKind.data, - expirationTime: - options.expirationTime || - Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60, - }, + async getFloorPrice(address: string) { + const data = await this.makeRequest(`/collections/v6/floor-ask`, { + contract: address, }); - - return { - listingId: listingData.order.hash, - status: listingData.order.status, - transactionHash: listingData.order.transactionHash, - marketplaceUrl: `https://ikigailabs.xyz/assets/${options.collectionAddress}/${options.tokenId}`, - }; - } - - async cancelListing(options: { - listingId: string; - marketplace: "ikigailabs"; - }): Promise<{ - status: string; - transactionHash?: string; - }> { - const cancelData = await this.fetchFromReservoir(`/order/v3/cancel`, { - orderHash: options.listingId, - orderbook: "ikigailabs", - }); - - return { - status: cancelData.status, - transactionHash: cancelData.transactionHash, - }; - } - - async getOwnedNFTs(owner: string): Promise< - Array<{ - tokenId: string; - collectionAddress: string; - name: string; - imageUrl?: string; - attributes?: Record; - }> - > { - const data = await this.fetchFromReservoir( - `/users/${owner}/tokens/v7`, - { - limit: 100, - includeAttributes: true, - } - ); - - return data.tokens.map((token: any) => ({ - tokenId: token.tokenId, - collectionAddress: token.contract, - name: token.name || `${token.collection.name} #${token.tokenId}`, - imageUrl: token.image, - attributes: token.attributes?.reduce( - (acc: Record, attr: any) => { - acc[attr.key] = attr.value; - return acc; - }, - {} - ), - })); - } - - async getFloorListings(options: { - collection: string; - limit: number; - sortBy: "price" | "rarity"; - }): Promise< - Array<{ - tokenId: string; - price: number; - seller: string; - marketplace: string; - }> - > { - const data = await this.fetchFromReservoir(`/tokens/v7`, { - collection: options.collection, - limit: options.limit, - sortBy: options.sortBy === "price" ? "floorAskPrice" : "rarity", - includeTopBid: true, - status: "listed", - }); - - return data.tokens.map((token: any) => ({ - tokenId: token.tokenId, - price: token.floorAsk.price.amount.native, - seller: token.floorAsk.maker, - marketplace: token.floorAsk.source.name, - })); - } - - async executeBuy(options: { - listings: Array<{ - tokenId: string; - price: number; - seller: string; - marketplace: string; - }>; - taker: string; - }): Promise<{ - path: string; - steps: Array<{ - action: string; - status: string; - }>; - }> { - // Execute buy orders through Reservoir API - const orders = options.listings.map((listing) => ({ - tokenId: listing.tokenId, - maker: listing.seller, - price: listing.price, - })); - - const data = await this.fetchFromReservoir(`/execute/bulk/v1`, { - taker: options.taker, - items: orders, - skipBalanceCheck: false, - currency: "ETH", - }); - - return { - path: data.path, - steps: data.steps.map((step: any) => ({ - action: step.type, - status: step.status, - })), - }; + return data.floorAsk?.price?.amount?.native || 0; } } diff --git a/packages/plugin-nft-collections/src/services/security-manager.ts b/packages/plugin-nft-collections/src/services/security-manager.ts new file mode 100644 index 0000000000..3c978e2561 --- /dev/null +++ b/packages/plugin-nft-collections/src/services/security-manager.ts @@ -0,0 +1,68 @@ +import * as crypto from "crypto"; + +interface SecurityConfig { + algorithm: string; +} + +export class SecurityManager { + private config: SecurityConfig; + private key: Buffer; + private iv: Buffer; + + constructor(config: SecurityConfig) { + this.config = config; + // Generate a secure key and IV + this.key = crypto.randomBytes(32); // 256 bits for AES-256 + this.iv = crypto.randomBytes(16); // 128 bits for AES + } + + encryptSensitiveData(data: any): string { + const cipher = crypto.createCipheriv( + this.config.algorithm, + this.key, + this.iv + ); + + let encrypted = cipher.update(JSON.stringify(data), "utf8", "hex"); + encrypted += cipher.final("hex"); + + // Return IV + encrypted data + return this.iv.toString("hex") + ":" + encrypted; + } + + decryptSensitiveData(encryptedData: string): T { + const [ivHex, data] = encryptedData.split(":"); + const iv = Buffer.from(ivHex, "hex"); + + const decipher = crypto.createDecipheriv( + this.config.algorithm, + this.key, + iv + ); + + let decrypted = decipher.update(data, "hex", "utf8"); + decrypted += decipher.final("utf8"); + + return JSON.parse(decrypted); + } + + hashData(data: string): string { + return crypto.createHash("sha256").update(data).digest("hex"); + } + + generateSignature(data: any, timestamp: number): string { + const message = JSON.stringify(data) + timestamp; + return crypto + .createHmac("sha256", this.key) + .update(message) + .digest("hex"); + } + + verifySignature(data: any, timestamp: number, signature: string): boolean { + const expectedSignature = this.generateSignature(data, timestamp); + return crypto.timingSafeEqual( + Buffer.from(signature), + Buffer.from(expectedSignature) + ); + } +} diff --git a/packages/plugin-nft-collections/src/services/social-analytics.ts b/packages/plugin-nft-collections/src/services/social-analytics.ts index e9ba7eb130..f23f382b07 100644 --- a/packages/plugin-nft-collections/src/services/social-analytics.ts +++ b/packages/plugin-nft-collections/src/services/social-analytics.ts @@ -1,227 +1,239 @@ -import { Service, ServiceType } from "@ai16z/eliza"; -import { SocialMetrics, NewsItem, CommunityMetrics } from "../types"; - -export class SocialAnalyticsService extends Service { - private twitterApiKey: string; - private discordApiKey: string; - private telegramApiKey: string; - private alchemyApiKey: string; - private nftscanApiKey: string; - - constructor(apiKeys: { +import axios from "axios"; +import { Service, ServiceType, IAgentRuntime } from "@ai16z/eliza"; +import type { CacheManager } from "./cache-manager"; +import type { RateLimiter } from "./rate-limiter"; + +interface SocialAnalyticsConfig { + cacheManager?: CacheManager; + rateLimiter?: RateLimiter; + apiKeys?: { twitter?: string; discord?: string; telegram?: string; - alchemy?: string; - nftscan?: string; - }) { + }; +} + +interface SocialData { + twitter: { + followers: number; + engagement: number; + sentiment: number; + recentTweets: Array<{ + id: string; + text: string; + likes: number; + retweets: number; + replies: number; + }>; + }; + discord: { + members: number; + activeUsers: number; + messageVolume: number; + topChannels: Array<{ + name: string; + messages: number; + users: number; + }>; + }; + telegram?: { + members: number; + activeUsers: number; + messageVolume: number; + }; + sentiment: { + overall: number; + twitter: number; + discord: number; + telegram?: number; + }; +} + +interface SocialMetrics { + twitter: { + followers: number; + engagement: { + likes: number; + retweets: number; + replies: number; + mentions: number; + }; + sentiment: { + positive: number; + neutral: number; + negative: number; + }; + }; + mentions: Array<{ + platform: string; + text: string; + timestamp: Date; + author: string; + engagement: number; + }>; + influencers: Array<{ + address: string; + platform: string; + followers: number; + engagement: number; + sentiment: number; + }>; + trending: boolean; +} + +export class SocialAnalyticsService extends Service { + private cacheManager?: CacheManager; + private rateLimiter?: RateLimiter; + protected runtime?: IAgentRuntime; + private apiKeys: Required>; + + constructor(config?: SocialAnalyticsConfig) { super(); - this.twitterApiKey = apiKeys.twitter || ""; - this.discordApiKey = apiKeys.discord || ""; - this.telegramApiKey = apiKeys.telegram || ""; - this.alchemyApiKey = apiKeys.alchemy || ""; - this.nftscanApiKey = apiKeys.nftscan || ""; + this.cacheManager = config?.cacheManager; + this.rateLimiter = config?.rateLimiter; + this.apiKeys = { + twitter: config?.apiKeys?.twitter || "", + discord: config?.apiKeys?.discord || "", + telegram: config?.apiKeys?.telegram || "", + }; } - static get serviceType(): ServiceType { + static override get serviceType(): ServiceType { return "nft_social_analytics" as ServiceType; } - async initialize(): Promise { - // Initialize API clients if needed + override async initialize(runtime: IAgentRuntime): Promise { + this.runtime = runtime; + // Initialize any required resources } - private async fetchTwitterMetrics(collectionAddress: string): Promise<{ - followers: number; - engagement: any; - sentiment: any; - }> { - // TODO: Implement Twitter API v2 calls - // GET /2/users/{id}/followers - // GET /2/tweets/search/recent - return { - followers: 0, - engagement: { - likes: 0, - retweets: 0, - replies: 0, - mentions: 0, - }, - sentiment: { - positive: 0, - neutral: 0, - negative: 0, - }, - }; + private async makeRequest( + endpoint: string, + params: Record = {} + ): Promise { + const cacheKey = `social:${endpoint}:${JSON.stringify(params)}`; + + // Check cache first + if (this.cacheManager) { + const cached = await this.cacheManager.get(cacheKey); + if (cached) return cached; + } + + // Check rate limit + if (this.rateLimiter) { + await this.rateLimiter.checkLimit("social"); + } + + try { + const response = await axios.get(endpoint, { params }); + + // Cache the response + if (this.cacheManager) { + await this.cacheManager.set(cacheKey, response.data); + } + + return response.data; + } catch (error) { + console.error("Social Analytics API error:", error); + throw error; + } } - private async fetchDiscordMetrics(serverId: string): Promise<{ - members: number; - activity: any; - channels: any[]; - }> { - // TODO: Implement Discord API calls - // GET /guilds/{guild.id} - // GET /guilds/{guild.id}/channels + async getAnalytics(address: string): Promise { + // Combine data from multiple sources + const [twitterData, discordData, telegramData, sentimentData] = + await Promise.all([ + this.getTwitterData(address), + this.getDiscordData(address), + this.getTelegramData(address), + this.getSentimentData(address), + ]); + return { - members: 0, - activity: { - messagesPerDay: 0, - activeUsers: 0, - growthRate: 0, - }, - channels: [], + twitter: twitterData, + discord: discordData, + telegram: telegramData, + sentiment: sentimentData, }; } - private async fetchTelegramMetrics(groupId: string): Promise<{ - members: number; - activity: any; - }> { - // TODO: Implement Telegram Bot API calls - // getChatMemberCount - // getChatMembersCount - return { - members: 0, - activity: { - messagesPerDay: 0, - activeUsers: 0, - growthRate: 0, - }, - }; + private async getTwitterData(address: string) { + return this.makeRequest(`/api/twitter/${address}`); } - private async fetchNFTScanSocial(collectionAddress: string): Promise<{ - mentions: any[]; - influencers: any[]; - trending: boolean; - }> { - // TODO: Implement NFTScan Social API calls - // GET /v1/social/collection/{address}/mentions - // GET /v1/social/collection/{address}/influencers - return { - mentions: [], - influencers: [], - trending: false, - }; + private async getDiscordData(address: string) { + return this.makeRequest(`/api/discord/${address}`); } - async getSocialMetrics(collectionAddress: string): Promise { - const [twitterData, nftscanData] = await Promise.all([ - this.fetchTwitterMetrics(collectionAddress), - this.fetchNFTScanSocial(collectionAddress), - ]); + private async getTelegramData(address: string) { + return this.makeRequest(`/api/telegram/${address}`); + } - return { - twitter: { - followers: twitterData.followers, - engagement: twitterData.engagement, - sentiment: twitterData.sentiment, - }, - mentions: nftscanData.mentions, - influencers: nftscanData.influencers, - trending: nftscanData.trending, - }; + private async getSentimentData(address: string) { + return this.makeRequest(`/api/sentiment/${address}`); } - async getNews(collectionAddress: string): Promise { - const nftscanData = await this.fetchNFTScanSocial(collectionAddress); - - // Transform mentions and social data into news items - return nftscanData.mentions.map((mention) => ({ - title: "", - source: "", - url: "", - timestamp: new Date(), - sentiment: "neutral", - relevance: 1, - })); - } - - async getCommunityMetrics( - collectionAddress: string, - discordId?: string, - telegramId?: string - ): Promise { - const [discordData, telegramData] = await Promise.all([ - discordId ? this.fetchDiscordMetrics(discordId) : null, - telegramId ? this.fetchTelegramMetrics(telegramId) : null, - ]); + async getEngagementMetrics(address: string) { + return this.makeRequest(`/api/engagement/${address}`); + } - return { - discord: discordData - ? { - members: discordData.members, - activity: discordData.activity, - channels: discordData.channels, - } - : null, - telegram: telegramData - ? { - members: telegramData.members, - activity: telegramData.activity, - } - : null, - totalMembers: - (discordData?.members || 0) + (telegramData?.members || 0), - growthRate: 0, // Calculate from historical data - engagement: { - activeUsers: 0, - messagesPerDay: 0, - topChannels: [], - }, - }; + async getSentimentAnalysis(address: string) { + return this.makeRequest(`/api/sentiment-analysis/${address}`); } - async analyzeSentiment(collectionAddress: string): Promise<{ - overall: number; - breakdown: { - positive: number; - neutral: number; - negative: number; - }; - trends: Array<{ - topic: string; - sentiment: number; - volume: number; - }>; - }> { - const [twitterData, nftscanData] = await Promise.all([ - this.fetchTwitterMetrics(collectionAddress), - this.fetchNFTScanSocial(collectionAddress), - ]); + async getCommunityGrowth(address: string) { + return this.makeRequest(`/api/community-growth/${address}`); + } - return { - overall: 0, // Calculate weighted average - breakdown: twitterData.sentiment, - trends: [], // Extract from mentions and social data - }; + async getInfluencerAnalysis(address: string) { + return this.makeRequest(`/api/influencers/${address}`); } - async trackSocialPerformance(collectionAddress: string): Promise<{ - metrics: { - reach: number; - engagement: number; - influence: number; - }; - trends: Array<{ - platform: string; - metric: string; - values: number[]; - }>; - }> { - const [twitterData, nftscanData] = await Promise.all([ - this.fetchTwitterMetrics(collectionAddress), - this.fetchNFTScanSocial(collectionAddress), + async getContentPerformance(address: string) { + return this.makeRequest(`/api/content/${address}`); + } + + async getCrossPlatformAnalytics(address: string) { + return this.makeRequest(`/api/cross-platform/${address}`); + } + + async getSocialMetrics(address: string): Promise { + const [twitterData, mentions, influencers] = await Promise.all([ + this.getTwitterData(address), + this.makeRequest(`/api/mentions/${address}`), + this.makeRequest(`/api/influencers/${address}`), ]); return { - metrics: { - reach: twitterData.followers, - engagement: 0, // Calculate from engagement data - influence: 0, // Calculate from influencer data + twitter: { + followers: twitterData.followers, + engagement: { + likes: twitterData.engagement?.likes || 0, + retweets: twitterData.engagement?.retweets || 0, + replies: twitterData.engagement?.replies || 0, + mentions: twitterData.engagement?.mentions || 0, + }, + sentiment: { + positive: twitterData.sentiment?.positive || 0, + neutral: twitterData.sentiment?.neutral || 0, + negative: twitterData.sentiment?.negative || 0, + }, }, - trends: [], // Compile historical data + mentions: mentions.map((mention: any) => ({ + platform: mention.platform, + text: mention.text, + timestamp: new Date(mention.timestamp), + author: mention.author, + engagement: mention.engagement, + })), + influencers: influencers.map((influencer: any) => ({ + address: influencer.address, + platform: influencer.platform, + followers: influencer.followers, + engagement: influencer.engagement, + sentiment: influencer.sentiment, + })), + trending: mentions.length > 100 || influencers.length > 10, }; } } diff --git a/packages/plugin-nft-collections/src/tests/services.test.ts b/packages/plugin-nft-collections/src/tests/services.test.ts index 26967f8fef..14d748ad5c 100644 --- a/packages/plugin-nft-collections/src/tests/services.test.ts +++ b/packages/plugin-nft-collections/src/tests/services.test.ts @@ -1,16 +1,31 @@ import { describe, expect, it, beforeEach, jest } from "@jest/globals"; import { ReservoirService } from "../services/reservoir"; -import { CoinGeckoService } from "../services/coingecko"; import { SocialAnalyticsService } from "../services/social-analytics"; +import { CacheManager } from "../services/cache-manager"; +import { RateLimiter } from "../services/rate-limiter"; import type { NFTCollection } from "../types"; describe("NFT Services", () => { describe("ReservoirService", () => { const apiKey = "test-api-key"; let service: ReservoirService; + let cacheManager: CacheManager; + let rateLimiter: RateLimiter; beforeEach(() => { - service = new ReservoirService(apiKey); + cacheManager = new CacheManager({ + ttl: 3600000, + maxSize: 1000, + }); + rateLimiter = new RateLimiter({ + maxRequests: 100, + windowMs: 60000, + }); + + service = new ReservoirService(apiKey, { + cacheManager, + rateLimiter, + }); global.fetch = jest.fn(() => Promise.resolve({ ok: true, @@ -44,41 +59,29 @@ describe("NFT Services", () => { }); }); - describe("CoinGeckoService", () => { - let service: CoinGeckoService; - - beforeEach(() => { - service = new CoinGeckoService(); - global.fetch = jest.fn(() => - Promise.resolve({ - ok: true, - json: () => - Promise.resolve({ - id: "test-collection", - contract_address: "0x1234", - name: "Test Collection", - floor_price_eth: 1.5, - volume_24h_eth: 100, - }), - } as Response) - ); - }); - - it("should fetch NFT market data", async () => { - const data = await service.getNFTMarketData("0x1234"); - expect(data?.floor_price_eth).toBe(1.5); - expect(data?.volume_24h_eth).toBe(100); - expect(global.fetch).toHaveBeenCalled(); - }); - }); - describe("SocialAnalyticsService", () => { let service: SocialAnalyticsService; + let cacheManager: CacheManager; + let rateLimiter: RateLimiter; beforeEach(() => { + cacheManager = new CacheManager({ + ttl: 3600000, + maxSize: 1000, + }); + rateLimiter = new RateLimiter({ + maxRequests: 100, + windowMs: 60000, + }); + service = new SocialAnalyticsService({ - twitter: "twitter-key", - nftscan: "nftscan-key", + cacheManager, + rateLimiter, + apiKeys: { + twitter: "test-twitter-key", + discord: "test-discord-key", + telegram: "test-telegram-key", + }, }); global.fetch = jest.fn(() => Promise.resolve({