diff --git a/packages/plugin-nft-collections/src/constants/curated-collections.ts b/packages/plugin-nft-collections/src/constants/curated-collections.ts index 7f09bbc22d..90b7610d40 100644 --- a/packages/plugin-nft-collections/src/constants/curated-collections.ts +++ b/packages/plugin-nft-collections/src/constants/curated-collections.ts @@ -1914,3 +1914,10 @@ export function getShareableCollectionLink( const url = getCollectionViewUrl(address, options); return `View this NFT collection on IkigaiLabs: ${url}`; } + +// Set of curated collection addresses (lowercase) +export const curatedCollections = new Set([ + // Add your curated collection addresses here + // Example: + // "0x1234...".toLowerCase(), +]); diff --git a/packages/plugin-nft-collections/src/index.ts b/packages/plugin-nft-collections/src/index.ts index 405dd59d0a..bf1d237e2d 100644 --- a/packages/plugin-nft-collections/src/index.ts +++ b/packages/plugin-nft-collections/src/index.ts @@ -7,7 +7,7 @@ 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 { MemoryCacheManager } from "./services/cache-manager"; import { RateLimiter } from "./services/rate-limiter"; import { SecurityManager } from "./services/security-manager"; import { nftCollectionProvider } from "./providers/nft-collections"; @@ -27,6 +27,9 @@ interface NFTCollectionsPluginConfig { windowMs?: number; }; }; + maxConcurrent?: number; + maxRetries?: number; + batchSize?: number; } interface ExtendedCharacter extends Character { @@ -43,7 +46,7 @@ export class NFTCollectionsPlugin implements Plugin { private reservoirService?: ReservoirService; private marketIntelligenceService?: MarketIntelligenceService; private socialAnalyticsService?: SocialAnalyticsService; - private cacheManager?: CacheManager; + private cacheManager?: MemoryCacheManager; private rateLimiter?: RateLimiter; private securityManager?: SecurityManager; @@ -54,10 +57,9 @@ export class NFTCollectionsPlugin implements Plugin { 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, - }); + this.cacheManager = new MemoryCacheManager( + this.config.caching.ttl || 3600000 // 1 hour default + ); } // Initialize rate limiter if enabled @@ -82,15 +84,18 @@ export class NFTCollectionsPlugin implements Plugin { throw new Error("RESERVOIR_API_KEY is required"); } - // Initialize Reservoir service + // Initialize Reservoir service with enhanced configuration this.reservoirService = new ReservoirService(reservoirApiKey, { cacheManager: this.cacheManager, rateLimiter: this.rateLimiter, + maxConcurrent: this.config.maxConcurrent, + maxRetries: this.config.maxRetries, + batchSize: this.config.batchSize, }); await this.reservoirService.initialize(character.runtime); await character.runtime.registerService(this.reservoirService); - // Initialize optional services + // Initialize optional services with enhanced configuration const marketApiKeys = { nansen: character.settings.secrets?.NANSEN_API_KEY, dune: character.settings.secrets?.DUNE_API_KEY, diff --git a/packages/plugin-nft-collections/src/services/cache-manager.ts b/packages/plugin-nft-collections/src/services/cache-manager.ts index 6ab1822d07..93c8eb6f51 100644 --- a/packages/plugin-nft-collections/src/services/cache-manager.ts +++ b/packages/plugin-nft-collections/src/services/cache-manager.ts @@ -1,57 +1,37 @@ -interface CacheConfig { - ttl: number; - maxSize: number; +export interface CacheManager { + get(key: string): Promise; + set(key: string, value: T, ttl?: number): Promise; + clear(): Promise; } -interface CacheEntry { - data: T; - timestamp: number; -} - -export class CacheManager { - private cache: Map>; - private config: CacheConfig; +export class MemoryCacheManager implements CacheManager { + private cache: Map; + private defaultTtl: number; - constructor(config: CacheConfig) { - this.config = config; + constructor(defaultTtl: number = 3600000) { + // 1 hour default this.cache = new Map(); + this.defaultTtl = defaultTtl; } async get(key: string): Promise { - const entry = this.cache.get(key); - if (!entry) return null; + const item = this.cache.get(key); + if (!item) return null; - // Check if entry has expired - if (Date.now() - entry.timestamp > this.config.ttl) { + if (Date.now() > item.expiry) { 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(), - }); + return item.value as T; } - async delete(key: string): Promise { - this.cache.delete(key); + async set(key: string, value: T, ttl?: number): Promise { + const expiry = Date.now() + (ttl || this.defaultTtl); + this.cache.set(key, { value, expiry }); } 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/reservoir.ts b/packages/plugin-nft-collections/src/services/reservoir.ts index 554d107a72..5ace89c098 100644 --- a/packages/plugin-nft-collections/src/services/reservoir.ts +++ b/packages/plugin-nft-collections/src/services/reservoir.ts @@ -1,12 +1,23 @@ -import axios from "axios"; +import axios, { AxiosError } 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"; +import { COLLECTIONS_BY_ADDRESS } from "../constants/curated-collections"; +import pRetry from "p-retry"; +import pQueue from "p-queue"; interface ReservoirConfig { cacheManager?: CacheManager; rateLimiter?: RateLimiter; + maxConcurrent?: number; + maxRetries?: number; + batchSize?: number; +} + +interface RequestQueueItem { + priority: number; + fn: () => Promise; } export class ReservoirService extends Service { @@ -15,12 +26,22 @@ export class ReservoirService extends Service { private cacheManager?: CacheManager; private rateLimiter?: RateLimiter; protected runtime?: IAgentRuntime; + private requestQueue: pQueue; + private maxRetries: number; + private batchSize: number; constructor(apiKey: string, config?: ReservoirConfig) { super(); this.apiKey = apiKey; this.cacheManager = config?.cacheManager; this.rateLimiter = config?.rateLimiter; + this.maxRetries = config?.maxRetries || 3; + this.batchSize = config?.batchSize || 20; + + // Initialize request queue with concurrency control + this.requestQueue = new pQueue({ + concurrency: config?.maxConcurrent || 5, + }); } static override get serviceType(): ServiceType { @@ -29,15 +50,19 @@ export class ReservoirService extends Service { override async initialize(runtime: IAgentRuntime): Promise { this.runtime = runtime; - // Initialize any required resources if (!this.apiKey) { throw new Error("Reservoir API key is required"); } } + private getRequestPriority(address: string): number { + return COLLECTIONS_BY_ADDRESS.has(address.toLowerCase()) ? 1 : 0; + } + private async makeRequest( endpoint: string, - params: Record = {} + params: Record = {}, + priority: number = 0 ): Promise { const cacheKey = `reservoir:${endpoint}:${JSON.stringify(params)}`; @@ -47,60 +72,127 @@ export class ReservoirService extends Service { if (cached) return cached; } - // Check rate limit - if (this.rateLimiter) { - await this.rateLimiter.checkLimit("reservoir"); - } + // Add request to queue with priority + return this.requestQueue.add( + async () => { + // Check rate limit + if (this.rateLimiter) { + await this.rateLimiter.checkLimit("reservoir"); + } + + // Implement retry logic with exponential backoff + return pRetry( + async () => { + try { + const response = await axios.get( + `${this.baseUrl}${endpoint}`, + { + params, + headers: { + "x-api-key": this.apiKey, + }, + } + ); + + // Cache successful response + if (this.cacheManager) { + const ttl = priority > 0 ? 3600000 : 1800000; // Longer TTL for curated collections (1h vs 30m) + await this.cacheManager.set( + cacheKey, + response.data, + ttl + ); + } + + return response.data; + } catch (error) { + if (error instanceof AxiosError) { + // Retry on specific error codes + if ( + error.response?.status === 429 || + error.response?.status >= 500 + ) { + throw error; + } + } + console.error("Reservoir API error:", error, { + endpoint, + params, + }); + throw error; + } + }, + { + retries: this.maxRetries, + onFailedAttempt: (error) => { + console.warn( + `Attempt ${error.attemptNumber} failed. ${ + this.maxRetries - error.attemptNumber + } attempts remaining.` + ); + }, + } + ); + }, + { priority } + ); + } - try { - const response = await axios.get(`${this.baseUrl}${endpoint}`, { - params, - headers: { - "x-api-key": this.apiKey, - }, - }); - - // Cache the response - if (this.cacheManager) { - await this.cacheManager.set(cacheKey, response.data); - } - - return response.data; - } catch (error) { - console.error("Reservoir API error:", error); - throw error; + async getCollections(addresses: string[]): Promise { + // Split addresses into batches + const batches = []; + for (let i = 0; i < addresses.length; i += this.batchSize) { + batches.push(addresses.slice(i, i + this.batchSize)); } + + // Process batches with priority + const results = await Promise.all( + batches.map(async (batch) => { + const priority = Math.max( + ...batch.map((addr) => this.getRequestPriority(addr)) + ); + const data = await this.makeRequest( + `/collections/v6`, + { contract: batch.join(",") }, + priority + ); + return data.collections; + }) + ); + + // Flatten and transform results + return results.flat().map((collection: any) => ({ + address: collection.id, + name: collection.name, + 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.volume24h || 0, + marketCap: collection.marketCap || 0, + totalSupply: collection.tokenCount || 0, + })); } 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, - }; + const collections = await this.getCollections([address]); + return collections[0]; } async getTopCollections(limit: number = 10): Promise { - const data = await this.makeRequest(`/collections/v6`, { - limit, - sortBy: "volume24h", - }); + const priority = 1; // High priority for top collections + const data = await this.makeRequest( + `/collections/v6`, + { + limit, + sortBy: "volume24h", + }, + priority + ); return data.collections.map((collection: any) => ({ address: collection.id, @@ -120,29 +212,39 @@ export class ReservoirService extends Service { } async getMarketStats(address: string) { - return this.makeRequest(`/collections/v6/stats`, { - contract: address, - }); + const priority = this.getRequestPriority(address); + return this.makeRequest( + `/collections/v6/stats`, + { contract: address }, + priority + ); } async getCollectionActivity(address: string, limit: number = 20) { - return this.makeRequest(`/collections/v6/activity`, { - contract: address, - limit, - }); + const priority = this.getRequestPriority(address); + return this.makeRequest( + `/collections/v6/activity`, + { contract: address, limit }, + priority + ); } async getTokens(address: string, limit: number = 20) { - return this.makeRequest(`/tokens/v6`, { - contract: address, - limit, - }); + const priority = this.getRequestPriority(address); + return this.makeRequest( + `/tokens/v6`, + { contract: address, limit }, + priority + ); } async getFloorPrice(address: string) { - const data = await this.makeRequest(`/collections/v6/floor-ask`, { - contract: address, - }); + const priority = this.getRequestPriority(address); + const data = await this.makeRequest( + `/collections/v6/floor-ask`, + { contract: address }, + priority + ); return data.floorAsk?.price?.amount?.native || 0; } } diff --git a/packages/plugin-nft-collections/src/tests/services.test.ts b/packages/plugin-nft-collections/src/tests/services.test.ts index 14d748ad5c..ebeaa3ea6e 100644 --- a/packages/plugin-nft-collections/src/tests/services.test.ts +++ b/packages/plugin-nft-collections/src/tests/services.test.ts @@ -1,7 +1,10 @@ import { describe, expect, it, beforeEach, jest } from "@jest/globals"; import { ReservoirService } from "../services/reservoir"; import { SocialAnalyticsService } from "../services/social-analytics"; -import { CacheManager } from "../services/cache-manager"; +import { + MemoryCacheManager, + type CacheManager, +} from "../services/cache-manager"; import { RateLimiter } from "../services/rate-limiter"; import type { NFTCollection } from "../types"; @@ -13,10 +16,7 @@ describe("NFT Services", () => { let rateLimiter: RateLimiter; beforeEach(() => { - cacheManager = new CacheManager({ - ttl: 3600000, - maxSize: 1000, - }); + cacheManager = new MemoryCacheManager(3600000); rateLimiter = new RateLimiter({ maxRequests: 100, windowMs: 60000, @@ -65,10 +65,7 @@ describe("NFT Services", () => { let rateLimiter: RateLimiter; beforeEach(() => { - cacheManager = new CacheManager({ - ttl: 3600000, - maxSize: 1000, - }); + cacheManager = new MemoryCacheManager(3600000); rateLimiter = new RateLimiter({ maxRequests: 100, windowMs: 60000,