diff --git a/.gitignore b/.gitignore index 86be41efaf2..7c6c92eb7b9 100644 --- a/.gitignore +++ b/.gitignore @@ -60,4 +60,4 @@ agent/content eliza.manifest eliza.manifest.sgx -eliza.sig +eliza.sig \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 7d430c55039..6e49b2043c4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -54,5 +54,6 @@ "package.json": "package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lockb,pnpm-workspace.yaml", "README.md": "*.md", "Dockerfile": "docker-compose-docs.yaml,docker-compose.yaml,Dockerfile.docs" - } + }, + "makefile.configureOnOpen": false } \ No newline at end of file diff --git a/agent/package.json b/agent/package.json index 223a46bb91d..6f1ab186130 100644 --- a/agent/package.json +++ b/agent/package.json @@ -92,6 +92,7 @@ "@elizaos/plugin-hyperliquid": "workspace:*", "@elizaos/plugin-akash": "workspace:*", "@elizaos/plugin-quai": "workspace:*", + "@elizaos/plugin-nft-collections": "workspace:*", "readline": "1.3.0", "ws": "8.18.0", "yargs": "17.7.2" diff --git a/agent/src/index.ts b/agent/src/index.ts index 68a58daa346..f529786934b 100644 --- a/agent/src/index.ts +++ b/agent/src/index.ts @@ -11,7 +11,6 @@ import { SlackClientInterface } from "@elizaos/client-slack"; import { TelegramClientInterface } from "@elizaos/client-telegram"; import { TwitterClientInterface } from "@elizaos/client-twitter"; // import { ReclaimAdapter } from "@elizaos/plugin-reclaim"; -import { DirectClient } from "@elizaos/client-direct"; import { PrimusAdapter } from "@elizaos/plugin-primus"; import { @@ -102,7 +101,7 @@ import net from "net"; import path from "path"; import { fileURLToPath } from "url"; import yargs from "yargs"; -import {dominosPlugin} from "@elizaos/plugin-dominos"; +import createNFTCollectionsPlugin from "@elizaos/plugin-nft-collections"; const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file const __dirname = path.dirname(__filename); // get the name of the directory @@ -152,14 +151,29 @@ function tryLoadFile(filePath: string): string | null { function mergeCharacters(base: Character, child: Character): Character { const mergeObjects = (baseObj: any, childObj: any) => { const result: any = {}; - const keys = new Set([...Object.keys(baseObj || {}), ...Object.keys(childObj || {})]); - keys.forEach(key => { - if (typeof baseObj[key] === 'object' && typeof childObj[key] === 'object' && !Array.isArray(baseObj[key]) && !Array.isArray(childObj[key])) { + const keys = new Set([ + ...Object.keys(baseObj || {}), + ...Object.keys(childObj || {}), + ]); + keys.forEach((key) => { + if ( + typeof baseObj[key] === "object" && + typeof childObj[key] === "object" && + !Array.isArray(baseObj[key]) && + !Array.isArray(childObj[key]) + ) { result[key] = mergeObjects(baseObj[key], childObj[key]); - } else if (Array.isArray(baseObj[key]) || Array.isArray(childObj[key])) { - result[key] = [...(baseObj[key] || []), ...(childObj[key] || [])]; + } else if ( + Array.isArray(baseObj[key]) || + Array.isArray(childObj[key]) + ) { + result[key] = [ + ...(baseObj[key] || []), + ...(childObj[key] || []), + ]; } else { - result[key] = childObj[key] !== undefined ? childObj[key] : baseObj[key]; + result[key] = + childObj[key] !== undefined ? childObj[key] : baseObj[key]; } }); return result; @@ -174,32 +188,36 @@ async function loadCharacter(filePath: string): Promise { let character = JSON.parse(content); validateCharacterConfig(character); - // .id isn't really valid - const characterId = character.id || character.name; - const characterPrefix = `CHARACTER.${characterId.toUpperCase().replace(/ /g, "_")}.`; - const characterSettings = Object.entries(process.env) - .filter(([key]) => key.startsWith(characterPrefix)) - .reduce((settings, [key, value]) => { - const settingKey = key.slice(characterPrefix.length); - return { ...settings, [settingKey]: value }; - }, {}); - if (Object.keys(characterSettings).length > 0) { - character.settings = character.settings || {}; - character.settings.secrets = { - ...characterSettings, - ...character.settings.secrets, - }; - } - // Handle plugins - character.plugins = await handlePluginImporting( - character.plugins - ); + // .id isn't really valid + const characterId = character.id || character.name; + const characterPrefix = `CHARACTER.${characterId.toUpperCase().replace(/ /g, "_")}.`; + const characterSettings = Object.entries(process.env) + .filter(([key]) => key.startsWith(characterPrefix)) + .reduce((settings, [key, value]) => { + const settingKey = key.slice(characterPrefix.length); + return { ...settings, [settingKey]: value }; + }, {}); + if (Object.keys(characterSettings).length > 0) { + character.settings = character.settings || {}; + character.settings.secrets = { + ...characterSettings, + ...character.settings.secrets, + }; + } + // Handle plugins + character.plugins = await handlePluginImporting(character.plugins); if (character.extends) { - elizaLogger.info(`Merging ${character.name} character with parent characters`); + elizaLogger.info( + `Merging ${character.name} character with parent characters` + ); for (const extendPath of character.extends) { - const baseCharacter = await loadCharacter(path.resolve(path.dirname(filePath), extendPath)); + const baseCharacter = await loadCharacter( + path.resolve(path.dirname(filePath), extendPath) + ); character = mergeCharacters(baseCharacter, character); - elizaLogger.info(`Merged ${character.name} with ${baseCharacter.name}`); + elizaLogger.info( + `Merged ${character.name} with ${baseCharacter.name}` + ); } } return character; @@ -472,7 +490,9 @@ function initializeDatabase(dataDir: string) { // Test the connection db.init() .then(() => { - elizaLogger.success("Successfully connected to Supabase database"); + elizaLogger.success( + "Successfully connected to Supabase database" + ); }) .catch((error) => { elizaLogger.error("Failed to connect to Supabase:", error); @@ -489,7 +509,9 @@ function initializeDatabase(dataDir: string) { // Test the connection db.init() .then(() => { - elizaLogger.success("Successfully connected to PostgreSQL database"); + elizaLogger.success( + "Successfully connected to PostgreSQL database" + ); }) .catch((error) => { elizaLogger.error("Failed to connect to PostgreSQL:", error); @@ -504,14 +526,17 @@ function initializeDatabase(dataDir: string) { }); return db; } else { - const filePath = process.env.SQLITE_FILE ?? path.resolve(dataDir, "db.sqlite"); + const filePath = + process.env.SQLITE_FILE ?? path.resolve(dataDir, "db.sqlite"); elizaLogger.info(`Initializing SQLite database at ${filePath}...`); const db = new SqliteDatabaseAdapter(new Database(filePath)); // Test the connection db.init() .then(() => { - elizaLogger.success("Successfully connected to SQLite database"); + elizaLogger.success( + "Successfully connected to SQLite database" + ); }) .catch((error) => { elizaLogger.error("Failed to connect to SQLite:", error); @@ -689,7 +714,8 @@ export async function createAgent( if ( process.env.PRIMUS_APP_ID && process.env.PRIMUS_APP_SECRET && - process.env.VERIFIABLE_INFERENCE_ENABLED === "true"){ + process.env.VERIFIABLE_INFERENCE_ENABLED === "true" + ) { verifiableInferenceAdapter = new PrimusAdapter({ appId: process.env.PRIMUS_APP_ID, appSecret: process.env.PRIMUS_APP_SECRET, @@ -851,8 +877,9 @@ export async function createAgent( getSecret(character, "AKASH_WALLET_ADDRESS") ? akashPlugin : null, - getSecret(character, "QUAI_PRIVATE_KEY") - ? quaiPlugin + getSecret(character, "QUAI_PRIVATE_KEY") ? quaiPlugin : null, + getSecret(character, "RESERVOIR_API_KEY") + ? createNFTCollectionsPlugin() : null, ].filter(Boolean), providers: [], diff --git a/package.json b/package.json index f550c3cfe54..fd6721cbc77 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "devDependencies": { "@commitlint/cli": "18.6.1", "@commitlint/config-conventional": "18.6.3", + "@types/jest": "^29.5.11", "@typescript-eslint/eslint-plugin": "8.16.0", "@typescript-eslint/parser": "8.16.0", "@vitest/eslint-plugin": "1.1.13", @@ -35,18 +36,17 @@ "eslint": "9.16.0", "eslint-config-prettier": "9.1.0", "husky": "9.1.7", + "jest": "^29.7.0", "lerna": "8.1.5", "only-allow": "1.2.1", "prettier": "3.4.1", + "ts-jest": "^29.1.1", "turbo": "2.3.3", "typedoc": "0.26.11", "typescript": "5.6.3", - "vite": "5.4.11", - "vitest": "2.1.5", "viem": "2.21.58", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.11", - "jest": "^29.7.0" + "vite": "5.4.11", + "vitest": "2.1.5" }, "pnpm": { "overrides": { @@ -61,9 +61,12 @@ "@0glabs/0g-ts-sdk": "0.2.1", "@coinbase/coinbase-sdk": "0.10.0", "@deepgram/sdk": "^3.9.0", + "@tensorflow/tfjs-node": "^4.22.0", "@vitest/eslint-plugin": "1.0.1", "amqplib": "0.10.5", + "axios": "^1.7.9", "csv-parse": "5.6.0", + "langdetect": "^0.2.1", "ollama-ai-provider": "0.16.1", "optional": "0.1.4", "pnpm": "9.14.4", @@ -74,4 +77,4 @@ "workspaces": [ "packages/*" ] -} +} \ No newline at end of file diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 3184f53f660..2a311adc4b1 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1554,4 +1554,4 @@ export enum TranscriptionProvider { export enum ActionTimelineType { ForYou = "foryou", Following = "following", -} +} \ No newline at end of file diff --git a/packages/plugin-evm/package.json b/packages/plugin-evm/package.json index cfd8ee1e51f..086ad85e263 100644 --- a/packages/plugin-evm/package.json +++ b/packages/plugin-evm/package.json @@ -35,4 +35,4 @@ "peerDependencies": { "whatwg-url": "7.1.0" } -} +} \ No newline at end of file diff --git a/packages/plugin-nft-collections/.eslintrc.json b/packages/plugin-nft-collections/.eslintrc.json new file mode 100644 index 00000000000..eb6b1760de8 --- /dev/null +++ b/packages/plugin-nft-collections/.eslintrc.json @@ -0,0 +1,8 @@ +{ + "extends": "../../.eslintrc.json", + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" + ], + "root": true +} \ No newline at end of file diff --git a/packages/plugin-nft-collections/.prettierrc b/packages/plugin-nft-collections/.prettierrc new file mode 100644 index 00000000000..3c4f9def446 --- /dev/null +++ b/packages/plugin-nft-collections/.prettierrc @@ -0,0 +1,7 @@ +{ + "tabWidth": 4, + "semi": true, + "singleQuote": false, + "trailingComma": "es5", + "printWidth": 80 +} diff --git a/packages/plugin-nft-collections/README.md b/packages/plugin-nft-collections/README.md new file mode 100644 index 00000000000..39fb66d93e4 --- /dev/null +++ b/packages/plugin-nft-collections/README.md @@ -0,0 +1,1770 @@ +# NFT Collections Plugin + +## 🚀 Unleashing NFT Market Intelligence + +**Transform raw NFT data into actionable insights with the most advanced market intelligence platform.** + +### Why This Matters + +The NFT market is complex, fragmented, and information-rich. Our plugin cuts through the noise, providing: + +- 🔍 **Comprehensive Market Scanning**: Analyze 420+ verified NFT collections in real-time +- 💡 **Intelligent Opportunity Detection**: Identify arbitrage, thin floor, and trading opportunities +- 🧠 **AI-Powered Insights**: Machine learning-driven market predictions and trend analysis + +### Core Capabilities + +- **Market Data Aggregation**: Consolidate information from Reservoir, OpenSea, Alchemy, and more +- **Advanced Arbitrage Detection**: Sophisticated algorithms to spot market inefficiencies +- **Social & Market Sentiment Analysis**: Track community engagement and market trends +- **Performance Optimization**: Intelligent caching, batch processing, and low-latency data retrieval + +### Key Features + +- Real-time NFT collection data and market stats +- Floor price tracking and volume analysis +- Social analytics across Twitter, Discord, and Telegram +- Machine learning-powered price predictions +- Comprehensive security and validation checks + +**Designed for traders, collectors, and market researchers who demand precision and depth in NFT market intelligence.** + +## Recent Improvements + +### Performance Optimizations + +- Implemented batch processing with configurable batch sizes for collection data +- Added parallel request handling with queue management +- Optimized caching with tiered expiration times for different data types +- Added LRU (Least Recently Used) cache with configurable size limits +- Implemented request prioritization for curated collections + +### Enhanced Error Handling + +- Added comprehensive error types and validation +- Implemented retry logic with exponential backoff +- Added detailed error tracking and reporting +- Improved error recovery mechanisms +- Added structured error logging + +### Rate Limiting & Security + +- Added advanced rate limiting with configurable thresholds +- Implemented queue-based request management +- Added per-service rate limiting +- Improved API key management and validation +- Added request validation and sanitization + +### Performance Monitoring + +- Added detailed performance metrics tracking +- Implemented alert system for performance issues +- Added periodic performance reporting +- Added latency, error rate, and throughput monitoring +- Implemented customizable alert thresholds + +### Data Validation + +- Added comprehensive schema validation using Zod +- Implemented strict type checking +- Added data sanitization utilities +- Added Ethereum address validation +- Added price and token ID validation + +### Enhanced Arbitrage Detection Logic 🕵️‍♂️ + +We've significantly upgraded our NFT collection analysis with a sophisticated arbitrage detection mechanism. The new implementation goes beyond simple price comparisons to provide deep market insights: + +#### Key Enhancements + +- **Multi-Listing Analysis**: Fetch and analyze up to 10 listings per collection +- **Dynamic Thresholds**: Configurable thinness and profit margin detection +- **Comprehensive Opportunity Scoring** + - Calculate precise price differences + - Compute potential profit margins + - Evaluate market inefficiencies + +#### Detection Algorithm + +```typescript +const detectThinFloorOpportunities = async (watchlistCollections) => { + const opportunities = []; + + for (const collection of watchlistCollections) { + const listings = await reservoirService.getListings({ + collection: collection.address, + sortBy: "price_asc", + limit: 10, + includeTokenDetails: true, + }); + + // Advanced thin floor detection logic + if (listings.length >= 2) { + const [lowestListing, secondLowestListing] = listings; + + const priceDifference = + secondLowestListing.price - lowestListing.price; + const thinnessPercentage = + (priceDifference / lowestListing.price) * 100; + const potentialProfit = + secondLowestListing.price / lowestListing.price; + + // Flexible threshold checking + if (thinnessPercentage > collection.maxThinnessThreshold) { + opportunities.push({ + collection: collection.address, + lowestPrice: lowestListing.price, + secondLowestPrice: secondLowestListing.price, + thinnessPercentage, + potentialProfit, + tokenIds: [ + lowestListing.tokenId, + secondLowestListing.tokenId, + ], + }); + } + } + } + + return opportunities.sort((a, b) => b.potentialProfit - a.potentialProfit); +}; +``` + +#### Opportunity Insights + +- **Precise Calculation**: Exact price difference computation +- **Flexible Thresholds**: Customizable detection parameters +- **Sorted Opportunities**: Ranked by potential profit +- **Token-Level Details**: Includes specific token IDs for targeted action + +#### Performance Optimizations + +- Batch processing of collection listings +- Efficient sorting and filtering +- Minimal API call overhead +- Configurable detection parameters + +#### Use Cases + +- Rapid arbitrage identification +- Market inefficiency exploitation +- Quick flip strategy development + +**Note**: Always conduct thorough research and understand the risks associated with NFT trading. + +## Features + +### Core Features (Reservoir Tools API) + +- Real-time NFT collection data and market stats +- Floor prices, volume, and market cap tracking +- Collection activity monitoring +- Token-level data and attributes +- Collection statistics and rankings + +### Market Intelligence + +- 420+ verified NFT collections featured on ikigailabs.xyz +- Enhanced metadata and social information +- Prioritized data fetching and caching +- Pre-verified contract addresses +- Featured collections highlighting +- Quick lookup and validation functions + +### Market Data + +- Real-time floor prices and volume tracking +- Market cap and holder statistics +- Price history and trends +- Multi-marketplace activity tracking +- Wash trading detection +- Liquidity analysis +- Price prediction +- Whale activity monitoring +- Market trend analysis + +### Social Analytics + +- Twitter engagement metrics +- Discord community stats +- Telegram group analytics +- Sentiment analysis +- Community growth tracking + +## Quick Start + +### Installation + +```bash +pnpm add @ai16z/plugin-nft-collections +``` + +## Configuration + +### Required Configuration + +```env +# Required +RESERVOIR_API_KEY=your-reservoir-api-key +``` + +### Optional Configuration + +```typescript +import { NFTCollectionsPlugin } from "@elizaos/plugin-nft-collections"; + +const plugin = new NFTCollectionsPlugin({ + caching: { + enabled: true, + ttl: 3600000, // 1 hour + maxSize: 1000, + }, + security: { + rateLimit: { + enabled: true, + maxRequests: 100, + windowMs: 60000, + }, + }, + maxConcurrent: 5, // Maximum concurrent requests + maxRetries: 3, // Maximum retry attempts + batchSize: 20, // Batch size for collection requests +}); + +// Register with your agent +agent.registerPlugin(plugin); +``` + +### Required Environment Variables + +```env +RESERVOIR_API_KEY=your-reservoir-api-key +``` + +### Optional API Keys + +```env +# Market Intelligence +NANSEN_API_KEY=your-nansen-api-key +DUNE_API_KEY=your-dune-api-key +ALCHEMY_API_KEY=your-alchemy-api-key +CHAINBASE_API_KEY=your-chainbase-api-key +NFTSCAN_API_KEY=your-nftscan-api-key + +# Social Analytics +TWITTER_API_KEY=your-twitter-api-key +DISCORD_API_KEY=your-discord-api-key +TELEGRAM_API_KEY=your-telegram-api-key +``` + +## Usage Examples + +### Collection Data + +```typescript +// Get top collections with optimized batch processing +const collections = await nftService.getTopCollections(); + +// Get market intelligence with caching +const intelligence = + await marketIntelligenceService.getMarketIntelligence("0x1234"); + +// Get social metrics with rate limiting +const metrics = await socialAnalyticsService.getSocialMetrics("0x1234"); +``` + +### Error Handling + +```typescript +try { + const collections = await nftService.getTopCollections(); +} catch (error) { + if (error.code === ErrorCode.RATE_LIMIT_EXCEEDED) { + // Handle rate limiting + } else if (error.code === ErrorCode.API_ERROR) { + // Handle API errors + } +} +``` + +### NFT Ownership + +```typescript +// Listen for performance alerts +performanceMonitor.on("alert", (alert) => { + console.log(`Performance alert: ${alert.type} for ${alert.operation}`); +}); + +// Get performance summary +const summary = performanceMonitor.getPerformanceSummary(); +``` + +## Performance Benchmarks + +### Response Times (p95) + +``` +Operation Cold Cached Batch (100) +Collection Data 300ms 50ms 2.5s +Floor Price 150ms 25ms 1.2s +Token Metadata 250ms 40ms 2.0s +Market Stats 400ms 75ms 3.0s +Social Metrics 350ms 60ms 2.8s +``` + +### Building + +```bash +pnpm build +``` + +### Resource Usage + +``` +Resource Idle Light Heavy +CPU 0.5% 15% 40% +Memory 150MB 300MB 600MB +Requests/s 10 100 1000 +``` + +## Best Practices + +1. **API Keys** + + - Secure storage of API keys + - Regular key rotation + - Use fallback keys for high availability + +2. **Error Handling** + + - Implement retry strategies + - Handle rate limits gracefully + - Log errors with context + +3. **Performance** + + - Use batch operations when possible + - Implement appropriate caching + - Monitor resource usage + +4. **Data Validation** + - Validate all input data + - Sanitize API responses + - Check Ethereum addresses + +## Architecture + +### System Components + +```mermaid +graph TD + A[Client] --> B[Plugin Interface] + B --> C[Cache Layer] + C --> D[API Manager] + D --> E[Reservoir API] + D --> F[Market APIs] + D --> G[Social APIs] + H[Monitor] --> I[Metrics] + H --> J[Alerts] +``` + +## Contributing + +1. Fork the repository +2. Create your feature branch +3. Commit your changes +4. Push to the branch +5. Create a Pull Request + +## License + +MIT + +## Support + +### Error Handling Flow + +```mermaid +graph TD + A[API Call] --> B{Error?} + B -->|Yes| C[Retry Strategy] + C -->|Success| D[Return Data] + C -->|Fail| E[Fallback API] + E -->|Success| D + E -->|Fail| F[Error Response] + B -->|No| D +``` + +### Optimization Strategies + +```mermaid +graph TD + A[Incoming Request] --> B{Optimizable?} + B -->|Yes| C[Batch Processing] + B -->|No| D[Direct Processing] + C --> E[Parallel Execution] + C --> F[Queue Management] + E --> G[Result Aggregation] + F --> G + D --> G +``` + +## Integrations + +### GraphQL Support + +```env +# GraphQL Configuration +GRAPHQL_ENDPOINT=your-graphql-endpoint +GRAPHQL_API_KEY=your-graphql-key +``` + +```typescript +// Query collections using GraphQL +const collections = await plugin.graphql.query( + ` + query GetCollections($first: Int!) { + collections(first: $first) { + id + name + floorPrice + volume24h + } + } +`, + { first: 10 } +); + +// Subscribe to collection updates +const subscription = plugin.graphql.subscribe( + ` + subscription OnFloorPriceChange($collectionId: ID!) { + floorPriceChanged(collectionId: $collectionId) { + newPrice + oldPrice + timestamp + } + } +`, + { collectionId: "0x1234" } +); +``` + +### WebSocket Real-time Updates + +```env +# WebSocket Configuration +WS_ENDPOINT=your-websocket-endpoint +WS_API_KEY=your-websocket-key +``` + +```typescript +// Subscribe to real-time collection updates +plugin.ws.subscribe("collection:0x1234", (update) => { + console.log("New floor price:", update.floorPrice); +}); + +// Subscribe to multiple events +plugin.ws.subscribeMany( + ["sales:0x1234", "listings:0x1234", "transfers:0x1234"], + (event) => { + console.log("Event type:", event.type); + console.log("Event data:", event.data); + } +); + +// Custom event filters +plugin.ws.subscribe( + "sales:*", + { + priceAbove: "10 ETH", + marketplace: ["opensea", "blur"], + }, + (sale) => { + console.log("Whale sale detected:", sale); + } +); +``` + +### IPFS Integration + +```env +# IPFS Configuration +IPFS_GATEWAY=your-ipfs-gateway +IPFS_API_KEY=your-ipfs-key +IPFS_FALLBACK_GATEWAYS=["https://ipfs.io", "https://cloudflare-ipfs.com"] +``` + +```typescript +// Fetch metadata from IPFS +const metadata = await plugin.ipfs.getMetadata("ipfs://Qm..."); + +// Upload metadata to IPFS +const cid = await plugin.ipfs.uploadMetadata({ + name: "Cool NFT", + description: "Very cool NFT", + image: "ipfs://Qm...", +}); + +// Pin content across multiple providers +await plugin.ipfs.pin(cid, { + providers: ["pinata", "web3.storage"], + replicas: 3, +}); + +// Smart gateway selection +const image = await plugin.ipfs.getImage(cid, { + preferredGateway: "cloudflare", + size: "thumbnail", + format: "webp", +}); +``` + +### Integration Best Practices + +1. **GraphQL** + + - Use fragments for reusable queries + - Implement proper error boundaries + - Cache complex queries + - Use persisted queries for production + +2. **WebSocket** + + - Implement reconnection logic + - Handle backpressure + - Use heartbeats + - Batch small updates + - Implement message queue for offline scenarios + +3. **IPFS** + - Use multiple gateway fallbacks + - Implement proper timeout handling + - Cache frequently accessed content + - Use appropriate gateway for content type + - Monitor gateway health + +### Integration Architecture + +```mermaid +graph TD + A[Plugin Core] --> B[GraphQL Client] + A --> C[WebSocket Manager] + A --> D[IPFS Gateway] + + B --> E[Query Builder] + B --> F[Subscription Manager] + + C --> G[Event Stream] + C --> H[Connection Pool] + + D --> I[Gateway Router] + D --> J[Content Cache] + + E --> K[API Endpoint] + F --> K + G --> L[WS Endpoint] + H --> L + I --> M[IPFS Network] +``` + +## Extended Features + +### Webhooks + +```env +# Webhook Configuration +WEBHOOK_SECRET=your-webhook-secret +WEBHOOK_RETRY_COUNT=3 +WEBHOOK_TIMEOUT=5000 +``` + +```typescript +// Register webhook endpoints +const webhook = plugin.webhooks.create({ + url: "https://api.yourdomain.com/webhooks/nft", + events: ["floor_change", "volume_spike", "whale_transfer"], + secret: process.env.WEBHOOK_SECRET, + metadata: { + name: "Price Monitor", + description: "Monitor floor price changes", + }, +}); + +// Configure event filters +webhook.addFilter({ + event: "floor_change", + conditions: { + percentageChange: ">5%", + timeWindow: "1h", + minVolume: "10 ETH", + }, +}); + +webhook.addFilter({ + event: "whale_transfer", + conditions: { + value: ">100 ETH", + fromAddress: ["!0x0000000000000000000000000000000000000000"], + toAddress: ["!0x0000000000000000000000000000000000000000"], + }, +}); + +// Handle webhook delivery status +webhook.on("delivered", (event) => { + console.log("Webhook delivered:", event.id); +}); + +webhook.on("failed", (event, error) => { + console.error("Webhook failed:", error); +}); +``` + +### ML-Powered Price Predictions + +```typescript +// Get price prediction for a collection +const prediction = await plugin.ml.predictPrice("0x1234", { + timeframe: "24h", + confidence: 0.8, + includeFactors: true, +}); + +// Response type +interface PricePrediction { + timeframe: "1h" | "24h" | "7d"; + currentPrice: number; + predictedPrice: number; + confidence: number; + factors: { + reason: string; + impact: number; + confidence: number; + }[]; + marketConditions: { + trend: "bullish" | "bearish" | "neutral"; + volatility: "high" | "medium" | "low"; + liquidity: "high" | "medium" | "low"; + }; +} + +// Batch predictions for multiple collections +const predictions = await plugin.ml.batchPredictPrice([ + { address: "0x1234", timeframe: "1h" }, + { address: "0x5678", timeframe: "24h" }, +]); + +// Get historical prediction accuracy +const accuracy = await plugin.ml.getPredictionAccuracy("0x1234", { + timeframe: "7d", + startDate: "2024-01-01", + endDate: "2024-01-07", +}); + +// Train custom prediction model +const model = await plugin.ml.trainCustomModel({ + collections: ["0x1234", "0x5678"], + features: ["volume", "social_sentiment", "whale_activity"], + timeframe: "24h", + trainingPeriod: "30d", +}); +``` + +### Advanced Analytics + +```typescript +// Rarity analysis with ML +const rarityScore = await plugin.ml.analyzeRarity("0x1234", "tokenId", { + method: "trait_rarity" | "statistical" | "neural", + includeExplanation: true, +}); + +// Wash trading detection +const tradeAnalysis = await plugin.ml.analyzeTrades("0x1234", { + timeframe: "24h", + minConfidence: 0.8, + includeEvidence: true, +}); + +// Market manipulation detection +const manipulationScore = await plugin.ml.detectManipulation("0x1234", { + indicators: ["wash_trading", "price_manipulation", "fake_volume"], + sensitivity: "high" | "medium" | "low", +}); +``` + +### Custom Alerts + +```typescript +// Set up custom alerts +const alert = plugin.alerts.create({ + name: "Whale Alert", + conditions: { + event: "transfer", + filters: { + value: ">50 ETH", + collectionAddress: "0x1234", + }, + }, + actions: [ + { + type: "webhook", + url: "https://api.yourdomain.com/alerts", + }, + { + type: "email", + to: "trader@domain.com", + }, + ], +}); + +// Alert with ML insights +const smartAlert = plugin.alerts.createWithML({ + name: "Smart Price Alert", + conditions: { + event: "price_prediction", + filters: { + confidence: ">0.8", + priceChange: ">10%", + timeframe: "24h", + }, + }, + mlConfig: { + model: "price_prediction", + features: ["market_sentiment", "whale_activity"], + }, +}); +``` + +### Feature Configuration + +```typescript +interface ExtendedFeatureConfig { + webhooks: { + maxRetries: number; + timeout: number; + batchSize: number; + rateLimits: { + perSecond: number; + perMinute: number; + }; + }; + ml: { + models: { + price: string; + rarity: string; + manipulation: string; + }; + updateFrequency: number; + minConfidence: number; + maxBatchSize: number; + }; + alerts: { + maxPerUser: number; + cooldown: number; + maxActions: number; + }; +} +``` + +### Extended Features Architecture + +```mermaid +graph TD + A[Plugin Core] --> B[Webhook Manager] + A --> C[ML Engine] + A --> D[Alert System] + + B --> E[Event Filter] + B --> F[Delivery Manager] + + C --> G[Price Predictor] + C --> H[Rarity Analyzer] + C --> I[Manipulation Detector] + + D --> J[Condition Evaluator] + D --> K[Action Executor] + + E --> L[Event Stream] + F --> M[Retry Queue] + + G --> N[Model Registry] + H --> N + I --> N + + J --> O[Alert Queue] + K --> P[Notification Service] +``` + +## Testing & Validation + +### Mock Data Generation + +```typescript +// Generate mock collections and transactions +const mockData = await plugin.testing.generateMockData({ + collections: 10, + transactions: 1000, + timeRange: [new Date("2024-01-01"), new Date("2024-01-07")], + options: { + priceRange: [0.1, 100], + traits: ["background", "body", "eyes", "mouth"], + rarityDistribution: "normal", + marketplaces: ["opensea", "blur", "x2y2"], + }, +}); + +// Generate realistic market activity +const marketActivity = await plugin.testing.generateMarketActivity({ + collection: "0x1234", + activityType: ["sales", "listings", "offers"], + volumeProfile: "whale_accumulation", + priceVolatility: "high", + duration: "7d", +}); + +// Generate social signals +const socialData = await plugin.testing.generateSocialData({ + sentiment: "bullish", + engagement: "viral", + platforms: ["twitter", "discord"], + influencerActivity: true, +}); +``` + +### Contract Validation + +```typescript +// Validate collection contract +const validation = await plugin.validation.validateContract("0x1234", { + checkERC: ["721", "1155"], + securityCheck: true, + options: { + checkOwnership: true, + checkRoyalties: true, + checkMetadata: true, + checkPermissions: true, + }, +}); + +// Response type +interface ValidationResult { + isValid: boolean; + standards: { + erc721: boolean; + erc1155: boolean; + erc2981: boolean; // Royalties + }; + security: { + maliciousCode: boolean; + knownExploits: boolean; + upgradeability: { + isUpgradeable: boolean; + adminAddress: string; + timelock: number; + }; + permissions: { + owner: string; + minter: string[]; + pauser: string[]; + }; + }; + metadata: { + isValid: boolean; + baseURI: string; + frozen: boolean; + }; +} + +// Batch validate multiple contracts +const batchValidation = await plugin.validation.batchValidateContracts( + ["0x1234", "0x5678"], + { + checkERC: ["721"], + securityCheck: true, + } +); +``` + +### Testing Utilities + +```typescript +// Time travel for testing +await plugin.testing.timeTravel({ + collection: "0x1234", + destination: new Date("2024-06-01"), + preserveState: true, +}); + +// Market simulation +await plugin.testing.simulateMarket({ + scenario: "bear_market", + duration: "30d", + collections: ["0x1234"], + variables: { + priceDecline: 0.5, + volumeReduction: 0.7, + sellerPanic: true, + }, +}); + +// Load testing +const loadTest = await plugin.testing.runLoadTest({ + concurrent: 100, + duration: "5m", + operations: ["getFloor", "getMetadata", "getTrades"], + targetRPS: 50, +}); +``` + +### Test Fixtures + +```typescript +// Collection fixture +const fixture = plugin.testing.createFixture({ + type: "collection", + traits: { + background: ["red", "blue", "green"], + body: ["type1", "type2"], + accessory: ["hat", "glasses"], + }, + supply: 1000, + distribution: "random", +}); + +// Market fixture +const marketFixture = plugin.testing.createMarketFixture({ + floorPrice: 1.5, + listings: 50, + topBid: 2.0, + volume24h: 100, + holders: 500, +}); + +// Event fixture +const eventFixture = plugin.testing.createEventFixture({ + type: "sale", + price: 5.0, + marketplace: "opensea", + timestamp: new Date(), +}); +``` + +### Testing Configuration + +```typescript +interface TestConfig { + mock: { + seed?: string; + deterministic: boolean; + networkLatency: number; + errorRate: number; + }; + validation: { + timeout: number; + retries: number; + concurrency: number; + }; + fixtures: { + cleanup: boolean; + persistence: "memory" | "disk"; + sharing: boolean; + }; +} +``` + +### Test Helpers + +```typescript +// Snapshot testing +const snapshot = await plugin.testing.createSnapshot("0x1234"); +await plugin.testing.compareSnapshots(snapshot, latestSnapshot); + +// Event assertions +await plugin.testing.assertEvent({ + type: "sale", + collection: "0x1234", + matcher: { + price: ">1 ETH", + buyer: "0x5678", + }, +}); + +// Market assertions +await plugin.testing.assertMarketState({ + collection: "0x1234", + conditions: { + floorPrice: ">1 ETH", + listings: ">10", + volume24h: ">100 ETH", + }, +}); +``` + +### Testing Architecture + +```mermaid +graph TD + A[Test Runner] --> B[Mock Generator] + A --> C[Validation Engine] + A --> D[Test Utilities] + + B --> E[Collection Mocks] + B --> F[Transaction Mocks] + B --> G[Market Mocks] + + C --> H[Contract Validator] + C --> I[Security Scanner] + C --> J[Standards Checker] + + D --> K[Time Machine] + D --> L[Market Simulator] + D --> M[Load Tester] + + E --> N[Test Execution] + F --> N + G --> N + + H --> O[Validation Results] + I --> O + J --> O + + K --> P[Test Results] + L --> P + M --> P +``` + +## Authentication & Security + +### API Key Management + +```typescript +// Configure API keys with rotation and fallback +const apiConfig = plugin.auth.configureAPI({ + primary: { + key: process.env.PRIMARY_API_KEY, + rotationSchedule: "0 0 * * *", // Daily rotation + rotationCallback: async (oldKey) => { + await notifyKeyExpiry(oldKey); + }, + }, + fallback: { + key: process.env.FALLBACK_API_KEY, + useCondition: (error) => error.status === 429 || error.status === 503, + }, + rotation: { + enabled: true, + interval: 86400000, // 24 hours in ms + strategy: "gradual", // or "immediate" + }, +}); + +// Key rotation handlers +plugin.auth.onKeyRotation(async (newKey, oldKey) => { + await updateKeyInVault(newKey); + await invalidateOldKey(oldKey); +}); + +// Automatic key validation +await plugin.auth.validateKeys({ + checkInterval: 3600000, // 1 hour + healthEndpoint: "/health", + timeout: 5000, +}); +``` + +### Rate Limiting + +```typescript +// Configure rate limits +const rateLimiter = plugin.security.configureRateLimits({ + global: { + maxRequests: 1000, + windowMs: 60000, // 1 minute + retryAfter: 60000, + }, + endpoints: { + "/collections": { + maxRequests: 100, + windowMs: 60000, + retryAfter: 30000, + }, + "/market-data": { + maxRequests: 50, + windowMs: 60000, + retryAfter: 60000, + }, + }, + strategies: { + type: "sliding-window", + errorHandling: "queue", // or "reject" + }, +}); + +// Custom rate limit handlers +rateLimiter.onLimitReached(async (context) => { + await notifyRateLimitExceeded(context); + return plugin.security.getBackoffStrategy(context); +}); + +// Distributed rate limiting with Redis +const distributedLimiter = plugin.security.createDistributedRateLimiter({ + redis: { + host: process.env.REDIS_HOST, + port: 6379, + password: process.env.REDIS_PASSWORD, + }, + sync: { + interval: 1000, + strategy: "eventual-consistency", + }, +}); +``` + +### Security Features + +```typescript +// Enable security features +const security = plugin.security.configure({ + encryption: { + algorithm: "aes-256-gcm", + keyRotation: true, + rotationInterval: 7776000000, // 90 days + }, + authentication: { + type: "jwt", + expiresIn: "24h", + refreshToken: true, + }, + headers: { + helmet: true, + cors: { + origin: ["https://yourdomain.com"], + methods: ["GET", "POST"], + }, + }, +}); + +// Request signing +const signedRequest = plugin.security.signRequest({ + method: "POST", + url: "/api/v1/trades", + body: tradeData, + nonce: Date.now(), + expiry: "5m", +}); + +// Payload encryption +const encryptedData = await plugin.security.encryptPayload(sensitiveData, { + algorithm: "aes-256-gcm", + keyId: "current", + metadata: { + purpose: "api-communication", + }, +}); +``` + +### Access Control + +```typescript +// Configure access control +const accessControl = plugin.security.configureAccess({ + roles: { + admin: { + permissions: ["read", "write", "delete"], + rateLimit: { multiplier: 2 }, + }, + user: { + permissions: ["read"], + rateLimit: { multiplier: 1 }, + }, + }, + resources: { + collections: ["read", "write"], + trades: ["read", "write", "delete"], + analytics: ["read"], + }, +}); + +// Role-based middleware +const authMiddleware = plugin.security.createAuthMiddleware({ + validateToken: true, + checkPermissions: true, + auditLog: true, +}); + +// IP allowlisting +const ipFilter = plugin.security.createIPFilter({ + allowlist: ["192.168.1.0/24"], + denylist: ["10.0.0.0/8"], + mode: "strict", +}); +``` + +### Audit Logging + +```typescript +// Configure audit logging +const auditLogger = plugin.security.configureAuditLog({ + storage: { + type: "elasticsearch", + config: { + node: process.env.ELASTICSEARCH_URL, + index: "nft-audit-logs", + }, + }, + retention: { + duration: "90d", + archival: true, + }, + events: { + "api.request": true, + "auth.login": true, + "data.modification": true, + }, +}); + +// Log security events +await auditLogger.log({ + action: "api.request", + actor: "user-123", + resource: "collection-456", + details: { + method: "GET", + path: "/api/v1/collections", + status: 200, + }, +}); + +// Query audit logs +const auditTrail = await auditLogger.query({ + timeRange: { + start: "2024-01-01", + end: "2024-01-07", + }, + filters: { + action: ["api.request", "auth.login"], + actor: "user-123", + }, +}); +``` + +### Security Configuration + +```typescript +interface SecurityConfig { + api: { + keys: { + rotation: { + enabled: boolean; + interval: number; + strategy: "gradual" | "immediate"; + }; + validation: { + interval: number; + timeout: number; + }; + }; + rateLimit: { + global: RateLimitConfig; + endpoints: Record; + distributed: boolean; + }; + }; + encryption: { + algorithm: string; + keyRotation: boolean; + rotationInterval: number; + }; + access: { + roles: Record; + resources: Record; + audit: { + enabled: boolean; + retention: string; + }; + }; +} +``` + +### Security Architecture + +```mermaid +graph TD + A[Plugin Core] --> B[Auth Manager] + A --> C[Rate Limiter] + A --> D[Security Manager] + + B --> E[Key Rotation] + B --> F[Key Validation] + + C --> G[Request Counter] + C --> H[Rate Rules] + + D --> I[Encryption] + D --> J[Access Control] + D --> K[Audit Logger] + + E --> L[Key Storage] + F --> L + + G --> M[Redis Cache] + H --> M + + I --> N[Key Management] + J --> O[Role Manager] + K --> P[Log Storage] +``` + +## Trading Agents + +### Agent Configuration + +```typescript +// Configure a trading agent +const tradingAgent = plugin.agents.createTradingAgent({ + name: "WhaleWatcher", + personality: { + style: "aggressive", + riskTolerance: "high", + tradingHours: "24/7", + }, + strategies: [ + { + name: "whale_following", + config: { + minTransactionValue: "100 ETH", + followDelay: "1m", + maxExposure: "500 ETH", + }, + }, + { + name: "floor_sweeping", + config: { + targetCollections: ["0x1234", "0x5678"], + maxPricePerItem: "2 ETH", + totalBudget: "50 ETH", + }, + }, + ], +}); + +// Configure agent communication +const agentNetwork = plugin.agents.createNetwork({ + agents: [tradingAgent, otherAgent], + communicationRules: { + shareMarketInsights: true, + coordinateTrading: true, + profitSharing: 0.5, + }, +}); + +// Set up agent behaviors +tradingAgent.on("whale_movement", async (event) => { + const analysis = await plugin.ml.analyzeWhaleMovement(event); + if (analysis.confidence > 0.8) { + await tradingAgent.executeStrategy("whale_following", { + collection: event.collection, + amount: analysis.recommendedAmount, + }); + } +}); +``` + +### Multi-Agent Trading Strategies + +```typescript +// Collaborative floor sweeping +const floorSweepTeam = plugin.agents.createTeam({ + name: "FloorSweepers", + members: [agent1, agent2, agent3], + strategy: { + type: "distributed_sweep", + config: { + totalBudget: "100 ETH", + maxPricePerAgent: "35 ETH", + targetCollections: ["0x1234"], + coordination: { + type: "price_zones", + zones: [ + { range: "0-1 ETH", agent: "agent1" }, + { range: "1-2 ETH", agent: "agent2" }, + { range: "2+ ETH", agent: "agent3" }, + ], + }, + }, + }, +}); + +// Market making strategy +const marketMaker = plugin.agents.createMarketMaker({ + collections: ["0x1234"], + strategy: { + spreadTarget: 0.05, + maxInventory: "10 ETH", + rebalanceThreshold: 0.02, + hedging: { + enabled: true, + instruments: ["wETH", "NFT indexes"], + }, + }, +}); +``` + +### Agent Learning & Adaptation + +```typescript +// Train agent on historical data +await tradingAgent.learn({ + dataset: "historical_trades", + timeframe: "90d", + features: ["whale_movements", "price_action", "social_sentiment"], + reinforcementConfig: { + rewardFunction: "profit_and_risk", + episodes: 1000, + batchSize: 64, + }, +}); + +// Adaptive strategy adjustment +tradingAgent.enableAdaptation({ + metrics: ["profit_loss", "win_rate", "drawdown"], + adjustmentPeriod: "1d", + thresholds: { + drawdown: { + max: 0.1, + action: "reduce_exposure", + }, + profitTarget: { + min: 0.2, + action: "increase_aggression", + }, + }, +}); +``` + +### Agent Monitoring & Analytics + +```typescript +// Monitor agent performance +const performance = await plugin.agents.getPerformance({ + agentId: tradingAgent.id, + timeframe: "30d", + metrics: ["total_profit", "win_rate", "avg_position_size", "max_drawdown"], +}); + +// Agent activity dashboard +const dashboard = plugin.agents.createDashboard({ + agents: [tradingAgent, marketMaker], + realtime: true, + metrics: { + performance: true, + activities: true, + insights: true, + }, + alerts: { + profitThreshold: "5 ETH", + lossThreshold: "2 ETH", + unusualActivity: true, + }, +}); +``` + +### Agent Architecture + +```mermaid +graph TD + A[Trading Agent] --> B[Strategy Manager] + A --> C[Learning Module] + A --> D[Communication Hub] + + B --> E[Whale Following] + B --> F[Floor Sweeping] + B --> G[Market Making] + + C --> H[Historical Analysis] + C --> I[Reinforcement Learning] + C --> J[Strategy Adaptation] + + D --> K[Agent Network] + D --> L[Team Coordination] + D --> M[Market Updates] + + E --> N[Execution Engine] + F --> N + G --> N + + H --> O[Performance Analytics] + I --> O + J --> O + + K --> P[Multi-Agent System] + L --> P + M --> P +``` + +## Caching Layer + +### Cache Configuration + +```typescript +// Configure multi-level caching +const cacheConfig = plugin.cache.configure({ + layers: { + memory: { + type: "memory", + maxSize: "1GB", + ttl: "1m", + priority: 1, + }, + redis: { + type: "redis", + connection: { + host: process.env.REDIS_HOST, + port: 6379, + password: process.env.REDIS_PASSWORD, + }, + ttl: "5m", + priority: 2, + }, + disk: { + type: "disk", + path: "./cache", + maxSize: "10GB", + ttl: "1h", + priority: 3, + }, + }, + strategies: { + preload: ["top_collections", "trending_collections"], + warmup: { + interval: "10m", + concurrency: 5, + }, + }, +}); + +// Configure per-collection caching +const collectionCache = plugin.cache.createCollectionCache({ + collection: "0x1234", + rules: { + metadata: { + ttl: "1d", + invalidateOn: ["metadata_update"], + }, + floorPrice: { + ttl: "30s", + invalidateOn: ["new_listing", "sale"], + }, + holders: { + ttl: "1h", + invalidateOn: ["transfer"], + }, + }, +}); +``` + +### Smart Caching Strategies + +```typescript +// Implement predictive caching +const predictiveCache = plugin.cache.enablePredictiveCaching({ + features: { + userBehavior: true, + timePatterns: true, + marketActivity: true, + }, + ml: { + model: "cache_prediction", + updateInterval: "1h", + minConfidence: 0.8, + }, +}); + +// Configure cache warming +const cacheWarmer = plugin.cache.createWarmer({ + schedule: "*/10 * * * *", // Every 10 minutes + strategy: { + type: "smart", + priorities: { + popularity: 0.4, + recentActivity: 0.3, + userRequests: 0.3, + }, + }, + limits: { + maxConcurrent: 5, + maxItems: 1000, + }, +}); +``` + +### Cache Monitoring + +```typescript +// Monitor cache performance +const cacheMetrics = plugin.cache.monitor({ + metrics: ["hit_rate", "miss_rate", "latency", "size"], + alerts: { + hitRate: { + threshold: 0.8, + window: "5m", + action: "adjust_ttl", + }, + latency: { + threshold: 100, + window: "1m", + action: "scale_cache", + }, + }, +}); + +// Cache analytics dashboard +const cacheDashboard = plugin.cache.createDashboard({ + realtime: true, + metrics: { + performance: true, + storage: true, + invalidations: true, + }, + visualization: { + graphs: true, + heatmaps: true, + }, +}); +``` + +### Cache Optimization + +```typescript +// Optimize cache storage +const storageOptimizer = plugin.cache.optimizeStorage({ + compression: { + enabled: true, + algorithm: "lz4", + level: "medium", + }, + deduplication: true, + partitioning: { + strategy: "access_pattern", + shards: 4, + }, +}); + +// Implement cache coherency +const coherencyManager = plugin.cache.manageCoherency({ + strategy: "write_through", + consistency: "eventual", + propagation: { + method: "pub_sub", + maxDelay: "100ms", + }, +}); +``` + +### Cache Architecture + +```mermaid +graph TD + A[Cache Manager] --> B[Memory Cache] + A --> C[Redis Cache] + A --> D[Disk Cache] + + E[Cache Warmer] --> A + F[Predictive Engine] --> A + G[Monitoring] --> A + + B --> H[Fast Access Layer] + C --> I[Distributed Layer] + D --> J[Persistence Layer] + + K[Optimization] --> B + K --> C + K --> D + + L[Coherency Manager] --> M[Write Through] + L --> N[Invalidation] + L --> O[Propagation] + + P[Analytics] --> Q[Performance] + P --> R[Usage Patterns] + P --> S[Optimization Suggestions] +``` + +## Thin Floor Buying Opportunities + +### Market Intelligence: Thin Floor Detection + +The NFT Collections Plugin introduces an advanced market intelligence feature for identifying high-potential buying opportunities through "Thin Floor" detection. This strategy focuses on finding NFT collections with significant price disparities between the lowest and second-lowest listings. + +#### Key Features + +- **Thin Floor Analysis**: Automatically detect collections with substantial price differences +- **Watchlist Management**: Track and monitor specific collections of interest +- **Configurable Thresholds**: Customize thinness and opportunity detection parameters + +#### How Thin Floor Detection Works + +```typescript +// Example of thin floor opportunity detection +const thinFloorOpportunities = await nftService.getThinFloorNFTs({ + maxThinnessThreshold: 0.2, // 20% price difference + minFloorPrice: 0.5, // Minimum floor price in ETH + maxFloorPrice: 10, // Maximum floor price in ETH +}); +``` + +#### Buying Opportunity Strategies + +1. **Price Arbitrage**: Identify collections where you can quickly buy and relist at a higher price +2. **Market Inefficiency Exploitation**: Detect pricing inconsistencies across marketplaces +3. **Rapid Profit Potential**: Find collections with quick flip opportunities + +#### Watchlist Management + +```typescript +// Add a collection to the watchlist for thin floor monitoring +await watchlist.addToWatchlist({ + address: "0x1234...", + maxThinnessThreshold: 0.15, // 15% thinness threshold + category: "PFP", + notificationPreferences: { + email: true, + telegram: true, + }, +}); +``` + +#### Advanced Filtering + +```typescript +// Filter thin floor opportunities with advanced criteria +const filteredOpportunities = watchlist.getWatchlist({ + category: "Art", + priceRange: { min: 1, max: 5 }, + thinnessRange: { min: 0.1, max: 0.3 }, +}); +``` + +#### Notification System + +- **Real-time Alerts**: Get instant notifications for thin floor opportunities +- **Multi-channel Support**: Email, Telegram, Discord webhooks +- **Customizable Filters**: Set personal thresholds and preferences + +### Performance Considerations + +- **Reduced API Calls**: Intelligent watchlist management +- **Configurable Batch Processing**: Optimize resource utilization +- **Caching Mechanisms**: Fast and efficient opportunity detection + +### Best Practices + +1. Start with a small, diverse watchlist +2. Experiment with different thinness thresholds +3. Monitor and adjust your strategy regularly +4. Use multiple marketplaces for comprehensive analysis + +### Disclaimer + +Trading NFTs involves significant risk. Always conduct thorough research and never invest more than you can afford to lose. diff --git a/packages/plugin-nft-collections/package.json b/packages/plugin-nft-collections/package.json new file mode 100644 index 00000000000..1d7becb1dfd --- /dev/null +++ b/packages/plugin-nft-collections/package.json @@ -0,0 +1,34 @@ +{ + "name": "@elizaos/plugin-nft-collections", + "version": "0.1.0", + "description": "NFT collections plugin for Eliza", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsup src/index.ts --format esm --dts", + "test": "vitest run", + "test:watch": "vitest", + "lint": "eslint src --ext .ts", + "format": "prettier --write src/**/*.ts" + }, + "dependencies": { + "@elizaos/core": "workspace:*", + "@elizaos/plugin-evm": "workspace:*", + "axios": "^1.6.7", + "rate-limiter-flexible": "^5.0.4" + }, + "devDependencies": { + "@types/node": "^20.11.16", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint": "^8.56.0", + "prettier": "^3.2.5", + "tsup": "^8.0.1", + "typescript": "^5.3.3", + "vitest": "^2.1.5" + }, + "peerDependencies": { + "@elizaos/core": "workspace:*" + } +} diff --git a/packages/plugin-nft-collections/packages/plugin-nft-collections/src/utils/errors.ts b/packages/plugin-nft-collections/packages/plugin-nft-collections/src/utils/errors.ts new file mode 100644 index 00000000000..899596afcce --- /dev/null +++ b/packages/plugin-nft-collections/packages/plugin-nft-collections/src/utils/errors.ts @@ -0,0 +1 @@ +export class BaseError extends Error { public readonly isOperational: boolean; public context?: Record; constructor(message: string, options: { isOperational?: boolean, context?: Record } = {}) { super(message); this.name = this.constructor.name; this.isOperational = options.isOperational ?? true; this.context = options.context || {}; Error.captureStackTrace(this, this.constructor); } } diff --git a/packages/plugin-nft-collections/src/__tests__/reservoir.test.ts b/packages/plugin-nft-collections/src/__tests__/reservoir.test.ts new file mode 100644 index 00000000000..60ad8530e18 --- /dev/null +++ b/packages/plugin-nft-collections/src/__tests__/reservoir.test.ts @@ -0,0 +1,47 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { IAgentRuntime } from "@elizaos/core"; +import { ReservoirService } from "../services/reservoir"; +import { MemoryCacheManager } from "../services/cache-manager"; +import { RateLimiter } from "../services/rate-limiter"; + +describe("ReservoirService", () => { + const mockRuntime = { + services: { + get: vi.fn(), + }, + messageManager: { + createMemory: vi.fn(), + }, + agentId: "00000000-0000-0000-0000-000000000000", + } as unknown as IAgentRuntime; + + let service: ReservoirService; + let cacheManager: MemoryCacheManager; + let rateLimiter: RateLimiter; + + beforeEach(() => { + cacheManager = new MemoryCacheManager(); + rateLimiter = new RateLimiter(); + service = new ReservoirService({ + cacheManager, + rateLimiter, + }); + }); + + it("should initialize correctly", async () => { + await service.initialize(mockRuntime); + expect(service).toBeDefined(); + }); + + it("should handle API requests with caching", async () => { + const mockData = { collections: [] }; + vi.spyOn(global, "fetch").mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockData), + } as Response); + + const result = await service.getTopCollections(5); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + }); +}); diff --git a/packages/plugin-nft-collections/src/actions/get-collections.ts b/packages/plugin-nft-collections/src/actions/get-collections.ts new file mode 100644 index 00000000000..0a296e900b3 --- /dev/null +++ b/packages/plugin-nft-collections/src/actions/get-collections.ts @@ -0,0 +1,1149 @@ +import { State } from "@elizaos/core"; +import { HandlerCallback } from "@elizaos/core"; +import { Action, IAgentRuntime, Memory, Provider } from "@elizaos/core"; +import { + CURATED_COLLECTIONS, + CuratedCollection, +} from "../constants/curated-collections"; +import { z } from "zod"; +import fs from "fs/promises"; +import path from "path"; + +// Enhanced Watchlist Entry Schema +const WatchlistEntrySchema = z.object({ + address: z.string(), + name: z.string().optional(), + maxThinnessThreshold: z.number().optional().default(15), + minFloorPrice: z.number().optional(), + maxFloorPrice: z.number().optional(), + minProfitMargin: z.number().optional().default(2), + maxListingPrice: z.number().optional(), + minListingPrice: z.number().optional(), + category: z + .string() + .refine( + (val) => + [ + "Gen Art", + "Photography", + "AI Inspired", + "Memetics", + "Iconic Gems", + ].includes(val), + { message: "Invalid category" } + ) + .optional(), + creator: z.string().optional(), + webhookUrl: z.string().url().optional(), + notificationPreferences: z + .object({ + email: z.string().email().optional(), + telegramId: z.string().optional(), + discordWebhook: z.string().url().optional(), + }) + .optional(), + lastNotificationTimestamp: z.number().optional(), + notificationCooldown: z.number().optional().default(3600000), // 1 hour +}); + +type WatchlistEntry = z.infer; + +// Persistent Storage Manager +class PersistentStorageManager { + private static STORAGE_PATH = path.join( + process.cwd(), + ".nft-watchlist.json" + ); + + static async saveData(data: any): Promise { + try { + await fs.writeFile( + this.STORAGE_PATH, + JSON.stringify(data, null, 2) + ); + } catch (error) { + console.error("Failed to save watchlist:", error); + } + } + + static async loadData(): Promise { + try { + const fileContents = await fs.readFile(this.STORAGE_PATH, "utf-8"); + return JSON.parse(fileContents); + } catch (error) { + // If file doesn't exist, return empty structure + return { watchlist: [], webhooks: [] }; + } + } +} + +// Webhook Notification Service +class WebhookNotificationService { + static async sendNotification(entry: WatchlistEntry, opportunityData: any) { + const notifications = []; + + // Email Notification (Placeholder - would integrate with email service) + if (entry.notificationPreferences?.email) { + notifications.push( + this.sendEmailNotification(entry, opportunityData) + ); + } + + // Telegram Notification (Placeholder - would use Telegram Bot API) + if (entry.notificationPreferences?.telegramId) { + notifications.push( + this.sendTelegramNotification(entry, opportunityData) + ); + } + + // Discord Webhook + if (entry.notificationPreferences?.discordWebhook) { + notifications.push( + this.sendDiscordNotification(entry, opportunityData) + ); + } + + // Direct Webhook URL + if (entry.webhookUrl) { + notifications.push(this.sendGenericWebhook(entry, opportunityData)); + } + + await Promise.allSettled(notifications); + } + + private static async sendEmailNotification( + entry: WatchlistEntry, + data: any + ) { + // Placeholder for email service integration + console.log( + `Email notification to ${entry.notificationPreferences?.email}` + ); + } + + private static async sendTelegramNotification( + entry: WatchlistEntry, + data: any + ) { + // Placeholder for Telegram Bot API + console.log( + `Telegram notification to ${entry.notificationPreferences?.telegramId}` + ); + } + + private static async sendDiscordNotification( + entry: WatchlistEntry, + data: any + ) { + try { + const response = await fetch( + entry.notificationPreferences!.discordWebhook!, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + content: `Thin Floor Opportunity for ${data.address}: + Lowest Price: ${data.lowestPrice} ETH + Thinness: ${data.floorThinnessPercentage.toFixed(2)}%`, + }), + } + ); + } catch (error) { + console.error("Discord webhook failed:", error); + } + } + + private static async sendGenericWebhook(entry: WatchlistEntry, data: any) { + try { + const response = await fetch(entry.webhookUrl!, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + } catch (error) { + console.error("Generic webhook failed:", error); + } + } +} + +// Advanced Filtering Utility +class WatchlistFilter { + private static VALID_CATEGORIES = [ + "Gen Art", + "Photography", + "AI Inspired", + "Memetics", + "Iconic Gems", + ]; + + // Complex filter method with multiple comparison strategies + static filter( + entries: WatchlistEntry[], + criteria: Partial & { + priceRange?: { min?: number; max?: number }; + thinnessRange?: { min?: number; max?: number }; + searchText?: string; + sortBy?: keyof WatchlistEntry; + sortOrder?: "asc" | "desc"; + } + ): WatchlistEntry[] { + let filteredEntries = [...entries]; + + // Normalize and validate category + if (criteria.category) { + const normalizedCategory = this.normalizeCategory( + criteria.category + ); + if (normalizedCategory) { + criteria.category = normalizedCategory; + } else { + // If category is invalid, return empty array + return []; + } + } + + // Text search across multiple fields + if (criteria.searchText) { + const searchTerm = criteria.searchText.toLowerCase(); + filteredEntries = filteredEntries.filter( + (entry) => + entry.address.toLowerCase().includes(searchTerm) || + entry.name?.toLowerCase().includes(searchTerm) || + entry.creator?.toLowerCase().includes(searchTerm) || + entry.category?.toLowerCase().includes(searchTerm) + ); + } + + // Basic field matching + Object.entries(criteria).forEach(([key, value]) => { + if ( + [ + "priceRange", + "thinnessRange", + "searchText", + "sortBy", + "sortOrder", + ].includes(key) + ) + return; + + filteredEntries = filteredEntries.filter( + (entry) => entry[key as keyof WatchlistEntry] === value + ); + }); + + // Price Range Filter + if (criteria.priceRange) { + filteredEntries = filteredEntries.filter((entry) => { + const floorPrice = entry.minFloorPrice; + return ( + (criteria.priceRange?.min === undefined || + floorPrice === undefined || + floorPrice >= criteria.priceRange.min) && + (criteria.priceRange?.max === undefined || + floorPrice === undefined || + floorPrice <= criteria.priceRange.max) + ); + }); + } + + // Thinness Range Filter + if (criteria.thinnessRange) { + filteredEntries = filteredEntries.filter((entry) => { + const thinness = entry.maxThinnessThreshold; + return ( + (criteria.thinnessRange?.min === undefined || + thinness === undefined || + thinness >= criteria.thinnessRange.min) && + (criteria.thinnessRange?.max === undefined || + thinness === undefined || + thinness <= criteria.thinnessRange.max) + ); + }); + } + + // Sorting + if (criteria.sortBy) { + filteredEntries.sort((a, b) => { + const aValue = a[criteria.sortBy!]; + const bValue = b[criteria.sortBy!]; + + if (aValue === undefined) return 1; + if (bValue === undefined) return -1; + + return criteria.sortOrder === "desc" + ? bValue > aValue + ? 1 + : -1 + : aValue > bValue + ? 1 + : -1; + }); + } + + return filteredEntries; + } + + // Helper method to normalize category + static normalizeCategory(category: string): string | null { + const normalizedInput = category.toLowerCase().trim(); + const matchedCategory = this.VALID_CATEGORIES.find( + (validCat) => validCat.toLowerCase() === normalizedInput + ); + return matchedCategory || null; + } + + // Predefined filter methods for common use cases + static filterByCategory(entries: WatchlistEntry[], category: string) { + const normalizedCategory = this.normalizeCategory(category); + return normalizedCategory + ? this.filter(entries, { category: normalizedCategory }) + : []; + } + + static filterByPriceRange( + entries: WatchlistEntry[], + min?: number, + max?: number + ) { + return this.filter(entries, { priceRange: { min, max } }); + } + + static filterByThinnessRange( + entries: WatchlistEntry[], + min?: number, + max?: number + ) { + return this.filter(entries, { thinnessRange: { min, max } }); + } +} + +// Enhanced NFT Watchlist with Persistent Storage +export class NFTWatchlist { + private static instance: NFTWatchlist; + private watchlist: WatchlistEntry[] = []; + + private constructor() { + this.initializeWatchlist(); + } + + public static getInstance(): NFTWatchlist { + if (!NFTWatchlist.instance) { + NFTWatchlist.instance = new NFTWatchlist(); + } + return NFTWatchlist.instance; + } + + private async initializeWatchlist() { + try { + // First, try to load from persistent storage + const data = await PersistentStorageManager.loadData(); + + // If no saved watchlist, initialize with curated collections + if (data.watchlist.length === 0) { + this.watchlist = CURATED_COLLECTIONS.map((collection) => ({ + address: collection.address, + name: collection.name, + category: collection.category || "Uncategorized", + creator: collection.creator, + maxThinnessThreshold: 50, // Default thin floor threshold + minProfitMargin: 2, // Default profit margin + })); + + // Save the initial curated collections to persistent storage + await PersistentStorageManager.saveData({ + watchlist: this.watchlist, + }); + } else { + this.watchlist = data.watchlist.map((entry) => + WatchlistEntrySchema.parse(entry) + ); + } + } catch (error) { + console.error("Failed to initialize watchlist:", error); + } + } + + public async addToWatchlist(entry: WatchlistEntry): Promise { + try { + const validatedEntry = WatchlistEntrySchema.parse(entry); + + const exists = this.watchlist.some( + (item) => + item.address.toLowerCase() === + validatedEntry.address.toLowerCase() + ); + + if (!exists) { + this.watchlist.push(validatedEntry); + await this.saveWatchlist(); + return true; + } + return false; + } catch (error) { + console.error("Invalid watchlist entry:", error); + return false; + } + } + + public async removeFromWatchlist(address: string): Promise { + const initialLength = this.watchlist.length; + this.watchlist = this.watchlist.filter( + (item) => item.address.toLowerCase() !== address.toLowerCase() + ); + + if (initialLength !== this.watchlist.length) { + await this.saveWatchlist(); + return true; + } + return false; + } + + private async saveWatchlist() { + await PersistentStorageManager.saveData({ + watchlist: this.watchlist, + }); + } + + public getWatchlist( + filters?: Parameters[1] + ): WatchlistEntry[] { + if (!filters) return [...this.watchlist]; + + return WatchlistFilter.filter(this.watchlist, filters); + } + + // New method to get filter suggestions + public getFilterSuggestions(): { + categories: string[]; + creators: string[]; + priceRanges: { min: number; max: number }; + thinnessRanges: { min: number; max: number }; + } { + return { + categories: [ + ...new Set( + this.watchlist + .map((entry) => entry.category) + .filter(Boolean) + ), + ], + creators: [ + ...new Set( + this.watchlist.map((entry) => entry.creator).filter(Boolean) + ), + ], + priceRanges: { + min: Math.min( + ...this.watchlist + .map((entry) => entry.minFloorPrice || Infinity) + .filter(Boolean) + ), + max: Math.max( + ...this.watchlist + .map((entry) => entry.maxFloorPrice || -Infinity) + .filter(Boolean) + ), + }, + thinnessRanges: { + min: Math.min( + ...this.watchlist + .map((entry) => entry.maxThinnessThreshold || Infinity) + .filter(Boolean) + ), + max: Math.max( + ...this.watchlist + .map((entry) => entry.maxThinnessThreshold || -Infinity) + .filter(Boolean) + ), + }, + }; + } +} + +export const getCollectionsAction = ( + nftCollectionProvider: Provider +): Action => { + return { + name: "GET_NFT_COLLECTIONS", + similes: ["LIST_NFT_COLLECTIONS", "SHOW_NFT_COLLECTIONS"], + description: + "Fetches information about curated NFT collections on Ethereum", + validate: async (runtime: IAgentRuntime, message: Memory) => { + return message.content.text + .toLowerCase() + .includes("nft collections"); + }, + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + options: any, + callback: HandlerCallback + ) => { + try { + const response = await nftCollectionProvider.get( + runtime, + message + ); + callback({ + text: response, + }); + await runtime.messageManager.createMemory({ + id: message.id, + content: { text: response }, + roomId: message.roomId, + userId: message.userId, + agentId: runtime.agentId, + }); + return true; + } catch (error) { + console.error("Error fetching NFT collections:", error); + await runtime.messageManager.createMemory({ + id: message.id, + content: { text: "Failed to fetch NFT collection data." }, + roomId: message.roomId, + userId: message.userId, + agentId: runtime.agentId, + }); + return false; + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Can you tell me about the top NFT collections?", + }, + }, + { + user: "{{user2}}", + content: { + text: "Certainly! Here are the top NFT collections on Ethereum:", + action: "GET_NFT_COLLECTIONS", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Can you show me a list of NFT collections?", + }, + }, + { + user: "{{user2}}", + content: { + text: "Sure! Here are some curated NFT collections on Ethereum:", + action: "GET_NFT_COLLECTIONS", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Do you know the best NFT collections?", + }, + }, + { + user: "{{user2}}", + content: { + text: "Absolutely! Here's a list of top NFT collections on Ethereum:", + action: "GET_NFT_COLLECTIONS", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Can you fetch Ethereum NFT collections for me?", + }, + }, + { + user: "{{user2}}", + content: { + text: "Of course! Fetching NFT collections on Ethereum:", + action: "GET_NFT_COLLECTIONS", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "I'm curious about NFTs. What are some collections I should look into?", + }, + }, + { + user: "{{user2}}", + content: { + text: "Here are some NFT collections you might find interesting:", + action: "GET_NFT_COLLECTIONS", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Tell me about the trending Ethereum NFT collections.", + }, + }, + { + user: "{{user2}}", + content: { + text: "Here's information on trending Ethereum NFT collections:", + action: "GET_NFT_COLLECTIONS", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "What are some cool NFT collections right now?", + }, + }, + { + user: "{{user2}}", + content: { + text: "Let me show you some popular NFT collections:", + action: "GET_NFT_COLLECTIONS", + }, + }, + ], + ], + }; +}; + +export const manageWatchlistAction = (reservoirService: any): Action => { + const watchlist = NFTWatchlist.getInstance(); + + return { + name: "MANAGE_NFT_WATCHLIST", + similes: [ + "ADD_NFT_COLLECTION", + "REMOVE_NFT_COLLECTION", + "NFT_WATCHLIST", + ], + description: + "Manage a watchlist of NFT collections for thin floor opportunities", + validate: async (runtime: IAgentRuntime, message: Memory) => { + const lowercaseText = message.content.text.toLowerCase(); + return ["watchlist", "add collection", "remove collection"].some( + (term) => lowercaseText.includes(term) + ); + }, + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + options: any, + callback: HandlerCallback + ) => { + try { + const text = message.content.text.toLowerCase(); + + // Advanced filtering command + if (text.includes("filter watchlist")) { + const filters: Parameters< + typeof WatchlistFilter.filter + >[1] = {}; + + // Parse category filter + const categoryMatch = text.match(/category:\s*(\w+)/i); + if (categoryMatch) { + const category = categoryMatch[1].replace(/\s+/g, " "); + const validCategories = [ + "Gen Art", + "Photography", + "AI Inspired", + "Memetics", + "Iconic Gems", + ]; + const matchedCategory = validCategories.find( + (validCat) => + validCat.toLowerCase() === + category.toLowerCase() + ); + + if (matchedCategory) { + filters.category = matchedCategory; + } + } + + // Parse price range filter + const priceRangeMatch = text.match(/price:\s*(\d+)-(\d+)/); + if (priceRangeMatch) { + filters.priceRange = { + min: parseFloat(priceRangeMatch[1]), + max: parseFloat(priceRangeMatch[2]), + }; + } + + // Parse thinness range filter + const thinnessRangeMatch = text.match( + /thinness:\s*(\d+)-(\d+)/ + ); + if (thinnessRangeMatch) { + filters.thinnessRange = { + min: parseFloat(thinnessRangeMatch[1]), + max: parseFloat(thinnessRangeMatch[2]), + }; + } + + // Parse sorting + const sortMatch = text.match( + /sort by:\s*(\w+)\s*(asc|desc)?/i + ); + if (sortMatch) { + filters.sortBy = sortMatch[1] as keyof WatchlistEntry; + filters.sortOrder = ( + sortMatch[2] || "asc" + ).toLowerCase() as "asc" | "desc"; + } + + // Perform filtering + const filteredEntries = watchlist.getWatchlist(filters); + + callback({ + text: + filteredEntries.length > 0 + ? "Filtered Watchlist:\n" + + filteredEntries + .map( + (entry) => + `${entry.address} (Category: ${entry.category || "N/A"}, ` + + `Thinness: ${entry.maxThinnessThreshold}%)` + ) + .join("\n") + : "No entries match the filter criteria.", + }); + return true; + } + + // Get filter suggestions + if (text.includes("filter suggestions")) { + const suggestions = watchlist.getFilterSuggestions(); + + callback({ + text: `Filter Suggestions: +Categories: ${suggestions.categories.join(", ")} +Creators: ${suggestions.creators.join(", ")} +Price Range: ${suggestions.priceRanges.min} - ${suggestions.priceRanges.max} ETH +Thinness Range: ${suggestions.thinnessRanges.min}% - ${suggestions.thinnessRanges.max}%`, + }); + return true; + } + + if (text.includes("add collection")) { + const addressMatch = text.match(/0x[a-fA-F0-9]{40}/); + if (!addressMatch) { + callback({ + text: "Please provide a valid Ethereum contract address.", + }); + return false; + } + + const address = addressMatch[0]; + const thinnessMatch = text.match(/(\d+)%/); + const thinnessThreshold = thinnessMatch + ? parseFloat(thinnessMatch[1]) + : 15; + + // Optional: Extract webhook URL + const webhookMatch = text.match(/(https?:\/\/\S+)/); + const webhookUrl = webhookMatch + ? webhookMatch[1] + : undefined; + + const entry: WatchlistEntry = { + address, + maxThinnessThreshold: thinnessThreshold, + webhookUrl, + notificationPreferences: { + discordWebhook: webhookUrl, + }, + }; + + const result = await watchlist.addToWatchlist(entry); + + callback({ + text: result + ? `Collection ${address} added to watchlist with ${thinnessThreshold}% thinness threshold.` + : "Collection already exists in watchlist.", + }); + return result; + } + + if (text.includes("remove collection")) { + const addressMatch = text.match(/0x[a-fA-F0-9]{40}/); + if (!addressMatch) { + callback({ + text: "Please provide a valid Ethereum contract address to remove.", + }); + return false; + } + + const address = addressMatch[0]; + const result = await watchlist.removeFromWatchlist(address); + + callback({ + text: result + ? `Collection ${address} removed from watchlist.` + : "Collection not found in watchlist.", + }); + return result; + } + + if (text.includes("show watchlist")) { + const currentWatchlist = watchlist.getWatchlist(); + callback({ + text: + currentWatchlist.length > 0 + ? "Current Watchlist:\n" + + currentWatchlist + .map( + (entry) => + `${entry.address} (Thinness: ${entry.maxThinnessThreshold}%)` + ) + .join("\n") + : "Watchlist is empty.", + }); + return true; + } + + callback({ text: "Invalid watchlist command." }); + return false; + } catch (error) { + console.error("Watchlist management error:", error); + callback({ text: "Error managing watchlist." }); + return false; + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { text: "Add collection 0x1234... to watchlist" }, + }, + { + user: "{{user2}}", + content: { text: "Collection added successfully." }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Remove collection 0x1234... from watchlist", + }, + }, + { + user: "{{user2}}", + content: { text: "Collection removed successfully." }, + }, + ], + [ + { + user: "{{user1}}", + content: { text: "Filter watchlist by category:Gen Art" }, + }, + { + user: "{{user2}}", + content: { text: "Filtered watchlist results..." }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Filter watchlist price:0.1-1 thinness:10-20", + }, + }, + { + user: "{{user2}}", + content: { text: "Filtered watchlist results..." }, + }, + ], + [ + { + user: "{{user1}}", + content: { text: "Get filter suggestions" }, + }, + { + user: "{{user2}}", + content: { text: "Available filter options..." }, + }, + ], + ], + }; +}; + +export const detectThinFloorOpportunities = async ( + watchlistCollections: WatchlistEntry[], + reservoirService: any +) => { + const opportunities: Array<{ + collection: string; + lowestPrice: number; + secondLowestPrice: number; + thinnessPercentage: number; + potentialProfit: number; + tokenIds: string[]; + name?: string; + category?: string; + historicalSales: { + latestSalePrice: number; + highestSalePrice: number; + averageSalePrice: number; + salesCount: number; + }[]; + }> = []; + + for (const collection of watchlistCollections) { + try { + // Fetch detailed listings with more context + const [listings, salesHistory] = await Promise.all([ + reservoirService.getListings({ + collection: collection.address, + sortBy: "price_asc", + limit: 10, // Fetch multiple listings for comprehensive analysis + includeTokenDetails: true, + }), + reservoirService.getSalesHistory({ + collection: collection.address, + limit: 50, // Fetch recent sales history + }), + ]); + + // Sort listings by price + const sortedListings = listings + .sort((a, b) => a.price - b.price) + .filter((listing) => listing.status === "active"); + + // Process sales history + const processedSales = salesHistory.map((sale) => ({ + tokenId: sale.tokenId, + salePrice: sale.price, + saleDate: sale.timestamp, + })); + + // Calculate sales statistics + const salesStats = { + latestSalePrice: + processedSales.length > 0 + ? processedSales[0].salePrice + : null, + highestSalePrice: + processedSales.length > 0 + ? Math.max( + ...processedSales.map((sale) => sale.salePrice) + ) + : null, + averageSalePrice: + processedSales.length > 0 + ? processedSales.reduce( + (sum, sale) => sum + sale.salePrice, + 0 + ) / processedSales.length + : null, + salesCount: processedSales.length, + }; + + // Detect thin floor opportunities with more sophisticated logic + if (sortedListings.length >= 2 && salesStats.latestSalePrice) { + const [lowestListing, secondLowestListing] = sortedListings; + + const lowestPrice = lowestListing.price; + const secondLowestPrice = secondLowestListing.price; + + const priceDifference = secondLowestPrice - lowestPrice; + const thinnessPercentage = + (priceDifference / lowestPrice) * 100; + const potentialProfit = secondLowestPrice / lowestPrice; + + // More flexible threshold checking with sales history context + const thinnessThreshold = collection.maxThinnessThreshold || 15; + const profitThreshold = collection.minProfitMargin || 2; + + // Additional criteria using sales history + const latestToLowestRatio = + salesStats.latestSalePrice / lowestPrice; + const highestToLowestRatio = salesStats.highestSalePrice + ? salesStats.highestSalePrice / lowestPrice + : 0; + + if ( + thinnessPercentage > thinnessThreshold && + potentialProfit >= profitThreshold && + latestToLowestRatio > 1.5 // Latest sale at least 50% higher than current floor + ) { + opportunities.push({ + collection: collection.address, + lowestPrice, + secondLowestPrice, + thinnessPercentage, + potentialProfit, + tokenIds: [ + lowestListing.tokenId, + secondLowestListing.tokenId, + ], + name: collection.name, + category: collection.category, + historicalSales: [ + { + latestSalePrice: salesStats.latestSalePrice, + highestSalePrice: salesStats.highestSalePrice, + averageSalePrice: salesStats.averageSalePrice, + salesCount: salesStats.salesCount, + }, + ], + }); + } + } + } catch (error) { + console.error( + `Thin floor detection error for ${collection.address}:`, + error + ); + } + } + + // Sort opportunities by potential profit and sales history + return opportunities.sort((a, b) => { + // Primary sort by potential profit + const profitDiff = b.potentialProfit - a.potentialProfit; + if (profitDiff !== 0) return profitDiff; + + // Secondary sort by latest sale price relative to floor + const latestSaleDiff = + b.historicalSales[0].latestSalePrice / b.lowestPrice - + a.historicalSales[0].latestSalePrice / a.lowestPrice; + + return latestSaleDiff; + }); +}; + +export const getThinFloorNFTsAction = ( + nftCollectionProvider: Provider, + reservoirService: any +): Action => { + const watchlist = NFTWatchlist.getInstance(); + + return { + name: "GET_THIN_FLOOR_NFTS", + similes: ["FIND_NFT_ARBITRAGE", "THIN_FLOOR_OPPORTUNITIES"], + description: "Advanced thin floor NFT arbitrage detection", + validate: async (runtime: IAgentRuntime, message: Memory) => { + const lowercaseText = message.content.text.toLowerCase(); + return [ + "thin floor", + "arbitrage", + "nft opportunity", + "watchlist", + ].some((term) => lowercaseText.includes(term)); + }, + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + options: any, + callback: HandlerCallback + ) => { + try { + const watchlistCollections = watchlist.getWatchlist(); + + if (watchlistCollections.length === 0) { + callback({ + text: "Watchlist is empty. Add collections first.", + }); + return false; + } + + // Detect opportunities + const opportunities = await detectThinFloorOpportunities( + watchlistCollections, + reservoirService + ); + + // Enhanced notification and logging + if (opportunities.length > 0) { + // Prepare detailed opportunity report + const opportunityReports = opportunities + .map( + (opp) => ` +🔥 Arbitrage Opportunity 🔥 +${opp.name ? `Collection: ${opp.name} (${opp.collection})` : `Collection: ${opp.collection}`} +${opp.category ? `Category: ${opp.category}` : ""} +Lowest Price: ${opp.lowestPrice.toFixed(3)} ETH +Second Lowest: ${opp.secondLowestPrice.toFixed(3)} ETH +Thinness: ${opp.thinnessPercentage.toFixed(2)}% +Potential Profit: ${((opp.potentialProfit - 1) * 100).toFixed(2)}% +Token IDs: ${opp.tokenIds.join(", ")} + +📊 Sales History: +Latest Sale: ${opp.historicalSales[0].latestSalePrice.toFixed(3)} ETH +Highest Sale: ${opp.historicalSales[0].highestSalePrice.toFixed(3)} ETH +Average Sale: ${opp.historicalSales[0].averageSalePrice.toFixed(3)} ETH +Total Sales: ${opp.historicalSales[0].salesCount} + ` + ) + .join("\n\n"); + + // Send notifications to each collection's configured channels + await Promise.all( + opportunities.map(async (opportunity) => { + const collectionEntry = watchlistCollections.find( + (entry) => + entry.address === opportunity.collection + ); + + if (collectionEntry) { + await WebhookNotificationService.sendNotification( + collectionEntry, + opportunity + ); + } + }) + ); + + // Callback with opportunities + callback({ + text: `Thin Floor Arbitrage Opportunities Detected:\n${opportunityReports}`, + }); + + return true; + } else { + callback({ + text: "No significant thin floor opportunities found in watchlist.", + }); + return false; + } + } catch (error) { + console.error("Error finding thin floor NFTs:", error); + callback({ + text: "Failed to find thin floor NFT opportunities.", + }); + return false; + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { text: "Find NFT arbitrage opportunities" }, + }, + { + user: "{{user2}}", + content: { + text: "Here are the top thin floor NFT opportunities...", + }, + }, + ], + ], + }; +}; diff --git a/packages/plugin-nft-collections/src/actions/list-nft.ts b/packages/plugin-nft-collections/src/actions/list-nft.ts new file mode 100644 index 00000000000..26370affe6f --- /dev/null +++ b/packages/plugin-nft-collections/src/actions/list-nft.ts @@ -0,0 +1,400 @@ +import { Action, IAgentRuntime, Memory, Provider, State } from "@elizaos/core"; +import { ReservoirService } from "../services/reservoir"; +import { HandlerCallback } from "@elizaos/core"; +import { z } from "zod"; + +// Helper function to extract NFT listing details from the message +function extractListingDetails(text: string): { + tokenId: string | null; + collectionAddress: string | null; + price?: number | null; + arbitrageMode?: boolean; +} { + const addressMatch = text.match(/(?:collection|from)\s*(0x[a-fA-F0-9]+)/i); + const tokenIdMatch = text.match(/(?:token|nft)\s*#?\s*(\d+)/i); + const priceMatch = text.match(/(\d+(?:\.\d+)?)\s*(?:eth|Ξ)/i); + const arbitrageModeMatch = text.match(/(?:arbitrage|auto)/i); + + return { + collectionAddress: addressMatch ? addressMatch[1] : null, + tokenId: tokenIdMatch ? tokenIdMatch[1] : null, + price: priceMatch ? parseFloat(priceMatch[1]) : undefined, + arbitrageMode: !!arbitrageModeMatch, + }; +} + +// Extended interface to handle potential methods +interface ExtendedReservoirService extends ReservoirService { + getLastSalePrice?: (params: { + collectionAddress: string; + tokenId: string; + }) => Promise; +} + +// Offer Acceptance Schema +const OfferAcceptanceSchema = z.object({ + tokenId: z.string(), + collection: z.string(), + offerPrice: z.number(), + listingPrice: z.number(), + acceptanceThreshold: z.number().optional().default(0.95), // 5% below listing price + maxAcceptanceDiscount: z.number().optional().default(0.1), // Max 10% below listing +}); + +export const listNFTAction = (nftService: ExtendedReservoirService): Action => { + return { + name: "LIST_NFT", + similes: ["SELL_NFT", "CREATE_LISTING", "ARBITRAGE_LISTING"], + description: + "Lists an NFT for sale on ikigailabs.xyz marketplace, with optional arbitrage mode for automatic 2x pricing.", + + validate: async (runtime: IAgentRuntime, message: Memory) => { + const content = message.content.text.toLowerCase(); + return ( + (content.includes("list") || + content.includes("sell") || + content.includes("arbitrage")) && + content.includes("nft") && + (content.includes("0x") || + content.includes("token") || + content.includes("#")) + ); + }, + + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + options: any, + callback: HandlerCallback + ) => { + try { + const { + collectionAddress, + tokenId, + price: userSpecifiedPrice, + arbitrageMode, + } = extractListingDetails(message.content.text); + + if (!collectionAddress || !tokenId) { + throw new Error( + "Please provide the collection address and token ID" + ); + } + + if (!nftService) { + throw new Error("NFT service not found"); + } + + // Verify ownership before listing + const ownedNFTs = await nftService.getOwnedNFTs(message.userId); + const ownedNFT = ownedNFTs.find( + (nft) => + nft.collectionAddress.toLowerCase() === + collectionAddress.toLowerCase() && + nft.tokenId === tokenId + ); + + if (!ownedNFT) { + throw new Error("You don't own this NFT"); + } + + // Determine listing price + let listingPrice: number; + if (userSpecifiedPrice) { + listingPrice = userSpecifiedPrice; + } else if (arbitrageMode) { + // In arbitrage mode, try to get the last sale price and double it + let lastSalePrice: number | undefined; + + // Check if the method exists, otherwise use a fallback + if (typeof nftService.getLastSalePrice === "function") { + lastSalePrice = await nftService.getLastSalePrice({ + collectionAddress, + tokenId, + }); + } + + // Fallback: use floor price + if (!lastSalePrice) { + const floorListings = await nftService.getFloorListings( + { + collection: collectionAddress, + limit: 1, + sortBy: "price", + } + ); + + lastSalePrice = + floorListings.length > 0 + ? floorListings[0].price + : undefined; + } + + listingPrice = lastSalePrice ? lastSalePrice * 2 : 0; + } else { + listingPrice = 0; // Default to market price + } + + // Create the listing on ikigailabs + const listing = await nftService.createListing({ + tokenId, + collectionAddress, + price: listingPrice, + marketplace: "ikigailabs", + expirationTime: + Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60, // 30 days + }); + + const response = + `Successfully created listing on ikigailabs.xyz:\n` + + `• Collection: ${collectionAddress}\n` + + `• Token ID: ${tokenId}\n` + + `• Listing Price: ${listingPrice.toFixed(3)} ETH\n` + + `• Listing Mode: ${arbitrageMode ? "Arbitrage" : "Standard"}\n` + + `• Status: ${listing.status}\n` + + `• Listing URL: ${listing.marketplaceUrl}\n` + + (listing.transactionHash + ? `• Transaction: ${listing.transactionHash}\n` + : ""); + + callback({ + text: response, + }); + + await runtime.messageManager.createMemory({ + id: message.id, + content: { text: response }, + roomId: message.roomId, + userId: message.userId, + agentId: runtime.agentId, + }); + + return true; + } catch (error) { + console.error("NFT listing failed:", error); + await runtime.messageManager.createMemory({ + id: message.id, + content: { + text: `Failed to list NFT: ${error.message}`, + }, + roomId: message.roomId, + userId: message.userId, + agentId: runtime.agentId, + }); + return false; + } + }, + + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "List token #123 from collection 0x1234...abcd in arbitrage mode", + }, + }, + { + user: "{{user2}}", + content: { + text: "Creating arbitrage listing on ikigailabs.xyz at 2x last sale price...", + action: "LIST_NFT", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "List token #123 from collection 0x1234...abcd for 5 ETH", + }, + }, + { + user: "{{user2}}", + content: { + text: "Creating listing on ikigailabs.xyz with specified price...", + action: "LIST_NFT", + }, + }, + ], + ], + }; +}; + +export const acceptNFTOfferAction = ( + nftCollectionProvider: Provider, + reservoirService: any +): Action => { + return { + name: "ACCEPT_NFT_OFFER", + similes: ["SELL_NFT", "PROCESS_OFFER"], + description: + "Intelligently accept NFT offers based on pricing strategy", + validate: async (runtime: IAgentRuntime, message: Memory) => { + const lowercaseText = message.content.text.toLowerCase(); + return [ + "accept offer", + "sell nft", + "process offer", + "accept bid", + ].some((term) => lowercaseText.includes(term)); + }, + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + options: any, + callback: HandlerCallback + ) => { + try { + // Extract offer details from message + const offerDetails = await extractOfferDetails( + message.content.text, + reservoirService + ); + + // Validate offer details + const validatedOffer = OfferAcceptanceSchema.parse({ + tokenId: offerDetails.tokenId, + collection: offerDetails.collection, + offerPrice: offerDetails.offerPrice, + listingPrice: offerDetails.listingPrice, + }); + + // Fetch current market context + const [currentListings, recentSales] = await Promise.all([ + reservoirService.getListings({ + collection: validatedOffer.collection, + limit: 10, + }), + reservoirService.getSalesHistory({ + collection: validatedOffer.collection, + limit: 20, + }), + ]); + + // Calculate market average price + const averageSalePrice = + recentSales.length > 0 + ? recentSales.reduce( + (sum, sale) => sum + sale.price, + 0 + ) / recentSales.length + : validatedOffer.listingPrice; + + // Intelligent offer acceptance logic + const shouldAcceptOffer = + // Offer is at or above 95% of listing price + validatedOffer.offerPrice >= + validatedOffer.listingPrice * + validatedOffer.acceptanceThreshold || + // Offer is within 10% of average recent sale price + (validatedOffer.offerPrice >= + averageSalePrice * + (1 - validatedOffer.maxAcceptanceDiscount) && + validatedOffer.offerPrice <= + averageSalePrice * + (1 + validatedOffer.maxAcceptanceDiscount)); + + if (shouldAcceptOffer) { + // Execute offer acceptance + const acceptanceResult = await reservoirService.acceptOffer( + { + tokenId: validatedOffer.tokenId, + collection: validatedOffer.collection, + offerPrice: validatedOffer.offerPrice, + } + ); + + // Prepare response + const responseText = `✅ Offer Accepted! +🏷️ Collection: ${validatedOffer.collection} +🖼️ Token ID: ${validatedOffer.tokenId} +💰 Offer Price: ${validatedOffer.offerPrice.toFixed(3)} ETH +📊 Market Context: Avg Recent Sale ${averageSalePrice.toFixed(3)} ETH`; + + callback({ text: responseText }); + + // Optional: Log the transaction + console.log("NFT Offer Accepted:", { + collection: validatedOffer.collection, + tokenId: validatedOffer.tokenId, + offerPrice: validatedOffer.offerPrice, + marketAveragePrice: averageSalePrice, + }); + + return true; + } else { + callback({ + text: `❌ Offer Rejected. +Offer Price: ${validatedOffer.offerPrice.toFixed(3)} ETH +Listing Price: ${validatedOffer.listingPrice.toFixed(3)} ETH +Market Average: ${averageSalePrice.toFixed(3)} ETH`, + }); + return false; + } + } catch (error) { + console.error("Error processing NFT offer:", error); + callback({ text: "Failed to process NFT offer." }); + return false; + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Accept offer for token 123 in collection 0x...", + }, + }, + { + user: "{{user2}}", + content: { text: "Offer accepted successfully!" }, + }, + ], + ], + }; +}; + +// Fetch current listing price for a specific token +async function fetchCurrentListingPrice( + collection: string, + tokenId: string, + reservoirService: any +) { + try { + const listings = await reservoirService.getListings({ + collection, + tokenId, + limit: 1, + }); + return listings.length > 0 ? listings[0].price : null; + } catch (error) { + console.error("Error fetching listing price:", error); + return null; + } +} + +// Helper function to extract offer details from message +async function extractOfferDetails(messageText: string, reservoirService: any) { + // Implement intelligent parsing of offer details + // This is a placeholder and should be enhanced with more robust parsing + const tokenIdMatch = messageText.match(/token\s*(\d+)/i); + const collectionMatch = messageText.match(/0x[a-fA-F0-9]{40}/); + const priceMatch = messageText.match(/(\d+(\.\d+)?)\s*ETH/i); + + if (!tokenIdMatch || !collectionMatch || !priceMatch) { + throw new Error("Insufficient offer details"); + } + + return { + tokenId: tokenIdMatch[1], + collection: collectionMatch[0], + offerPrice: parseFloat(priceMatch[1]), + listingPrice: await fetchCurrentListingPrice( + collectionMatch[0], + tokenIdMatch[1], + reservoirService + ), + }; +} diff --git a/packages/plugin-nft-collections/src/actions/sweep-floor.ts b/packages/plugin-nft-collections/src/actions/sweep-floor.ts new file mode 100644 index 00000000000..076d9b048f2 --- /dev/null +++ b/packages/plugin-nft-collections/src/actions/sweep-floor.ts @@ -0,0 +1,224 @@ +import { Action, IAgentRuntime, Memory, State } from "@elizaos/core"; +import { ReservoirService } from "../services/reservoir"; +import { HandlerCallback } from "@elizaos/core"; +import { z } from "zod"; + +// Recreate the WatchlistEntrySchema from get-collections.ts +const WatchlistEntrySchema = z.object({ + address: z.string(), + name: z.string().optional(), + maxThinnessThreshold: z.number().optional().default(15), + category: z.string().optional(), +}); + +type WatchlistEntry = z.infer; + +// Define types for marketplace interactions +interface ListingDetails { + tokenId: string; + price: number; + seller?: string; + marketplace?: string; +} + +interface BuyResult { + path: string; + steps: Array<{ action: string; status: string }>; + status?: string; +} + +interface ListResult { + status: string; + marketplaceUrl?: string; + transactionHash?: string; +} + +interface ArbitrageOpportunity { + collection: string; + lowestPrice: number; + secondLowestPrice: number; + thinnessPercentage: number; + tokenIds: string[]; +} + +export const sweepFloorArbitrageAction = ( + nftService: ReservoirService, + reservoirService: any +): Action => { + // Mock watchlist for demonstration + const mockWatchlist: WatchlistEntry[] = [ + { + address: "0x...", // QQL Collection Address + name: "QQL by Tyler Hobbs", + category: "Art", + maxThinnessThreshold: 50, + }, + ]; + + const detectThinFloorOpportunities = async (): Promise< + ArbitrageOpportunity[] + > => { + const watchlistCollections = mockWatchlist.filter( + (collection) => collection.category === "Art" + ); + + const opportunities: ArbitrageOpportunity[] = []; + + for (const collection of watchlistCollections) { + try { + const listings = await reservoirService.getListings({ + collection: collection.address, + sortBy: "price_asc", + limit: 10, + includeTokenDetails: true, + }); + + if (listings.length >= 2) { + const [lowestListing, secondLowestListing] = listings; + const priceDifference = + secondLowestListing.price - lowestListing.price; + const thinnessPercentage = + (priceDifference / lowestListing.price) * 100; + + // Use collection's custom thinness threshold or default to 50% + const thinnessThreshold = + collection.maxThinnessThreshold || 50; + + if (thinnessPercentage > thinnessThreshold) { + opportunities.push({ + collection: collection.address, + lowestPrice: lowestListing.price, + secondLowestPrice: secondLowestListing.price, + thinnessPercentage, + tokenIds: [lowestListing.tokenId], + }); + } + } + } catch (error) { + console.error( + `Thin floor detection error for ${collection.address}:`, + error + ); + } + } + + return opportunities.sort( + (a, b) => b.thinnessPercentage - a.thinnessPercentage + ); + }; + + return { + name: "SWEEP_FLOOR_ARBITRAGE", + similes: ["AUTO_BUY_FLOOR_NFT", "QUICK_FLIP_NFT"], + description: + "Automatically detect and execute thin floor arbitrage opportunities in art collections", + + validate: async (runtime: IAgentRuntime, message: Memory) => { + const content = message.content.text.toLowerCase(); + return ( + (content.includes("arbitrage") || + content.includes("auto buy")) && + content.includes("art") && + content.includes("nft") + ); + }, + + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + options: any, + callback: HandlerCallback + ) => { + try { + // Detect thin floor opportunities + const opportunities = await detectThinFloorOpportunities(); + + if (opportunities.length === 0) { + callback({ + text: "No thin floor arbitrage opportunities found.", + }); + return false; + } + + const results = []; + + // Process top 3 opportunities + for (const opportunity of opportunities.slice(0, 3)) { + // Buy floor NFT + const buyResult: BuyResult = await nftService.executeBuy({ + listings: [ + { + tokenId: opportunity.tokenIds[0], + price: opportunity.lowestPrice, + seller: "marketplace", + marketplace: "ikigailabs", + }, + ], + taker: message.userId, + }); + + // Relist at 2x price + const relistPrice = opportunity.secondLowestPrice * 2; + const listResult: ListResult = + await nftService.createListing({ + tokenId: opportunity.tokenIds[0], + collectionAddress: opportunity.collection, + price: relistPrice, + marketplace: "ikigailabs", + expirationTime: + Math.floor(Date.now() / 1000) + + 30 * 24 * 60 * 60, // 30 days + }); + + results.push({ + collection: opportunity.collection, + buyPrice: opportunity.lowestPrice, + relistPrice, + thinnessPercentage: opportunity.thinnessPercentage, + buyStatus: buyResult.steps[0]?.status || "Unknown", + listStatus: listResult.status, + }); + } + + const response = results + .map( + (result) => + `🔥 Arbitrage Opportunity 🔥\n` + + `Collection: ${result.collection}\n` + + `Buy Price: ${result.buyPrice.toFixed(3)} ETH\n` + + `Relist Price: ${result.relistPrice.toFixed(3)} ETH\n` + + `Thinness: ${result.thinnessPercentage.toFixed(2)}%\n` + + `Buy Status: ${result.buyStatus}\n` + + `List Status: ${result.listStatus}` + ) + .join("\n\n"); + + callback({ text: response }); + return true; + } catch (error) { + console.error("Arbitrage workflow failed:", error); + callback({ + text: `Arbitrage workflow failed: ${error.message}`, + }); + return false; + } + }, + + examples: [ + [ + { + user: "{{user1}}", + content: { text: "Run art NFT arbitrage workflow" }, + }, + { + user: "{{user2}}", + content: { + text: "Executing automated thin floor arbitrage for art collections...", + action: "SWEEP_FLOOR_ARBITRAGE", + }, + }, + ], + ], + }; +}; diff --git a/packages/plugin-nft-collections/src/actions/tweet-alpha.ts b/packages/plugin-nft-collections/src/actions/tweet-alpha.ts new file mode 100644 index 00000000000..feedab59d81 --- /dev/null +++ b/packages/plugin-nft-collections/src/actions/tweet-alpha.ts @@ -0,0 +1,254 @@ +import { Action, IAgentRuntime, Memory, Provider, State } from "@elizaos/core"; +import { HandlerCallback } from "@elizaos/core"; +import { NFTWatchlist } from "./get-collections"; +import { detectThinFloorOpportunities } from "./get-collections"; +import { CURATED_COLLECTIONS } from "../constants/curated-collections"; + +interface CuratedCollection { + address?: string; + name?: string; + description?: string; + category?: + | "Gen Art" + | "Photography" + | "AI Inspired" + | "Memetics" + | "Iconic Gems"; + creator?: string; + tokenIdRange?: { start?: string; end?: string }; +} + +export const publishDailyNFTOpportunitiesTweetAction = ( + nftCollectionProvider: Provider, + reservoirService: any, + twitterClient: any +): Action => { + const watchlist = NFTWatchlist.getInstance(); + + return { + name: "PUBLISH_DAILY_NFT_OPPORTUNITIES_TWEET", + similes: ["NFT_DAILY_INSIGHTS", "TWEET_FLOOR_GEMS"], + description: + "Publish daily tweet about discovered NFT buying opportunities", + validate: async (runtime: IAgentRuntime, message: Memory) => { + const lowercaseText = message.content.text.toLowerCase(); + return ( + lowercaseText.includes("publish daily tweet") || + lowercaseText.includes("tweet nft opportunities") + ); + }, + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + options: any, + callback: HandlerCallback + ) => { + try { + // Fetch opportunities from watchlist + const watchlistCollections = watchlist.getWatchlist(); + const opportunities = await detectThinFloorOpportunities( + watchlistCollections, + reservoirService + ); + + // Select top 3 opportunities for the tweet + const topOpportunities = opportunities.slice(0, 3); + + if (topOpportunities.length === 0) { + callback({ + text: "No significant NFT opportunities found today.", + }); + return false; + } + + // Craft an engaging and creative tweet + const tweetContent = `🕵️‍♂️ NFT Arbitrage Hunters: Today's Hidden Gems 💎 + +Uncover the market's best-kept secrets before anyone else: + +${topOpportunities + .map( + (opp, index) => ` +${index + 1}. ${opp.name || "Mystery Collection"} 🎨 +💸 Floor Price Hack: ${opp.lowestPrice.toFixed(3)} ETH +🚀 Profit Potential: ${((opp.potentialProfit - 1) * 100).toFixed(2)}% +💡 Last Sale Whispers: ${opp.historicalSales[0].latestSalePrice.toFixed(3)} ETH +🔍 Dive deeper: https://ikigailabs.xyz/collection/${opp.collection}` + ) + .join("\n")} + +💡 Pro Tip: Speed is your ally in the NFT arbitrage game! + +Powered by Ikigai Labs 🔥`; + + // Publish tweet + const tweetResponse = + await twitterClient.v2.tweet(tweetContent); + + // Callback and logging + callback({ + text: `Daily NFT opportunities tweet published. Tweet ID: ${tweetResponse.data.id}`, + }); + + // Optional: Log tweet details + console.log("Daily NFT Opportunities Tweet:", { + tweetId: tweetResponse.data.id, + opportunities: topOpportunities.map( + (opp) => opp.collection + ), + }); + + return true; + } catch (error) { + console.error( + "Error publishing daily NFT opportunities tweet:", + error + ); + callback({ + text: "Failed to publish daily NFT opportunities tweet.", + }); + return false; + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Publish daily tweet about NFT opportunities", + }, + }, + { + user: "{{user2}}", + content: { + text: "Daily NFT opportunities tweet published successfully.", + }, + }, + ], + ], + }; +}; + +export const publishDailyCuratedCollectionTweetAction = ( + nftCollectionProvider: Provider, + reservoirService: any, + twitterClient: any +): Action => { + return { + name: "PUBLISH_DAILY_CURATED_COLLECTION_TWEET", + similes: ["NFT_COLLECTION_SPOTLIGHT", "DAILY_ART_SHOWCASE"], + description: + "Publish a daily tweet highlighting a curated NFT collection", + validate: async (runtime: IAgentRuntime, message: Memory) => { + const lowercaseText = message.content.text.toLowerCase(); + return ( + lowercaseText.includes("daily collection") || + lowercaseText.includes("curated collection tweet") + ); + }, + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + options: any, + callback: HandlerCallback + ) => { + try { + // Select a random curated collection + const collection = + CURATED_COLLECTIONS[ + Math.floor(Math.random() * CURATED_COLLECTIONS.length) + ]; + + // Fetch additional collection insights + const [floorListings, salesHistory] = await Promise.all([ + reservoirService.getFloorListings({ + collection: collection.address, + limit: 5, + }), + reservoirService.getSalesHistory({ + collection: collection.address, + limit: 20, + }), + ]); + + // Calculate sales statistics + const salesStats = { + totalVolume: salesHistory.reduce( + (sum, sale) => sum + sale.price, + 0 + ), + averagePrice: + salesHistory.length > 0 + ? salesHistory.reduce( + (sum, sale) => sum + sale.price, + 0 + ) / salesHistory.length + : 0, + totalSales: salesHistory.length, + floorPrice: + floorListings.length > 0 ? floorListings[0].price : 0, + }; + + // Craft an engaging tweet + const tweetContent = `🎨 Curated Collection Spotlight: ${collection.name} 💎 + +Dive into the story behind today's digital masterpiece: + +🖌️ Artist: ${collection.creator || "Anonymous"} +🌟 Collection Essence: ${collection.description || "A unique digital art experience that pushes the boundaries of creativity"} +📊 Floor Price: ${salesStats.floorPrice.toFixed(3)} ETH +💸 Total Volume: ${salesStats.totalVolume.toFixed(3)} ETH +🔢 Total Sales: ${salesStats.totalSales} + +Explore the art: https://ikigailabs.xyz/collection/${collection.address} + +Powered by Ikigai Labs 🔥`; + + // Publish tweet + const tweetResponse = + await twitterClient.v2.tweet(tweetContent); + + // Callback and logging + callback({ + text: `Daily curated collection tweet published for ${collection.name}. Tweet ID: ${tweetResponse.data.id}`, + }); + + // Optional: Log tweet details + console.log("Daily Curated Collection Tweet:", { + tweetId: tweetResponse.data.id, + collection: collection.name, + collectionAddress: collection.address, + }); + + return true; + } catch (error) { + console.error( + "Error publishing daily curated collection tweet:", + error + ); + callback({ + text: "Failed to publish daily curated collection tweet.", + }); + return false; + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Publish daily collection tweet", + }, + }, + { + user: "{{user2}}", + content: { + text: "Daily curated collection tweet published successfully.", + }, + }, + ], + ], + }; +}; diff --git a/packages/plugin-nft-collections/src/constants/collections.ts b/packages/plugin-nft-collections/src/constants/collections.ts new file mode 100644 index 00000000000..c02654c98f7 --- /dev/null +++ b/packages/plugin-nft-collections/src/constants/collections.ts @@ -0,0 +1,106 @@ +import { z } from "zod"; + +export const NFTCollectionSchema = z.object({ + address: z.string(), + name: z.string(), + symbol: z.string().optional(), + description: z.string().optional(), + imageUrl: z.string().optional(), + externalUrl: z.string().optional(), + twitterUsername: z.string().optional(), + discordUrl: z.string().optional(), + 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; + +/** + * Curated list of NFT collections featured on ikigailabs.xyz + * This list is used to prioritize and enhance functionality for these collections + */ +export const CURATED_COLLECTIONS: NFTCollection[] = [ + { + address: "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", + name: "Bored Ape Yacht Club", + symbol: "BAYC", + description: + "The Bored Ape Yacht Club is a collection of 10,000 unique Bored Ape NFTs.", + verified: true, + featured: true, + twitterUsername: "BoredApeYC", + discordUrl: "https://discord.gg/3P5K3dzgdB", + }, + // Add more collections here... +]; + +/** + * Map of collection addresses to their metadata for quick lookup + */ +export const COLLECTIONS_MAP = new Map( + CURATED_COLLECTIONS.map((collection) => [ + collection.address.toLowerCase(), + collection, + ]) +); + +/** + * Check if a collection address is in our curated list + */ +export function isCuratedCollection(address: string): boolean { + return COLLECTIONS_MAP.has(address.toLowerCase()); +} + +/** + * Get collection metadata if it exists in our curated list + */ +export function getCuratedCollection( + address: string +): NFTCollection | undefined { + return COLLECTIONS_MAP.get(address.toLowerCase()); +} + +/** + * Get all curated collection addresses + */ +export function getCuratedAddresses(): string[] { + return CURATED_COLLECTIONS.map((collection) => + collection.address.toLowerCase() + ); +} + +/** + * Get featured collection addresses + */ +export function getFeaturedAddresses(): string[] { + return CURATED_COLLECTIONS.filter((collection) => collection.featured).map( + (collection) => collection.address.toLowerCase() + ); +} + +/** + * Get verified collection addresses + */ +export function getVerifiedAddresses(): string[] { + return CURATED_COLLECTIONS.filter((collection) => collection.verified).map( + (collection) => collection.address.toLowerCase() + ); +} diff --git a/packages/plugin-nft-collections/src/constants/curated-collections.ts b/packages/plugin-nft-collections/src/constants/curated-collections.ts new file mode 100644 index 00000000000..6fb5bb3c407 --- /dev/null +++ b/packages/plugin-nft-collections/src/constants/curated-collections.ts @@ -0,0 +1,1926 @@ +import { z } from "zod"; + +export const CollectionCategory = z.enum([ + "Gen Art", + "Photography", + "AI Inspired", + "Memetics", + "Iconic Gems", +]); + +export type CollectionCategory = z.infer; + +export type CuratedCollection = z.infer & { + description?: string; +}; + +export const CuratedCollectionSchema = z.object({ + address: z.string().optional(), + name: z.string().optional(), + description: z.string().optional(), + category: CollectionCategory.optional(), + creator: z.string().optional(), + tokenIdRange: z + .object({ + start: z.string().optional(), + end: z.string().optional(), + }) + .optional(), +}); + +/** + * Curated list of NFT collections featured on ikigailabs.xyz + */ +export const CURATED_COLLECTIONS: CuratedCollection[] = [ + // Gen Art Collections + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Fidenza", + category: "Gen Art", + creator: "Tyler Hobbs", + tokenIdRange: { + start: "78000000", + end: "78999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Ringers", + category: "Gen Art", + creator: "Dmitri Cherniak", + tokenIdRange: { + start: "13000000", + end: "13999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Pigments", + category: "Gen Art", + creator: "Darien Brito", + tokenIdRange: { + start: "129000000", + end: "129999999", + }, + }, + { + address: "0x99a9b7c1116f9ceeb1652de04d5969cce509b069", + name: "Human Unreadable", + category: "Gen Art", + creator: "Operator", + tokenIdRange: { + start: "455000000", + end: "455999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Skulptuur", + category: "Gen Art", + creator: "Piter Pasma", + tokenIdRange: { + start: "173000000", + end: "173999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Scribbled Boundaries", + category: "Gen Art", + creator: "William Tan", + tokenIdRange: { + start: "131000000", + end: "131999999", + }, + }, + { + address: "0x99a9b7c1116f9ceeb1652de04d5969cce509b069", + name: "The Harvest", + category: "Gen Art", + creator: "Per Kristian Stoveland", + tokenIdRange: { + start: "407000000", + end: "407999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Fragments of an Infinite Field", + category: "Gen Art", + creator: "Monica Rizzolli", + tokenIdRange: { + start: "159000000", + end: "159999999", + }, + }, + { + address: "0x0a1bbd57033f57e7b6743621b79fcb9eb2ce3676", + name: "FOLIO", + category: "Gen Art", + creator: "Matt DesLauriers", + tokenIdRange: { + start: "8000000", + end: "8999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Meridian", + category: "Gen Art", + creator: "Matt DesLauriers", + tokenIdRange: { + start: "163000000", + end: "163999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Archetype", + category: "Gen Art", + creator: "Kjetil Golid", + tokenIdRange: { + start: "23000000", + end: "23999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Gazers", + category: "Gen Art", + creator: "Matt Kane", + tokenIdRange: { + start: "215000000", + end: "215999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Subscapes", + category: "Gen Art", + creator: "Matt DesLauriers", + tokenIdRange: { + start: "53000000", + end: "53999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Anticyclone", + category: "Gen Art", + creator: "William Mapan", + tokenIdRange: { + start: "304000000", + end: "304999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Memories of Qilin", + category: "Gen Art", + creator: "Emily Xie", + tokenIdRange: { + start: "282000000", + end: "282999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Elevated Deconstructions", + category: "Gen Art", + creator: "luxpris", + tokenIdRange: { + start: "7000000", + end: "7999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Screens", + category: "Gen Art", + creator: "Thomas Lin Pedersen", + tokenIdRange: { + start: "255000000", + end: "255999999", + }, + }, + { + address: "0x059edd72cd353df5106d2b9cc5ab83a52287ac3a", + name: "Genesis", + category: "Gen Art", + creator: "DCA", + tokenIdRange: { + start: "1000000", + end: "1999999", + }, + }, + { + address: "0x8cdbd7010bd197848e95c1fd7f6e870aac9b0d3c", + name: "///", + category: "Gen Art", + creator: "Snowfro", + tokenIdRange: { + start: "2000000", + end: "2999999", + }, + }, + { + address: "0x0a1bbd57033f57e7b6743621b79fcb9eb2ce3676", + name: "100 Untitled Spaces", + category: "Gen Art", + creator: "Snowfro", + tokenIdRange: { + start: "28000000", + end: "28999999", + }, + }, + { + address: "0x0a1bbd57033f57e7b6743621b79fcb9eb2ce3676", + name: "Inflection", + category: "Gen Art", + creator: "Jeff Davis", + tokenIdRange: { + start: "3000000", + end: "3999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Rapture", + category: "Gen Art", + creator: "Thomas Lin Pedersen", + tokenIdRange: { + start: "141000000", + end: "141999999", + }, + }, + { + address: "0x99a9b7c1116f9ceeb1652de04d5969cce509b069", + name: "Blind Spots", + category: "Gen Art", + creator: "Shaderism", + tokenIdRange: { + start: "484000000", + end: "484999999", + }, + }, + { + address: "0xc73b17179bf0c59cd5860bb25247d1d1092c1088", + name: "QQL Mint Pass", + category: "Gen Art", + creator: "Tyler Hobbs & Dandelion Wist", + }, + { + address: "0x495f947276749ce646f68ac8c248420045cb7b5e", + name: "888", + category: "Gen Art", + creator: "Kevin Abosch", + tokenIdRange: { + start: "opensea-888-by-kevin-abosch", + end: "opensea-888-by-kevin-abosch", + }, + }, + { + address: "0x0e42ffbac75bcc30cd0015f8aaa608539ba35fbb", + name: "Mind the Gap", + category: "Gen Art", + creator: "MountVitruvius", + }, + { + address: "0x7d2d93eed47e55c873b9580b4e6ebd5bc045d1b6", + name: "Mercedes", + category: "Gen Art", + }, + { + address: "0x4e1f41613c9084fdb9e34e11fae9412427480e56", + name: "Terraforms", + category: "Gen Art", + creator: "Mathcastles", + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Hōrō", + category: "Gen Art", + creator: "makio135", + }, + { + address: "0x2b0bfa93beb22f44e7c1be88efd80396f8d9f1d4", + name: "STATE OF THE ART", + category: "Gen Art", + creator: "ThankYouX", + }, + { + address: "0xA4F6105B612f913e468F6B27FCbb48c3569ACbE7", + name: "TECTONICS", + category: "Gen Art", + creator: "mpkoz", + }, + { + address: "0x845dd2a7ee2a92a0518ab2135365ed63fdba0c88", + name: "QQL", + category: "Gen Art", + creator: "Tyler Hobbs & Dandelion Wist", + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Passin", + category: "Gen Art", + tokenIdRange: { + start: "314000000", + end: "314999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Yazid", + category: "Gen Art", + tokenIdRange: { + start: "281000000", + end: "281999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Radix 2", + category: "Gen Art", + tokenIdRange: { + start: "139000000", + end: "139999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Radix 1", + category: "Gen Art", + tokenIdRange: { + start: "104000000", + end: "104999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Catblocks", + category: "Gen Art", + tokenIdRange: { + start: "73000000", + end: "73999999", + }, + }, + { + address: "0x4d928ab507bf633dd8e68024a1fb4c99316bbdf3", + name: "Love Tennis", + category: "Gen Art", + }, + { + address: "0x99a9b7c1116f9ceeb1652de04d5969cce509b069", + name: "Renders Game", + category: "Gen Art", + creator: "MountVitruvius", + tokenIdRange: { + start: "415000000", + end: "415999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Running Moon", + category: "Gen Art", + creator: "Licia He", + tokenIdRange: { + start: "334000000", + end: "334999999", + }, + }, + { + address: "0x99a9b7c1116f9ceeb1652de04d5969cce509b069", + name: "Neural Sediments", + category: "Gen Art", + creator: "Eko33", + tokenIdRange: { + start: "418000000", + end: "418999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Fontana", + category: "Gen Art", + creator: "Harvey Rayner", + tokenIdRange: { + start: "367000000", + end: "367999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Algobots", + category: "Gen Art", + creator: "Stina Jones", + tokenIdRange: { + start: "40000000", + end: "40999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Apparitions", + category: "Gen Art", + creator: "Aaron Penne", + tokenIdRange: { + start: "28000000", + end: "28999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "[Dis]entanglement", + category: "Gen Art", + creator: "onlygenerated", + tokenIdRange: { + start: "97000000", + end: "97999999", + }, + }, + { + address: "0x99a9b7c1116f9ceeb1652de04d5969cce509b069", + name: "Semblance", + category: "Gen Art", + creator: "rahul iyer", + tokenIdRange: { + start: "447000000", + end: "447999999", + }, + }, + { + address: "0xCe3aB0D9D5e36a12235def6CaB84C355D51703aB", + name: "Interference", + category: "Gen Art", + creator: "Phaust", + }, + { + address: "0x495f947276749ce646f68ac8c248420045cb7b5e", + name: "888", + category: "Gen Art", + creator: "Kevin Abosch", + tokenIdRange: { + start: "opensea-888-by-kevin-abosch", + end: "opensea-888-by-kevin-abosch", + }, + }, + { + address: "0x2DB452c9A7b14f927F51589a54B4D56dD4B31977", + name: "Web", + category: "Gen Art", + creator: "Jan Robert Leegte / Superposition", + }, + { + address: "0x7F72528229F85C99D8843C0317eF91F4A2793Edf", + name: "1111", + category: "Gen Art", + creator: "Kevin Abosch", + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Geometry Runners", + category: "Gen Art", + creator: "Rich Lord", + tokenIdRange: { + start: "138000000", + end: "138999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Ecumenopolis", + category: "Gen Art", + creator: "Joshua Bagley", + tokenIdRange: { + start: "119000000", + end: "119999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Edifice", + category: "Gen Art", + creator: "Ben Kovach", + tokenIdRange: { + start: "204000000", + end: "204999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Singularity", + category: "Gen Art", + creator: "Hideki Tsukamoto", + tokenIdRange: { + start: "8000000", + end: "8999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Rinascita", + category: "Gen Art", + creator: "Stefano Contiero", + tokenIdRange: { + start: "121000000", + end: "121999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Alien Insects", + category: "Gen Art", + creator: "Shvembldr", + tokenIdRange: { + start: "137000000", + end: "137999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "720 Minutes", + category: "Gen Art", + creator: "Alexis André", + tokenIdRange: { + start: "27000000", + end: "27999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "CENTURY", + category: "Gen Art", + creator: "Casey REAS", + tokenIdRange: { + start: "100000000", + end: "100999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "LeWitt Generator Generator", + category: "Gen Art", + creator: "Mitchell F. Chan", + tokenIdRange: { + start: "118000000", + end: "118999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Endless Nameless", + category: "Gen Art", + creator: "Rafaël Rozendaal", + tokenIdRange: { + start: "120000000", + end: "120999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Obicera", + category: "Gen Art", + creator: "Alexis André", + tokenIdRange: { + start: "130000000", + end: "130999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Bubble Blobby", + category: "Gen Art", + creator: "Jason Ting", + tokenIdRange: { + start: "62000000", + end: "62999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Divisions", + category: "Gen Art", + creator: "Michael Connolly", + tokenIdRange: { + start: "108000000", + end: "108999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Phototaxis", + category: "Gen Art", + creator: "Casey REAS", + tokenIdRange: { + start: "164000000", + end: "164999999", + }, + }, + { + address: "0x99a9b7c1116f9ceeb1652de04d5969cce509b069", + name: "ORI", + category: "Gen Art", + creator: "James Merrill", + tokenIdRange: { + start: "379000000", + end: "379999999", + }, + }, + { + address: "0x99a9b7c1116f9ceeb1652de04d5969cce509b069", + name: "Trichro-matic", + category: "Gen Art", + creator: "MountVitruvius", + tokenIdRange: { + start: "482000000", + end: "482999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Return", + category: "Gen Art", + creator: "Aaron Penne", + tokenIdRange: { + start: "77000000", + end: "77999999", + }, + }, + { + address: "0x99a9b7c1116f9ceeb1652de04d5969cce509b069", + name: "Pre-Process", + category: "Gen Art", + creator: "Casey REAS", + tokenIdRange: { + start: "383000000", + end: "383999999", + }, + }, + { + address: "0x99a9b7c1116f9ceeb1652de04d5969cce509b069", + name: "Cargo", + category: "Gen Art", + creator: "Kim Asendorf", + tokenIdRange: { + start: "426000000", + end: "426999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Ieva", + category: "Gen Art", + creator: "Shvembldr", + tokenIdRange: { + start: "339000000", + end: "339999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Color Study", + category: "Gen Art", + creator: "Jeff Davis", + tokenIdRange: { + start: "16000000", + end: "16999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "R3sonance", + category: "Gen Art", + creator: "ge1doot", + tokenIdRange: { + start: "19000000", + end: "19999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Primitives", + category: "Gen Art", + creator: "Aranda\\Lasch", + tokenIdRange: { + start: "368000000", + end: "368999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "RASTER", + category: "Gen Art", + creator: "itsgalo", + tokenIdRange: { + start: "341000000", + end: "341999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Messengers", + category: "Gen Art", + creator: "Alexis André", + tokenIdRange: { + start: "68000000", + end: "68999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Squares", + category: "Gen Art", + creator: "Martin Grasser", + tokenIdRange: { + start: "330000000", + end: "330999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "The Liths of Sisyphus", + category: "Gen Art", + creator: "nonfigurativ", + tokenIdRange: { + start: "124000000", + end: "124999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Stroming", + category: "Gen Art", + creator: "Bart Simons", + tokenIdRange: { + start: "86000000", + end: "86999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Paths", + category: "Gen Art", + creator: "Darien Brito", + tokenIdRange: { + start: "217000000", + end: "217999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Enchiridion", + category: "Gen Art", + creator: "Generative Artworks", + tokenIdRange: { + start: "101000000", + end: "101999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Getijde", + category: "Gen Art", + creator: "Bart Simons", + tokenIdRange: { + start: "226000000", + end: "226999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Flux", + category: "Gen Art", + creator: "Owen Moore", + tokenIdRange: { + start: "296000000", + end: "296999999", + }, + }, + { + address: "0x99a9b7c1116f9ceeb1652de04d5969cce509b069", + name: "Good, Computer", + category: "Gen Art", + creator: "Dean Blacc", + tokenIdRange: { + start: "396000000", + end: "396999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Non Either", + category: "Gen Art", + creator: "Rafaël Rozendaal", + tokenIdRange: { + start: "260000000", + end: "260999999", + }, + }, + { + address: "0x99a9b7c1116f9ceeb1652de04d5969cce509b069", + name: "Gumbo", + category: "Gen Art", + creator: "Mathias Isaksen", + tokenIdRange: { + start: "462000000", + end: "462999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "70s Pop Series One", + category: "Gen Art", + creator: "Daniel Catt", + tokenIdRange: { + start: "46000000", + end: "46999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Vahria", + category: "Gen Art", + creator: "Darien Brito", + tokenIdRange: { + start: "340000000", + end: "340999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Pointila", + category: "Gen Art", + creator: "Phaust", + tokenIdRange: { + start: "353000000", + end: "353999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Intersections", + category: "Gen Art", + creator: "Rafaël Rozendaal", + tokenIdRange: { + start: "373000000", + end: "373999999", + }, + }, + { + address: "0x99a9b7c1116f9ceeb1652de04d5969cce509b069", + name: "This Is Not A Rock", + category: "Gen Art", + creator: "Nicole Vella", + tokenIdRange: { + start: "471000000", + end: "471999999", + }, + }, + { + address: "0x99a9b7c1116f9ceeb1652de04d5969cce509b069", + name: "Immaterial", + category: "Gen Art", + creator: "Bjørn Staal", + tokenIdRange: { + start: "481000000", + end: "481999999", + }, + }, + { + address: "0x7d2d93eed47e55c873b9580b4e6ebd5bc045d1b6", + name: "Maschine", + category: "Gen Art", + }, + { + address: "0xcbc8a5472bba032125c1a7d11427aa3b5035207b", + name: "Blocks", + category: "Gen Art", + creator: "Harto", + }, + { + address: "0x145789247973c5d612bf121e9e4eef84b63eb707", + name: "923 EMPTY ROOMS", + category: "Gen Art", + creator: "Casey REAS", + tokenIdRange: { + start: "1000000", + end: "1999999", + }, + }, + { + address: "0x71b1956bc6640a70893e49f5816724425891f159", + name: "Fleeting Thoughts", + category: "Gen Art", + creator: "Nadieh Bremer", + }, + { + address: "0xc332fa232ab53628d0e9acbb806c5ee5a82b3467", + name: "Hypnagogic", + category: "Gen Art", + creator: "rudxane", + }, + { + address: "0x32d4be5ee74376e08038d652d4dc26e62c67f436", + name: "Elefante", + category: "Gen Art", + creator: "Michael Connolly", + tokenIdRange: { + start: "4000000", + end: "4999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Brushpops", + category: "Gen Art", + creator: "Matty Mariansky", + tokenIdRange: { + start: "135000000", + end: "135999999", + }, + }, + { + address: "0xeb7088423d7f8c1448ef074fc372bc67efa4de44", + name: "Toys", + category: "Gen Art", + creator: "0xTechno", + }, + { + address: "0x99a9b7c1116f9ceeb1652de04d5969cce509b069", + name: "Fleur", + category: "Gen Art", + creator: "AnaPet", + tokenIdRange: { + start: "378000000", + end: "378999999", + }, + }, + { + address: "0x29e891f4f2ae6a516026e3bcf0353d798e1de90", + name: "Cathartic Prism", + category: "Gen Art", + }, + { + address: "0x0a1bbd57033f57e7b6743621b79fcb9eb2ce3676", + name: "100 Sunsets", + category: "Gen Art", + creator: "Zach Lieberman", + tokenIdRange: { + start: "29000000", + end: "29999999", + }, + }, + { + address: "0x0a1bbd57033f57e7b6743621b79fcb9eb2ce3676", + name: "Sparkling Goodbye", + category: "Gen Art", + creator: "Licia He", + tokenIdRange: { + start: "47000000", + end: "47999999", + }, + }, + { + address: "0xe034bb2b1b9471e11cf1a0a9199a156fb227aa5d", + name: "Themes and Variations", + category: "Gen Art", + creator: "Vera Molnár", + }, + { + address: "0x0a1bbd57033f57e7b6743621b79fcb9eb2ce3676", + name: "Formation", + category: "Gen Art", + creator: "Jeff Davis", + tokenIdRange: { + start: "11000000", + end: "11999999", + }, + }, + { + address: "0x229b1a62210c2329fe7a0ee67f517ae611789b35", + name: "CIPHERS", + category: "Gen Art", + creator: "Per Kristian Stoveland", + }, + { + address: "0xaa39b261b8d4fdaa8a1ed436cc14a723c0480ee9", + name: "Glitch", + category: "Gen Art", + }, + { + address: "0x95864937cc8c90878c3254cf418632f8154d3b7d", + name: "Quadrature", + category: "Gen Art", + creator: "Darien Brito", + }, + { + address: "0x9bf53d8c65f03d895dacaa776cc960e462ecb599", + name: "Primera", + category: "Gen Art", + creator: "Mitchell and Yun", + }, + { + address: "0x0a1bbd57033f57e7b6743621b79fcb9eb2ce3676", + name: "1935", + category: "Gen Art", + creator: "William Mapan", + tokenIdRange: { + start: "25000000", + end: "25999999", + }, + }, + { + address: "0x99a9b7c1116f9ceeb1652de04d5969cce509b069", + name: "Memories of Digital Data", + category: "Gen Art", + creator: "Kazuhiro Tanimoto", + tokenIdRange: { + start: "428000000", + end: "428999999", + }, + }, + { + address: "0x2c7f335460fb9df460ff7ad6cc64cb7dd4064862", + name: "BITFRAMES", + category: "Gen Art", + }, + + // Photography Collections + { + address: "0x509a050f573be0d5e01a73c3726e17161729558b", + name: "Where My Vans Go", + category: "Photography", + }, + // ... rest of Photography collections ... + + // AI Inspired Collections + // ... AI Inspired collections ... + + // Memetics Collections + // ... Memetics collections ... + + // Iconic Gems Collections + { + address: "0xd754937672300ae6708a51229112de4017810934", + name: "DEAFBEEF Series 4", + category: "Iconic Gems", + }, + { + address: "0x34eebee6942d8def3c125458d1a86e0a897fd6f9", + name: "Checks VV", + category: "Iconic Gems", + }, + { + address: "0x6339e5e072086621540d0362c4e3cea0d643e114", + name: "Opepen", + category: "Iconic Gems", + }, + { + address: "0xc3f733ca98e0dad0386979eb96fb1722a1a05e69", + name: "Mooncats", + category: "Iconic Gems", + }, + { + address: "0xdb7F99605FD3Cc23067c3d8c1bA637109f083dc2", + name: "Doppelganger", + category: "Iconic Gems", + }, + { + address: "0x6b6dd0c1aab55052bfaac891c3fb81a1cd7230ec", + name: "Justin Aversano - Cognition", + category: "Iconic Gems", + creator: "Justin Aversano", + }, + { + address: "0xb92b8d7e45c0f197a8236c8345b86765250baf7c", + name: "Asprey Bugatti La Voiture Noire Collection", + category: "Iconic Gems", + }, + { + address: "0x5e86F887fF9676a58f25A6E057B7a6B8d65e1874", + name: "Bitchcoin", + category: "Iconic Gems", + }, + { + address: "0x7bd29408f11d2bfc23c34f18275bbf23bb716bc7", + name: "MeeBits", + category: "Iconic Gems", + }, + { + address: "0x12f28e2106ce8fd8464885b80ea865e98b465149", + name: "Beeple Genesis", + category: "Iconic Gems", + creator: "Beeple", + }, + { + address: "0xb852c6b5892256c264cc2c888ea462189154d8d7", + name: "rektguy", + category: "Iconic Gems", + }, + { + address: "0x7487b35cc8902964599a6e5a90763a8e80f1395e", + name: "Life In Japan Editions", + category: "Iconic Gems", + creator: "Grant Yun", + }, + { + address: "0xc17038437143b7d62f0bf861ccc154889d17efe9", + name: "Beeple Everydays", + category: "Iconic Gems", + creator: "Beeple", + }, + { + address: "0xae1fb0cce66904b9fa2b60bef2b8057ce2441538", + name: "REPLICATOR", + category: "Iconic Gems", + creator: "Mad Dog Jones", + tokenIdRange: { + start: "4295032833", + end: "4295032833", + }, + }, + { + address: "0x082dcab372505ae56eafde58204ba5b12ff3f3f5", + name: "Light Years", + category: "Iconic Gems", + creator: "Dmitri Cherniak", + }, + { + address: "0x8a939fd297fab7388d6e6c634eee3c863626be57", + name: "xCopy", + category: "Iconic Gems", + creator: "XCOPY", + }, + { + address: "0xaadc2d4261199ce24a4b0a57370c4fcf43bb60aa", + name: "The Currency", + category: "Iconic Gems", + creator: "Damien Hirst", + }, + { + address: "0x513cd71defc801b9c1aa763db47b5df223da77a2", + name: "OSF's Red Lite District", + category: "Iconic Gems", + }, + { + address: "0x1f493aa73c628259f755fd8b6540a3b4de3e994c", + name: "Decal", + category: "Iconic Gems", + creator: "Reuben Wu", + }, + { + address: "0x6b00de202e3cd03c523ca05d8b47231dbdd9142b", + name: "Tom Sachs: Rocket Factory - Rockets", + category: "Iconic Gems", + creator: "Tom Sachs", + }, + { + address: "0xc2c747e0f7004f9e8817db2ca4997657a7746928", + name: "Hashmasks", + category: "Iconic Gems", + }, + { + address: "0x68d0f6d1d99bb830e17ffaa8adb5bbed9d6eec2e", + name: "Penthouse", + category: "Iconic Gems", + creator: "0xdgb", + tokenIdRange: { + start: "opensea-penthouse-by-0xdgb", + end: "opensea-penthouse-by-0xdgb", + }, + }, + { + address: "0x33fd426905f149f8376e227d0c9d3340aad17af1", + name: "6529Collections", + category: "Iconic Gems", + }, + { + address: "0x34b45aad69b78bf5dc8cc2ac74d895f522a451a9", + name: "Light Years: Process Works", + category: "Iconic Gems", + creator: "Dmitri Cherniak", + }, + { + address: "0x7afeda4c714e1c0a2a1248332c100924506ac8e6", + name: "FVCK_CRYSTAL", + category: "Iconic Gems", + }, + { + address: "0x2e55fb6e20e29344adb531200811007092051443", + name: "Pop Wonder SuperRare", + category: "Iconic Gems", + }, + { + address: "0xd754937672300ae6708a51229112de4017810934", + name: "DeadBeef", + category: "Iconic Gems", + creator: "DEAFBEEF", + }, + { + address: "0xda1bf9b5de160cecde3f9304b187a2f5f5b83707", + name: "CHRONOPHOTOGRAPH", + category: "Iconic Gems", + creator: "0xDEAFBEEF", + }, + { + address: "0x6f854b0c8c596128504eaff09eae53ca625bad90", + name: "0xdgb Editions (2023)", + category: "Iconic Gems", + creator: "0xdgb", + }, + { + address: "0x495f947276749ce646f68ac8c248420045cb7b5e", + name: "Pop Wonder OS", + category: "Iconic Gems", + tokenIdRange: { + start: "opensea-pop-wonder-world", + end: "opensea-pop-wonder-world", + }, + }, + { + address: "0xd92e44ac213b9ebda0178e1523cc0ce177b7fa96", + name: "Beeple", + category: "Iconic Gems", + creator: "Beeple", + }, + { + address: "0xd1169e5349d1cb9941f3dcba135c8a4b9eacfdde", + name: "Max Pain Xcopy", + category: "Iconic Gems", + creator: "XCOPY", + }, + { + address: "0xCcDF1373040D9Ca4B5BE1392d1945C1DaE4a862c", + name: "Porsche", + category: "Iconic Gems", + }, + { + address: "0x495f947276749ce646f68ac8c248420045cb7b5e", + name: "SABET og", + category: "Iconic Gems", + creator: "SABET", + tokenIdRange: { + start: "opensea-sabet", + end: "opensea-sabet", + }, + }, + { + address: "0xd90829c6c6012e4dde506bd95d7499a04b9a56de", + name: "The Broken Keys", + category: "Iconic Gems", + }, + { + address: "0xc0979e362143b7d62f0bf861ccc154889d17efe9", + name: "Curious Cabins", + category: "Iconic Gems", + }, + { + address: "0x0dbfb2640f0692dd96d6d66657a1eac816121f03", + name: "Caravan", + category: "Iconic Gems", + }, + { + address: "0x495f947276749ce646f68ac8c248420045cb7b5e", + name: "Pop Wonder Editions", + category: "Iconic Gems", + tokenIdRange: { + start: "opensea-pop-wonder-editions", + end: "opensea-pop-wonder-editions", + }, + }, + { + address: "0x09b0ef6e8ef63db4be5df9e20b5f4fd3e3b92dac", + name: "Porsche Pioneers", + category: "Iconic Gems", + }, + { + address: "0x0cf3da2732ae7f078f8400c7325496774761d098", + name: "Daniloff", + category: "Iconic Gems", + }, + { + address: "0x4f96a7116a4c2391fdaf239d2fb7260ac2fc0545", + name: "Cath behind the scenes", + category: "Iconic Gems", + }, + { + address: "0xe8554c1362ffedc2664645a9a90be54a08ee1b44", + name: "Blue Patagonia", + category: "Iconic Gems", + }, + { + address: "0x1ac8acb916fd62b5ed35587a10d64cdfc940a271", + name: "Night Vision Series", + category: "Iconic Gems", + creator: "Jake Fried", + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Running Moon", + category: "Iconic Gems", + creator: "Licia He", + tokenIdRange: { + start: "334000000", + end: "334999999", + }, + }, + { + address: "0x4d928ab507bf633dd8e68024a1fb4c99316bbdf3", + name: "LOVE Tennis Art Project", + category: "Iconic Gems", + creator: "Martin Grasser", + }, + { + address: "0xd1169e5349d1cb9941f3dcba135c8a4b9eacfdde", + name: "MAX PAIN AND FRENS", + category: "Iconic Gems", + creator: "XCOPY", + }, + { + address: "0x34eebee6942d8def3c125458d1a86e0a897fd6f9", + name: "Checks - VV Edition", + category: "Iconic Gems", + }, + { + address: "0x6339e5e072086621540d0362c4e3cea0d643e114", + name: "Opepen Edition", + category: "Iconic Gems", + }, + { + address: "0xefec8fb24b41b9ea9c594eb7956aadcc6dd0490f", + name: "Vibes", + category: "Iconic Gems", + creator: "Amber Vittoria", + }, + { + address: "0x8cdbd7010bd197848e95c1fd7f6e870aac9b0d3c", + name: "Trademark", + category: "Iconic Gems", + creator: "Jack Butcher", + tokenIdRange: { + start: "4000000", + end: "4999999", + }, + }, + { + address: "0x8cdbd7010bd197848e95c1fd7f6e870aac9b0d3c", + name: "Signature", + category: "Iconic Gems", + creator: "Jack Butcher", + tokenIdRange: { + start: "3000000", + end: "3999999", + }, + }, + { + address: "0xda6558fa1c2452938168ef79dfd29c45aba8a32b", + name: "LUCI: Chapter 5 - The Monument Game", + category: "Iconic Gems", + creator: "Sam Spratt", + }, + { + address: "0xdfea2b364db868b1d2601d6b833d74db4de94460", + name: "REMNANTS", + category: "Iconic Gems", + }, + { + address: "0x16edf9d65a54e1617921a8125d77ef48c4e8c449", + name: "Monster Soup", + category: "Iconic Gems", + creator: "Des Lucrece", + }, + { + address: "0x5116edd4ac94d6aeb54b5a1533ca51a7e0c86807", + name: "Station3 Patron", + category: "Iconic Gems", + }, + { + address: "0xe77ad290adab2989a81ae62ab2467c01b45feeff", + name: "Proceed w/ Caution", + category: "Iconic Gems", + }, + { + address: "0xb2e6951a52d38814ed3ce2f4b9bec26091304747", + name: "Ackstract Editions", + category: "Iconic Gems", + }, + { + address: "0x25b834999ea471429ee211e2d465e85adae0ce14", + name: "batz editions", + category: "Iconic Gems", + }, + { + address: "0xb41e9aa79bda9890e9c74127d2af0aa610606aed", + name: "EXIF", + category: "Iconic Gems", + creator: "Guido Di Salle", + }, + { + address: "0x720786231ddf158ebd23bd590f73b29bff78d783", + name: "Strands of Solitude", + category: "Iconic Gems", + creator: "William Mapan", + }, + { + address: "0x8bd8eab9655573165fdafa404e72dc5e769a83fa", + name: "Alternate", + category: "Iconic Gems", + creator: "Kim Asendorf", + }, + { + address: "0x379b5616a6afe6bc6baa490ef8fd98bf6d7db45c", + name: "Checks - VV Elements", + category: "Iconic Gems", + }, + { + address: "0xa94161fbe69e08ff5a36dfafa61bdf29dd2fb928", + name: "Voxelglyph", + category: "Iconic Gems", + }, + { + address: "0x026224a2940bfe258d0dbe947919b62fe321f042", + name: "lobsterdao", + category: "Iconic Gems", + }, + { + address: "0x36f4d96fe0d4eb33cdc2dc6c0bca15b9cdd0d648", + name: "gmDAO", + category: "Iconic Gems", + }, + { + address: "0xfd6a5540ad049853420c42bbd46c01fd5c9e5f5a", + name: "Interwoven", + category: "Iconic Gems", + creator: "Emily Xie", + }, + { + address: "0xd32938e992a1821b6441318061136c83ea715ba1", + name: "Formation", + category: "Iconic Gems", + creator: "Harto", + }, + { + address: "0x4b33a369a9b4ff51bfc0a7267e30940507b81d84", + name: "Distance", + category: "Iconic Gems", + creator: "William Mapan", + }, + { + address: "0x9f803635a5af311d9a3b73132482a95eb540f71a", + name: "The Great Color Study", + category: "Iconic Gems", + }, + { + address: "0x36f20faf3785d226bf5478f9b271a7077859b5a9", + name: "SquiggleDAO", + category: "Iconic Gems", + }, + { + address: "0xb034fa4ba0a5cca4bd9f5b9db845fb26c5500b8c", + name: "Decal", + category: "Iconic Gems", + creator: "XCOPY", + }, + { + address: "0x186e2eece5ddbac8f1dde73723586b2c86aa8b58", + name: "ACID PEPES", + category: "Iconic Gems", + creator: "LORS", + }, + { + address: "0xbf476fad7e4ae2d679e9e739d3704a890f53c2a2", + name: "Now Pass", + category: "Iconic Gems", + }, + { + address: "0x66293a9b1339ca99623e82bc71f88d767f60ad21", + name: "Catharsis", + category: "Iconic Gems", + creator: "Dario Lanza", + }, + { + address: "0xc23a563a26afff06e945ace77173e1568f288ce5", + name: "OSF Editions Season 1", + category: "Iconic Gems", + }, + { + address: "0x27787755137863bb7f2387ed34942543c9f24efe", + name: "Factura", + category: "Iconic Gems", + creator: "Mathias Isaksen", + }, + { + address: "0x8eaa9ae1ac89b1c8c8a8104d08c045f78aadb42d", + name: "Tableland Rigs", + category: "Iconic Gems", + }, + { + address: "0x495f947276749ce646f68ac8c248420045cb7b5e", + name: "Cozy Homes", + category: "Iconic Gems", + creator: "Grant Yun", + tokenIdRange: { + start: "opensea-cozyhomes", + end: "opensea-cozyhomes", + }, + }, + { + address: "0xd3f9551e9bc926cc180ac8d3e27364f4081df624", + name: "servants of the Muse", + category: "Iconic Gems", + }, + { + address: "0xd752ad52ab60e58960e8a193c037383ffce8dd70", + name: "Open Eyes (Signal)", + category: "Iconic Gems", + creator: "Jake Fried", + }, + { + address: "0xbd874d3d6c27f1d3156001e5df38a3dfdd3dbcf8", + name: "alterego", + category: "Iconic Gems", + creator: "Russell Young", + }, + { + address: "0xd93eb3bcd333d934b5c18f28fee3ab72b2aec5af", + name: "ripcache", + category: "Iconic Gems", + }, + { + address: "0x3c72d904a2006c02e4ebdbab32477e9182d9e59d", + name: "Warothys", + category: "Iconic Gems", + }, + { + address: "0x49129a186169ecebf3c1ab036d99d4ecb9a95c67", + name: "The Flowers Project", + category: "Iconic Gems", + }, + { + address: "0x7e9b9ba1a3b4873279857056279cef6a4fcdf340", + name: "Noble Gallery", + category: "Iconic Gems", + }, + { + address: "0x055f16af0c61aa67176224d8c2407c9a5628bcca", + name: "archive edition", + category: "Iconic Gems", + }, + { + address: "0x31237f02f9b7ffc22ea7a9d9649520c0833d16f4", + name: "Amber Vittoria's Artwork", + category: "Iconic Gems", + creator: "Amber Vittoria", + }, + { + address: "0x05218d1744caf09190f72333f9167ce12d18af5c", + name: "Memories Of A Masterpiece", + category: "Iconic Gems", + }, + { + address: "0x1067b71aac9e2f2b1a4e6ab6c1ed10510876924a", + name: "24 Hours of Art", + category: "Iconic Gems", + }, + { + address: "0x5b9e53848d28db2295f5d25ae634c4f7711a2216", + name: "Two Worlds", + category: "Iconic Gems", + creator: "Jeremy Booth & Orkhan Isayev", + }, + { + address: "0x495f947276749ce646f68ac8c248420045cb7b5e", + name: "It's Because You're Pretty", + category: "Iconic Gems", + creator: "Amber Vittoria", + tokenIdRange: { + start: "opensea-amber-vittoria-pretty", + end: "opensea-amber-vittoria-pretty", + }, + }, + { + address: "0x5ab44d97b0504ed90b8c5b8a325aa61376703c88", + name: "E30D", + category: "Iconic Gems", + creator: "glitch gallery", + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "Incomplete Control", + category: "Iconic Gems", + creator: "Tyler Hobbs", + tokenIdRange: { + start: "228000000", + end: "228999999", + }, + }, + { + address: "0x059edd72cd353df5106d2b9cc5ab83a52287ac3a", + name: "Chromie Squiggle", + category: "Iconic Gems", + creator: "Snowfro", + tokenIdRange: { + start: "0", + end: "999999", + }, + }, + { + address: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", + name: "The Eternal Pump", + category: "Iconic Gems", + creator: "Dmitri Cherniak", + tokenIdRange: { + start: "22000000", + end: "22999999", + }, + }, + { + address: "0x112bec51a4b0942e7f7b2a5090f5ad57b7901559", + name: "TechnOrigami", + category: "Iconic Gems", + }, + { + address: "0xc3c415be22282859fbfc04ddd382685dfe7ed7f8", + name: "Decal", + category: "Iconic Gems", + creator: "Grant Yun", + }, + { + address: "0x9d63898298310c225de30ae9da0f0b738a7b7005", + name: "Samsung MX1 ART COLLECTION", + category: "Iconic Gems", + }, + { + address: "0xd4a6669e4787f23a2f711e0b6c6fb5431ce1594e", + name: "Geometries", + category: "Iconic Gems", + creator: "Frank Stella", + }, + { + address: "0xb932a70a57673d89f4acffbe830e8ed7f75fb9e0", + name: "SuperRare 1/1s: Dimitri Daniloff", + category: "Iconic Gems", + creator: "Dimitri Daniloff", + tokenIdRange: { + start: "superrare-shared-0xf9789dce5346c367c68ad0abcc2e38928d12dd9d", + end: "superrare-shared-0xf9789dce5346c367c68ad0abcc2e38928d12dd9d", + }, + }, + { + address: "0x0483b0dfc6c78062b9e999a82ffb795925381415", + name: "Orbit", + category: "Iconic Gems", + creator: "Jiannan Huang", + }, + { + address: "0x68d0f6d1d99bb830e17ffaa8adb5bbed9d6eec2e", + name: "Solitaire", + category: "Iconic Gems", + creator: "Terrell Jones", + tokenIdRange: { + start: "opensea-solitaire-by-terrell-jones", + end: "opensea-solitaire-by-terrell-jones", + }, + }, + { + address: "0x92ed200771647b26a5ea72737f1ba9a7366e471e", + name: "An Old Soul", + category: "Iconic Gems", + }, + { + address: "0xb932a70a57673d89f4acffbe830e8ed7f75fb9e0", + name: "SuperRare 1/1s: Brendan North", + category: "Iconic Gems", + creator: "Brendan North", + tokenIdRange: { + start: "superrare-shared-0x077bfc14dd6725f260e1abfd5c942ee13a27091b", + end: "superrare-shared-0x077bfc14dd6725f260e1abfd5c942ee13a27091b", + }, + }, + { + address: "0x3e34ff1790bf0a13efd7d77e75870cb525687338", + name: "DAMAGE CONTROL", + category: "Iconic Gems", + creator: "XCOPY", + }, + { + address: "0x8d9b2560bf173603b680c7c4780397775ddea09c", + name: "Pop Wonder Editions", + category: "Iconic Gems", + }, + { + address: "0xbc5dc6e819a5ff4686af6fb9b1550b5cabb3a58d", + name: "FVCKRENDER ARCHIVE", + category: "Iconic Gems", + creator: "FVCKRENDER", + }, + { + address: "0xc8bdf7c6e22930b8e8e1007ffc55be59b239ea93", + name: "Earth Iterations", + category: "Iconic Gems", + }, + { + address: "0x484e5155ae4b277cdb7f13a80ab3f627ff491149", + name: "Legalize Ground Beef", + category: "Iconic Gems", + }, + { + address: "0xbe39273b36c7bb971fed88c5f2a093270e0267e0", + name: "BODY MACHINE (MERIDIANS)", + category: "Iconic Gems", + creator: "Sougwen Chung", + }, + { + address: "0xcce4727300f460719588be90f7069c6f7b82748f", + name: "Edouard et Bastien", + category: "Iconic Gems", + }, + { + address: "0xc9976839b3db2e96e58abfbf4e42925d0656ec27", + name: "Edouard et Bastien", + category: "Iconic Gems", + }, + { + address: "0xbead5e1bd976bd8b27bd54ed50328e7364ea77bd", + name: "NORTH STAR", + category: "Iconic Gems", + creator: "Jake Fried", + }, + { + address: "0x6c646767b605e561846e7a4e8ee7afefe0af476c", + name: "The Cameras", + category: "Iconic Gems", + }, + { + address: "0xc04e0000726ed7c5b9f0045bc0c4806321bc6c65", + name: "ICXN", + category: "Iconic Gems", + }, +]; + +// Export helper functions +export { + isCuratedCollection, + getCuratedCollection, + getCuratedAddresses, + getFeaturedAddresses, + getVerifiedAddresses, +} from "./collections"; + +// Helper functions +export function getCollectionsByCategory( + category: CollectionCategory +): CuratedCollection[] { + return CURATED_COLLECTIONS.filter( + (collection) => collection.category === category + ); +} + +export function getCategoryCount(category: CollectionCategory): number { + return getCollectionsByCategory(category).length; +} + +export function getAllCategories(): CollectionCategory[] { + return [ + ...new Set( + CURATED_COLLECTIONS.map((collection) => collection.category) + ), + ]; +} + +export function getCollectionsByCreator(creator: string): CuratedCollection[] { + return CURATED_COLLECTIONS.filter( + (collection) => + collection.creator?.toLowerCase() === creator.toLowerCase() + ); +} + +// Create a map for quick lookups +export const COLLECTIONS_BY_ADDRESS = new Map( + CURATED_COLLECTIONS.map((collection) => [ + collection.address?.toLowerCase(), + collection, + ]) +); + +// URL and viewing helpers +export const IKIGAI_BASE_URL = "https://ikigailabs.xyz/ethereum"; + +export interface CollectionViewOptions { + sortBy?: + | "floor_asc" + | "floor_desc" + | "volume_asc" + | "volume_desc" + | "created_asc" + | "created_desc"; + filterBy?: "listed" | "all"; +} + +export function getCollectionUrl( + address: string, + collection?: CuratedCollection +): string { + if (!collection) { + collection = COLLECTIONS_BY_ADDRESS.get(address.toLowerCase()); + } + + let url = `${IKIGAI_BASE_URL}/${address}`; + + // If collection has tokenIdRange, append it to the URL + if (collection?.tokenIdRange?.start && collection?.tokenIdRange?.end) { + url += `:${collection.tokenIdRange.start}:${collection.tokenIdRange.end}`; + } + + return url; +} + +export function getCollectionViewUrl( + address: string, + options?: CollectionViewOptions +): string { + const collection = COLLECTIONS_BY_ADDRESS.get(address.toLowerCase()); + const baseUrl = getCollectionUrl(address, collection); + if (!options) return baseUrl; + + const params = new URLSearchParams(); + if (options.sortBy) params.append("sort", options.sortBy); + if (options.filterBy) params.append("filter", options.filterBy); + + return `${baseUrl}?${params.toString()}`; +} + +// Helper to get URLs for all collections in a category +export function getCategoryUrls(category: CollectionCategory): string[] { + return getCollectionsByCategory(category).map((collection) => + getCollectionUrl(collection.address, collection) + ); +} + +// Helper to get URLs for collections by a specific creator +export function getCreatorCollectionUrls(creator: string): string[] { + return getCollectionsByCreator(creator).map((collection) => + getCollectionUrl(collection.address, collection) + ); +} + +// Helper to get a formatted collection view with URL +export function getCollectionView(address: string): { + collection: CuratedCollection | undefined; + url: string; +} { + const collection = COLLECTIONS_BY_ADDRESS.get(address.toLowerCase()); + return { + collection, + url: getCollectionUrl(address, collection), + }; +} + +// Helper to get multiple collection views +export function getCollectionViews(addresses: string[]): { + collection: CuratedCollection | undefined; + url: string; +}[] { + return addresses.map((address) => getCollectionView(address)); +} + +// Helper to get all collections in a category with their URLs +export function getCategoryCollectionViews(category: CollectionCategory): { + collection: CuratedCollection; + url: string; +}[] { + return getCollectionsByCategory(category).map((collection) => ({ + collection, + url: getCollectionUrl(collection.address, collection), + })); +} + +// Helper to format collection data for display +export function formatCollectionData(collection: CuratedCollection): string { + const url = getCollectionUrl(collection.address, collection); + return ` +Collection: ${collection.name} +Category: ${collection.category} +${collection.creator ? `Creator: ${collection.creator}` : ""} +View on IkigaiLabs: ${url} +${collection.tokenIdRange ? `Token Range: ${collection.tokenIdRange.start || "0"} - ${collection.tokenIdRange.end || "unlimited"}` : ""} +`; +} + +// Helper to get a shareable collection link with optional sort/filter +export function getShareableCollectionLink( + address: string, + options?: CollectionViewOptions +): string { + 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/evaluators/nft-knowledge.ts b/packages/plugin-nft-collections/src/evaluators/nft-knowledge.ts new file mode 100644 index 00000000000..c8f78720038 --- /dev/null +++ b/packages/plugin-nft-collections/src/evaluators/nft-knowledge.ts @@ -0,0 +1,109 @@ +import { Evaluator, IAgentRuntime, Memory, State } from "@elizaos/core"; +import { NFTKnowledge } from "../types"; + +export const nftKnowledgeEvaluator: Evaluator = { + name: "nft-collection-evaluator", + description: "Evaluates NFT-related content in messages", + similes: [ + "nft-evaluator", + "nft-knowledge", + "market-analysis", + "artist-info", + ], + alwaysRun: false, + validate: async (runtime: IAgentRuntime, message: Memory) => { + const content = message.content.text.toLowerCase(); + return ( + content.includes("nft") || + content.includes("collection") || + content.includes("market") || + content.includes("trading") || + content.includes("artist") || + content.includes("contract") || + content.includes("news") || + content.includes("onchain") + ); + }, + handler: async (runtime: IAgentRuntime, message: Memory, state: State) => { + const content = message.content.text.toLowerCase(); + + const extractedInfo: NFTKnowledge = { + mentionsCollection: + content.includes("collection") || content.includes("nft"), + mentionsFloorPrice: + content.includes("floor price") || content.includes("floor"), + mentionsVolume: + content.includes("volume") || + content.includes("trading volume"), + mentionsRarity: + content.includes("rare") || content.includes("rarity"), + mentionsMarketTrends: + content.includes("trend") || + content.includes("market") || + content.includes("movement"), + mentionsTraders: + content.includes("trader") || + content.includes("whale") || + content.includes("investor"), + mentionsSentiment: + content.includes("bull") || + content.includes("bear") || + content.includes("sentiment") || + content.includes("mood"), + mentionsMarketCap: + content.includes("market cap") || + content.includes("marketcap") || + content.includes("valuation"), + mentionsArtist: + content.includes("artist") || + content.includes("creator") || + content.includes("founder"), + mentionsOnChainData: + content.includes("onchain") || + content.includes("blockchain") || + content.includes("contract") || + content.includes("holder") || + content.includes("transfer"), + mentionsNews: + content.includes("news") || + content.includes("announcement") || + content.includes("update"), + mentionsSocial: + content.includes("twitter") || + content.includes("discord") || + content.includes("telegram") || + content.includes("social"), + mentionsContract: + content.includes("contract") || + content.includes("royalty") || + content.includes("standard") || + content.includes("erc"), + }; + + return { + ...state, + nftKnowledge: extractedInfo, + }; + }, + examples: [ + { + context: "Evaluating comprehensive NFT collection data", + messages: [ + { + user: "{{user1}}", + content: { + text: "Tell me about the artist and on-chain stats for this collection", + }, + }, + { + user: "{{user2}}", + content: { + text: "I'll analyze the creator's background and blockchain metrics.", + }, + }, + ], + outcome: + "The message requests artist and on-chain information and should be evaluated.", + }, + ], +}; diff --git a/packages/plugin-nft-collections/src/evaluators/nft-taste-evaluator.ts b/packages/plugin-nft-collections/src/evaluators/nft-taste-evaluator.ts new file mode 100644 index 00000000000..785bbb869c8 --- /dev/null +++ b/packages/plugin-nft-collections/src/evaluators/nft-taste-evaluator.ts @@ -0,0 +1,192 @@ +import { z } from "zod"; + +// Comprehensive schema for NFT evaluation +export const NFTTasteSchema = z.object({ + name: z.string(), + creator: z.string(), + collection: z.string(), + description: z.string(), + imageUrl: z.string().url(), + evaluationCriteria: z.object({ + aesthetics: z.object({ + composition: z.number().min(0).max(10), + colorPalette: z.number().min(0).max(10), + originality: z.number().min(0).max(10), + }), + conceptualDepth: z.object({ + meaningfulness: z.number().min(0).max(10), + symbolism: z.number().min(0).max(10), + narrativeStrength: z.number().min(0).max(10), + }), + technicalMastery: z.object({ + technique: z.number().min(0).max(10), + complexity: z.number().min(0).max(10), + execution: z.number().min(0).max(10), + }), + culturalRelevance: z.object({ + contemporarySignificance: z.number().min(0).max(10), + artistReputation: z.number().min(0).max(10), + movementAlignment: z.number().min(0).max(10), + }), + }), +}); + +export type NFTTaste = z.infer; + +export class NFTTasteEvaluator { + private learningHistory: NFTTaste[] = []; + + evaluateNFTTaste(nft: NFTTaste): number { + const { + aesthetics, + conceptualDepth, + technicalMastery, + culturalRelevance, + } = nft.evaluationCriteria; + + // Weighted scoring system with dynamic weights + const weights = this.getDynamicWeights(); + + const score = + aesthetics.composition * weights.aesthetics.composition + + aesthetics.colorPalette * weights.aesthetics.colorPalette + + aesthetics.originality * weights.aesthetics.originality + + conceptualDepth.meaningfulness * + weights.conceptualDepth.meaningfulness + + conceptualDepth.symbolism * weights.conceptualDepth.symbolism + + conceptualDepth.narrativeStrength * + weights.conceptualDepth.narrativeStrength + + technicalMastery.technique * weights.technicalMastery.technique + + technicalMastery.complexity * weights.technicalMastery.complexity + + technicalMastery.execution * weights.technicalMastery.execution + + culturalRelevance.contemporarySignificance * + weights.culturalRelevance.contemporarySignificance; + + return Math.min(Math.max(score, 0), 100); + } + + private getDynamicWeights() { + // Default weights with potential for machine learning adjustment + return { + aesthetics: { + composition: 0.2, + colorPalette: 0.15, + originality: 0.15, + }, + conceptualDepth: { + meaningfulness: 0.1, + symbolism: 0.1, + narrativeStrength: 0.1, + }, + technicalMastery: { + technique: 0.1, + complexity: 0.05, + execution: 0.05, + }, + culturalRelevance: { + contemporarySignificance: 0.05, + }, + }; + } + + // Method to record preference and potentially adjust weights + recordPreference(nft: NFTTaste, isPreferred: boolean) { + this.learningHistory.push(nft); + + // Placeholder for more sophisticated learning mechanism + if (isPreferred) { + // Potentially adjust weights or learning model + console.log(`Learned preference for: ${nft.name}`); + } + } + + // Compare two NFTs without revealing preference + compareNFTs( + nftA: NFTTaste, + nftB: NFTTaste + ): { + nftAScore: number; + nftBScore: number; + } { + return { + nftAScore: this.evaluateNFTTaste(nftA), + nftBScore: this.evaluateNFTTaste(nftB), + }; + } + + // Future: Implement more advanced machine learning methods + trainTasteModel() { + // Placeholder for advanced ML training + console.log("Taste model training initiated"); + } +} + +// Example usage +export const nftTasteEvaluator = new NFTTasteEvaluator(); + +// Two sample NFTs for taste evaluation +export const nftA: NFTTaste = { + name: "Quantum Echoes", + creator: "Digital Dreamweaver", + collection: "Ethereal Visions", + description: + "A generative art piece exploring the intersection of quantum mechanics and digital consciousness", + imageUrl: "https://example.com/quantum-echoes.png", + evaluationCriteria: { + aesthetics: { + composition: 7, + colorPalette: 6, + originality: 8, + }, + conceptualDepth: { + meaningfulness: 9, + symbolism: 8, + narrativeStrength: 7, + }, + technicalMastery: { + technique: 7, + complexity: 8, + execution: 6, + }, + culturalRelevance: { + contemporarySignificance: 8, + artistReputation: 6, + movementAlignment: 7, + }, + }, +}; + +export const nftB: NFTTaste = { + name: "Urban Fragments", + creator: "Street Pixel", + collection: "Concrete Dreams", + description: + "A pixelated representation of urban decay and architectural fragmentation", + imageUrl: "https://example.com/urban-fragments.png", + evaluationCriteria: { + aesthetics: { + composition: 5, + colorPalette: 4, + originality: 6, + }, + conceptualDepth: { + meaningfulness: 5, + symbolism: 4, + narrativeStrength: 5, + }, + technicalMastery: { + technique: 5, + complexity: 4, + execution: 5, + }, + culturalRelevance: { + contemporarySignificance: 4, + artistReputation: 3, + movementAlignment: 5, + }, + }, +}; + +// Demonstration +const comparison = nftTasteEvaluator.compareNFTs(nftA, nftB); +console.log("NFT Comparison:", comparison); diff --git a/packages/plugin-nft-collections/src/evaluators/nft-taste-expert.ts b/packages/plugin-nft-collections/src/evaluators/nft-taste-expert.ts new file mode 100644 index 00000000000..4c6ff2ba918 --- /dev/null +++ b/packages/plugin-nft-collections/src/evaluators/nft-taste-expert.ts @@ -0,0 +1,440 @@ +import { z } from "zod"; +// @ts-ignore +import * as tf from "@tensorflow/tfjs-node"; +import axios from "axios"; +import { NFTTasteSchema, NFTTaste } from "./nft-taste-evaluator"; + +// Use NFTTaste as base type and extend with expert metadata +const NFTTasteExpertSchema = NFTTasteSchema.extend({ + expertMetadata: z + .object({ + emotionalImpact: z.number().min(0).max(10).optional(), + innovationScore: z.number().min(0).max(10).optional(), + culturalRelevance: z.number().min(0).max(10).optional(), + }) + .optional(), + metadata: z + .object({ + artMovement: z + .enum([ + "Generative Art", + "Digital Surrealism", + "Crypto Art", + "AI Art", + "Pixel Art", + "Minimalism", + "Abstract", + ]) + .optional(), + creatorReputation: z.number().optional(), + marketTraction: z.number().optional(), + emotionalImpact: z.number().optional(), + innovationScore: z.number().optional(), + culturalRelevance: z.number().optional(), + }) + .optional(), + evaluationCriteria: z + .object({ + aesthetics: z + .object({ + composition: z.number().optional(), + colorPalette: z.number().optional(), + originality: z.number().optional(), + }) + .optional(), + conceptualDepth: z + .object({ + meaningfulness: z.number().optional(), + symbolism: z.number().optional(), + narrativeStrength: z.number().optional(), + }) + .optional(), + technicalMastery: z + .object({ + technique: z.number().optional(), + complexity: z.number().optional(), + execution: z.number().optional(), + }) + .optional(), + psychologicalImpact: z + .object({ + emotionalResonance: z.number().optional(), + cognitiveComplexity: z.number().optional(), + perceptualChallenge: z.number().optional(), + }) + .optional(), + }) + .optional(), +}); + +type NFTTasteExpert = z.infer; + +export class AdvancedNFTTasteExpert { + private model: tf.Sequential | null = null; + private learningHistory: NFTTasteExpert[] = []; + private preferenceWeights: Record = { + aesthetics: 0.3, + conceptualDepth: 0.25, + technicalMastery: 0.2, + psychologicalImpact: 0.15, + marketTrends: 0.1, + }; + + constructor() { + this.initializeMachineLearningModel(); + } + + // Initialize TensorFlow machine learning model + private async initializeMachineLearningModel() { + this.model = tf.sequential(); + + // Add layers for learning NFT taste preferences + this.model.add( + tf.layers.dense({ + inputShape: [10], // Input features from NFT metadata + units: 16, + activation: "relu", + }) + ); + this.model.add( + tf.layers.dense({ + units: 8, + activation: "relu", + }) + ); + this.model.add( + tf.layers.dense({ + units: 1, + activation: "sigmoid", // Output preference probability + }) + ); + + this.model.compile({ + optimizer: "adam", + loss: "binaryCrossentropy", + metrics: ["accuracy"], + }); + } + + // Fetch external art criticism and market trends + private async fetchExternalInsights(nft: NFTTasteExpert) { + try { + // Hypothetical API calls to art criticism and market trend sources + const artCriticismResponse = await axios.get( + `https://art-criticism-api.com/analyze?nft=${nft.name}` + ); + const marketTrendsResponse = await axios.get( + `https://nft-market-trends.com/analyze?collection=${nft.collection}` + ); + + return { + criticismScore: artCriticismResponse.data.score || 0, + marketTrendScore: marketTrendsResponse.data.trendScore || 0, + }; + } catch (error) { + console.error("Error fetching external insights:", error); + return { criticismScore: 0, marketTrendScore: 0 }; + } + } + + // Advanced taste evaluation with multiple dimensions + async evaluateTaste(nft: NFTTasteExpert): Promise { + const { + aesthetics, + conceptualDepth, + technicalMastery, + psychologicalImpact, + } = nft.evaluationCriteria; + const { metadata } = nft; + + // External insights + const externalInsights = await this.fetchExternalInsights(nft); + + // Base score calculation with weighted dimensions + const baseScore = + (aesthetics.composition * 0.2 + + aesthetics.colorPalette * 0.1 + + aesthetics.originality * 0.1) * + this.preferenceWeights.aesthetics + + (conceptualDepth.meaningfulness * 0.1 + + conceptualDepth.symbolism * 0.1 + + conceptualDepth.narrativeStrength * 0.1) * + this.preferenceWeights.conceptualDepth + + (technicalMastery.technique * 0.1 + + technicalMastery.complexity * 0.1 + + technicalMastery.execution * 0.1) * + this.preferenceWeights.technicalMastery; + + // Psychological impact score + const psychologicalScore = psychologicalImpact + ? (psychologicalImpact.emotionalResonance * 0.1 + + psychologicalImpact.cognitiveComplexity * 0.1 + + psychologicalImpact.perceptualChallenge * 0.1) * + this.preferenceWeights.psychologicalImpact + : 0; + + // Market and external insights + const marketScore = + ((metadata?.marketTraction || 0) / 10 + + externalInsights.marketTrendScore / 10) * + this.preferenceWeights.marketTrends; + + // Machine learning prediction + const mlPrediction = await this.predictTastePreference(nft); + + // Comprehensive score calculation + const comprehensiveScore = + baseScore + psychologicalScore + marketScore + mlPrediction * 10; // Scale ML prediction + + // Bonus for unique metadata + const metadataBonus = this.calculateMetadataBonus(nft); + + return Math.min(Math.max(comprehensiveScore + metadataBonus, 0), 100); + } + + // Machine learning taste preference prediction + private async predictTastePreference(nft: NFTTasteExpert): Promise { + if (!this.model) { + console.warn("ML model not initialized"); + return 0.5; // Neutral prediction + } + + // Convert NFT features to tensor for prediction + const features = this.extractFeatures(nft); + const inputTensor = tf.tensor2d([features]); + + // Predict preference probability + const prediction = this.model.predict(inputTensor) as tf.Tensor; + const predictionValue = prediction.dataSync()[0]; + + return predictionValue; + } + + // Extract features from NFT for ML model + private extractFeatures(nft: NFTTasteExpert): number[] { + const { aesthetics, conceptualDepth, technicalMastery } = + nft.evaluationCriteria; + const { metadata } = nft; + + return [ + aesthetics.composition / 10, + aesthetics.colorPalette / 10, + aesthetics.originality / 10, + conceptualDepth.meaningfulness / 10, + conceptualDepth.symbolism / 10, + technicalMastery.technique / 10, + metadata?.creatorReputation || 0, + metadata?.marketTraction || 0, + metadata?.innovationScore || 0, + metadata?.culturalRelevance || 0, + ]; + } + + // Adaptive learning mechanism + async learnFromPreference(nft: NFTTasteExpert, isPreferred: boolean) { + this.learningHistory.push(nft); + + if (isPreferred && this.model) { + // Prepare training data + const features = this.extractFeatures(nft); + const labels = [isPreferred ? 1 : 0]; + + const featureTensor = tf.tensor2d([features]); + const labelTensor = tf.tensor2d([labels]); + + // Train model with new preference + await this.model.fit(featureTensor, labelTensor, { epochs: 1 }); + + // Dynamically adjust preference weights + this.adjustPreferenceWeights(nft); + } + } + + // Adaptive weight adjustment + private adjustPreferenceWeights(nft: NFTTasteExpert) { + const { aesthetics, conceptualDepth, technicalMastery } = + nft.evaluationCriteria; + + // Slightly increase weights for aspects of preferred NFTs + this.preferenceWeights.aesthetics *= 1.05; + this.preferenceWeights.conceptualDepth *= 1.03; + this.preferenceWeights.technicalMastery *= 1.02; + + // Normalize weights + const total = Object.values(this.preferenceWeights).reduce( + (a, b) => a + b, + 0 + ); + Object.keys(this.preferenceWeights).forEach((key) => { + this.preferenceWeights[key] /= total; + }); + } + + // Contextual bonus for additional metadata + private calculateMetadataBonus(nft: NFTTasteExpert): number { + let bonus = 0; + + if (nft.metadata) { + const movementBonus: Record = { + "Generative Art": 2, + "AI Art": 1.5, + "Digital Surrealism": 1.5, + "Crypto Art": 1, + "Pixel Art": 0.5, + }; + + if (nft.metadata.artMovement) { + bonus += movementBonus[nft.metadata.artMovement] || 0; + } + + bonus += (nft.metadata.creatorReputation || 0) / 2; + bonus += (nft.metadata.marketTraction || 0) / 3; + } + + return bonus; + } + + // Compare two NFTs without revealing preference + async compareNFTs(nftA: NFTTasteExpert, nftB: NFTTasteExpert) { + return { + nftAScore: await this.evaluateTaste(nftA), + nftBScore: await this.evaluateTaste(nftB), + }; + } + + // Bridge method to integrate with basic evaluator + async enhanceEvaluation(nft: z.infer) { + // Optional advanced evaluation + if (this.model) { + try { + const advancedScore = await this.evaluateTaste( + nft as NFTTasteExpert + ); + return { + baseEvaluation: nft, + advancedScore, + isEnhanced: true, + }; + } catch (error) { + console.warn("Advanced evaluation failed:", error); + return { + baseEvaluation: nft, + advancedScore: null, + isEnhanced: false, + }; + } + } + + // Fallback to basic evaluation if advanced model is not initialized + return { + baseEvaluation: nft, + advancedScore: null, + isEnhanced: false, + }; + } + + // Optional initialization method + async initializeOptionally() { + try { + await this.initializeMachineLearningModel(); + return true; + } catch (error) { + console.warn("Could not initialize advanced taste expert:", error); + return false; + } + } +} + +// Singleton instance for easy access +export const advancedNFTTasteExpert = new AdvancedNFTTasteExpert(); + +// Example NFTs for demonstration +export const nftA: NFTTasteExpert = { + name: "Quantum Echoes", + creator: "Digital Dreamweaver", + collection: "Ethereal Visions", + description: + "A generative art piece exploring quantum mechanics and digital consciousness", + imageUrl: "https://example.com/quantum-echoes.png", + metadata: { + artMovement: "Generative Art", + creatorReputation: 8, + marketTraction: 7, + emotionalImpact: 9, + innovationScore: 8, + culturalRelevance: 7, + }, + evaluationCriteria: { + aesthetics: { + composition: 8, + colorPalette: 7, + originality: 9, + }, + conceptualDepth: { + meaningfulness: 9, + symbolism: 8, + narrativeStrength: 8, + }, + technicalMastery: { + technique: 8, + complexity: 9, + execution: 7, + }, + psychologicalImpact: { + emotionalResonance: 8, + cognitiveComplexity: 9, + perceptualChallenge: 7, + }, + }, +}; + +export const nftB: NFTTasteExpert = { + name: "Urban Fragments", + creator: "Street Pixel", + collection: "Concrete Dreams", + description: + "A pixelated representation of urban decay and architectural fragmentation", + imageUrl: "https://example.com/urban-fragments.png", + metadata: { + artMovement: "Pixel Art", + creatorReputation: 5, + marketTraction: 4, + emotionalImpact: 6, + innovationScore: 5, + culturalRelevance: 5, + }, + evaluationCriteria: { + aesthetics: { + composition: 6, + colorPalette: 5, + originality: 7, + }, + conceptualDepth: { + meaningfulness: 6, + symbolism: 5, + narrativeStrength: 6, + }, + technicalMastery: { + technique: 6, + complexity: 5, + execution: 6, + }, + psychologicalImpact: { + emotionalResonance: 5, + cognitiveComplexity: 6, + perceptualChallenge: 5, + }, + }, +}; + +// Demonstration +async function demonstrateNFTTasteEvaluation() { + const comparison = await advancedNFTTasteExpert.compareNFTs(nftA, nftB); + console.log("NFT Comparison:", comparison); + + // Learn from preference + await advancedNFTTasteExpert.learnFromPreference(nftA, true); + await advancedNFTTasteExpert.learnFromPreference(nftB, false); +} + +demonstrateNFTTasteEvaluation(); diff --git a/packages/plugin-nft-collections/src/index.ts b/packages/plugin-nft-collections/src/index.ts new file mode 100644 index 00000000000..59c7d3fcd76 --- /dev/null +++ b/packages/plugin-nft-collections/src/index.ts @@ -0,0 +1,109 @@ +import { Plugin } from "@elizaos/core"; +import { createNftCollectionProvider } from "./providers/nft-collections"; +import { + getCollectionsAction, + getThinFloorNFTsAction, + manageWatchlistAction, +} from "./actions/get-collections"; +import { listNFTAction } from "./actions/list-nft"; +import { sweepFloorArbitrageAction } from "./actions/sweep-floor"; + +import { ReservoirService } from "./services/reservoir"; +import { MemoryCacheManager } from "./services/cache-manager"; +import { RateLimiter } from "./services/rate-limiter"; +import { MarketIntelligenceService } from "./services/market-intelligence"; +import { SocialAnalyticsService } from "./services/social-analytics"; +import { validateEnvironmentVariables } from "./utils/validation"; + +// Configuration with sensible defaults and environment variable overrides +const config = { + caching: { + enabled: process.env.CACHE_ENABLED === "true" || true, + ttl: Number(process.env.CACHE_TTL) || 3600000, // 1 hour + maxSize: Number(process.env.CACHE_MAX_SIZE) || 1000, + }, + security: { + rateLimit: { + enabled: process.env.RATE_LIMIT_ENABLED === "true" || true, + maxRequests: Number(process.env.RATE_LIMIT_MAX_REQUESTS) || 100, + windowMs: Number(process.env.RATE_LIMIT_WINDOW_MS) || 60000, + }, + }, + maxConcurrent: Number(process.env.MAX_CONCURRENT) || 5, + maxRetries: Number(process.env.MAX_RETRIES) || 3, + batchSize: Number(process.env.BATCH_SIZE) || 20, +}; + +function createNFTCollectionsPlugin(): Plugin { + // Validate environment variables + try { + validateEnvironmentVariables({ + TWITTER_API_KEY: process.env.TWITTER_API_KEY, + DUNE_API_KEY: process.env.DUNE_API_KEY, + OPENSEA_API_KEY: process.env.OPENSEA_API_KEY, + RESERVOIR_API_KEY: process.env.RESERVOIR_API_KEY, + }); + } catch (error) { + console.error("Environment Variable Validation Error:", error.message); + throw error; // Prevent plugin initialization with invalid config + } + + // Initialize reusable CacheManager if caching is enabled + const cacheManager = config.caching?.enabled + ? new MemoryCacheManager({ + ttl: config.caching.ttl, + maxSize: config.caching.maxSize, + }) + : null; + + // Initialize reusable RateLimiter if rate limiting is enabled + const rateLimiter = config.security?.rateLimit?.enabled + ? new RateLimiter({ + maxRequests: config.security.rateLimit.maxRequests, + windowMs: config.security.rateLimit.windowMs, + }) + : null; + + const reservoirService = new ReservoirService({ + cacheManager, + rateLimiter, + maxConcurrent: config.maxConcurrent, + maxRetries: config.maxRetries, + batchSize: config.batchSize, + }); + + const marketIntelligenceService = new MarketIntelligenceService({ + cacheManager, + rateLimiter, + openSeaApiKey: process.env.OPENSEA_API_KEY, + reservoirApiKey: process.env.RESERVOIR_API_KEY, + }); + + const socialAnalyticsService = new SocialAnalyticsService({ + twitterApiKey: process.env.TWITTER_API_KEY, + duneApiKey: process.env.DUNE_API_KEY, + }); + + const nftCollectionProvider = createNftCollectionProvider( + reservoirService, + marketIntelligenceService, + socialAnalyticsService + ); + + return { + name: "nft-collections", + description: + "Provides NFT collection information and market intelligence", + providers: [nftCollectionProvider], + actions: [ + getCollectionsAction(nftCollectionProvider), + listNFTAction(reservoirService), + sweepFloorArbitrageAction(reservoirService, nftCollectionProvider), + getThinFloorNFTsAction(nftCollectionProvider, reservoirService), + manageWatchlistAction(nftCollectionProvider), + ], + evaluators: [], + }; +} + +export default createNFTCollectionsPlugin; diff --git a/packages/plugin-nft-collections/src/providers/nft-collections.ts b/packages/plugin-nft-collections/src/providers/nft-collections.ts new file mode 100644 index 00000000000..87d76398f09 --- /dev/null +++ b/packages/plugin-nft-collections/src/providers/nft-collections.ts @@ -0,0 +1,92 @@ +import { Provider, type IAgentRuntime, type Memory } from "@elizaos/core"; +import { ReservoirService } from "../services/reservoir"; +import { MarketIntelligenceService } from "../services/market-intelligence"; +import { SocialAnalyticsService } from "../services/social-analytics"; + +export const createNftCollectionProvider = ( + nftService: ReservoirService, + marketIntelligenceService: MarketIntelligenceService, + socialAnalyticsService: SocialAnalyticsService +): Provider => { + return { + get: async ( + runtime: IAgentRuntime, + message: Memory + ): Promise => { + if (!nftService) { + throw new Error("NFT service not found"); + } + + const collections = await nftService.getTopCollections(runtime, 10); + let response = "Here are the top NFT collections:\n\n"; + + for (const collection of collections) { + response += `${collection.name}:\n`; + response += `• Floor Price: ${collection.floorPrice} ETH\n`; + response += `• 24h Volume: ${collection.volume24h} ETH\n`; + response += `• Market Cap: ${collection.marketCap} ETH\n`; + response += `• Holders: ${collection.holders}\n\n`; + } + + // If a specific collection is mentioned in the message, get detailed information + const collection = collections.find( + (c) => + message.content.text + .toLowerCase() + .includes(c.name.toLowerCase()) || + message.content.text + .toLowerCase() + .includes(c.address.toLowerCase()) + ); + + if (collection) { + response += `\nDetailed information for ${collection.name}:\n\n`; + + // Market intelligence data (optional) + if (marketIntelligenceService) { + try { + const marketIntelligence = + await marketIntelligenceService.getMarketIntelligence( + collection.address + ); + response += "Market Intelligence:\n"; + response += `• Best Offer: ${marketIntelligence.bestOffer || "N/A"} ETH\n`; + response += `• 24h Volume: ${marketIntelligence.volume24h || "N/A"} ETH\n`; + response += `• 7d Volume: ${marketIntelligence.volume7d || "N/A"} ETH\n`; + response += `• 30d Volume: ${marketIntelligence.volume30d || "N/A"} ETH\n\n`; + } catch (error) { + console.error( + "Failed to fetch market intelligence:", + error + ); + } + } + + // Social analytics data (optional) + if (socialAnalyticsService) { + try { + const socialMetrics = + await socialAnalyticsService.getSocialMetrics( + collection.address + ); + + response += "Social Metrics:\n"; + response += `• Twitter Followers: ${socialMetrics.twitterFollowers || "N/A"}\n`; + response += `• Twitter Engagement: ${socialMetrics.twitterEngagement || "N/A"}\n`; + response += `• Discord Members: ${socialMetrics.discordMembers || "N/A"}\n`; + response += `• Discord Active: ${socialMetrics.discordActive || "N/A"}\n`; + response += `• Telegram Members: ${socialMetrics.telegramMembers || "N/A"}\n`; + response += `• Telegram Active: ${socialMetrics.telegramActive || "N/A"}\n\n`; + } catch (error) { + console.error( + "Failed to fetch social analytics:", + error + ); + } + } + } + + return response; + }, + }; +}; diff --git a/packages/plugin-nft-collections/src/services/alchemy.ts b/packages/plugin-nft-collections/src/services/alchemy.ts new file mode 100644 index 00000000000..d723880181f --- /dev/null +++ b/packages/plugin-nft-collections/src/services/alchemy.ts @@ -0,0 +1,321 @@ +export class AlchemyService { + private baseUrl = "https://eth-mainnet.g.alchemy.com/v2/"; + private apiKey: string; + + constructor(apiKey?: string) { + this.apiKey = apiKey || process.env.ALCHEMY_API_KEY || ""; + if (!this.apiKey) { + throw new Error("Alchemy API key is required"); + } + } + + private async fetchAlchemyAPI( + method: string, + params: any = {} + ): Promise { + const response = await fetch(`${this.baseUrl}${this.apiKey}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method, + params: [params], + }), + }); + + if (!response.ok) { + throw new Error(`Alchemy API error: ${response.statusText}`); + } + + const data = await response.json(); + return data.result; + } + + async getNFTOwnershipData( + contractAddress: string, + options: { + tokenId?: string; + owner?: string; + includeMetadata?: boolean; + } = {} + ) { + try { + const params = { + contractAddress, + tokenId: options.tokenId, + owner: options.owner, + withMetadata: options.includeMetadata, + }; + + const ownersData = await this.fetchAlchemyAPI<{ + owners: Array<{ + ownerAddress: string; + tokenBalances: Array<{ tokenId: string }>; + }>; + totalSupply?: number; + metadata?: any; + }>("alchemy_getOwnersForContract", params); + + return { + owners: ownersData.owners.map((owner) => ({ + address: owner.ownerAddress, + tokenIds: owner.tokenBalances.map((tb) => tb.tokenId), + balance: owner.tokenBalances.length, + })), + metadata: ownersData.metadata, + }; + } catch (error) { + console.error("Error fetching NFT ownership data:", error); + return { owners: [] }; + } + } + + async getTokenMetadata(contractAddress: string, tokenId: string) { + try { + return await this.fetchAlchemyAPI("alchemy_getNFTMetadata", { + contractAddress, + tokenId, + }); + } catch (error) { + console.error("Error fetching token metadata:", error); + return {}; + } + } + + async getNFTTransferHistory( + contractAddress: string, + options: { + tokenId?: string; + fromBlock?: number; + toBlock?: number; + limit?: number; + } = {} + ) { + try { + const transfers = await this.fetchAlchemyAPI( + "alchemy_getNFTSales", + { + contractAddress, + tokenId: options.tokenId, + fromBlock: options.fromBlock, + toBlock: options.toBlock, + limit: options.limit || 100, + } + ); + + return transfers.map((sale: any) => ({ + from: sale.sellerAddress, + to: sale.buyerAddress, + tokenId: sale.tokenId, + blockNumber: sale.blockNumber, + timestamp: new Date(sale.timestamp).toISOString(), + transactionHash: sale.transactionHash, + price: sale.price, + marketplace: sale.marketplace, + })); + } catch (error) { + console.error("Error fetching NFT transfer history:", error); + return []; + } + } + + async getCollectionInsights(contractAddress: string) { + try { + const [owners, attributes] = await Promise.all([ + this.fetchAlchemyAPI<{ + owners: Array<{ + ownerAddress: string; + tokenBalances: Array; + }>; + totalSupply: number; + }>("alchemy_getOwnersForContract", { contractAddress }), + this.fetchAlchemyAPI<{ + attributeDistribution: Record< + string, + { + count: number; + percentage: number; + } + >; + }>("alchemy_getNFTAttributeSummary", { contractAddress }), + ]); + + const sortedOwners = owners.owners + .map((owner) => ({ + address: owner.ownerAddress, + tokenCount: owner.tokenBalances.length, + percentage: + (owner.tokenBalances.length / owners.totalSupply) * 100, + })) + .sort((a, b) => b.tokenCount - a.tokenCount) + .slice(0, 10); + + return { + totalUniqueOwners: owners.owners.length, + holdingDistribution: [ + { + ownerType: "individual", + percentage: 100, + count: owners.owners.length, + }, + ], + topHolders: sortedOwners, + tokenTypeBreakdown: Object.entries( + attributes.attributeDistribution || {} + ).map(([type, data]) => ({ + type, + count: data.count, + percentage: data.percentage, + })), + }; + } catch (error) { + console.error("Error fetching collection insights:", error); + return { + totalUniqueOwners: 0, + holdingDistribution: [], + topHolders: [], + tokenTypeBreakdown: [], + }; + } + } + + async getNFTMarketAnalytics( + contractAddress: string, + options: { timeframe?: "7d" | "30d" | "90d" } = {} + ) { + try { + const [floorPrice, sales] = await Promise.all([ + this.fetchAlchemyAPI<{ + openSea?: { floorPrice: number }; + }>("alchemy_getFloorPrice", { contractAddress }), + this.fetchAlchemyAPI>( + "alchemy_getNFTSales", + { + contractAddress, + timeframe: options.timeframe || "30d", + } + ), + ]); + + const prices = sales.map((sale) => sale.price); + + return { + floorPrice: floorPrice.openSea?.floorPrice || 0, + volumeTraded: sales.reduce((sum, sale) => sum + sale.price, 0), + averageSalePrice: prices.length + ? prices.reduce((sum, price) => sum + price, 0) / + prices.length + : 0, + salesCount: sales.length, + priceRange: { + min: prices.length ? Math.min(...prices) : 0, + max: prices.length ? Math.max(...prices) : 0, + median: prices.length + ? prices.sort((a, b) => a - b)[ + Math.floor(prices.length / 2) + ] + : 0, + }, + marketTrends: [], // Alchemy doesn't provide direct market trends + }; + } catch (error) { + console.error("Error fetching NFT market analytics:", error); + return { + floorPrice: 0, + volumeTraded: 0, + averageSalePrice: 0, + salesCount: 0, + priceRange: { min: 0, max: 0, median: 0 }, + marketTrends: [], + }; + } + } + + async getTokenRarityScore(contractAddress: string, tokenId: string) { + try { + const [metadata, attributeSummary] = await Promise.all([ + this.fetchAlchemyAPI<{ + attributes?: Array<{ + trait_type: string; + value: string; + }>; + }>("alchemy_getNFTMetadata", { + contractAddress, + tokenId, + }), + this.fetchAlchemyAPI<{ + attributeDistribution: Record< + string, + { + percentage: number; + } + >; + }>("alchemy_getNFTAttributeSummary", { contractAddress }), + ]); + + const traitBreakdown = (metadata.attributes || []).map((attr) => { + const attrSummary = + attributeSummary.attributeDistribution?.[attr.trait_type]; + const rarityScore = attrSummary + ? (1 / attrSummary.percentage) * 100 + : 0; + + return { + trait: attr.trait_type, + value: attr.value, + rarityScore, + }; + }); + + const rarityScore = traitBreakdown.reduce( + (sum, trait) => sum + trait.rarityScore, + 0 + ); + + return { + rarityScore, + rarityRank: 0, // Alchemy doesn't provide direct rarity ranking + traitBreakdown, + }; + } catch (error) { + console.error("Error fetching token rarity score:", error); + return { + rarityScore: 0, + rarityRank: 0, + traitBreakdown: [], + }; + } + } + + async searchCollections(query: string, options: { limit?: number } = {}) { + try { + const contracts = await this.fetchAlchemyAPI< + Array<{ + name: string; + address: string; + description: string; + }> + >("alchemy_searchContractMetadata", { + query, + limit: options.limit || 10, + }); + + return contracts.map((contract) => ({ + collection: { + name: contract.name, + address: contract.address, + description: contract.description, + }, + })); + } catch (error) { + console.error("Error searching Alchemy collections:", error); + return []; + } + } +} + +// Optional: Create a singleton instance +export const alchemyService = new AlchemyService(process.env.ALCHEMY_API_KEY); 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 00000000000..097dddb7599 --- /dev/null +++ b/packages/plugin-nft-collections/src/services/cache-manager.ts @@ -0,0 +1,86 @@ +import { LRUCache } from "lru-cache"; + +interface CacheOptions { + ttl?: number; + maxSize?: number; +} + +interface CacheEntry { + data: T; + expiresAt: number; + priority: number; +} + +export class MemoryCacheManager { + private cache: LRUCache>; + private readonly DEFAULT_TTL = 3600000; // 1 hour + private readonly COLLECTION_TTL = 300000; // 5 minutes + private readonly MARKET_TTL = 60000; // 1 minute + + constructor(options: CacheOptions = {}) { + this.cache = new LRUCache({ + max: options.maxSize || 1000, + ttl: options.ttl || this.DEFAULT_TTL, + updateAgeOnGet: true, + updateAgeOnHas: true, + }); + } + + private getExpirationTime(key: string): number { + if (key.startsWith("collection:")) return this.COLLECTION_TTL; + if (key.startsWith("market:")) return this.MARKET_TTL; + return this.DEFAULT_TTL; + } + + async get(key: string): Promise { + const entry = this.cache.get(key) as CacheEntry; + if (!entry) return null; + + if (Date.now() > entry.expiresAt) { + this.cache.delete(key); + return null; + } + + return entry.data; + } + + async set(key: string, value: T, priority: number = 0): Promise { + const ttl = this.getExpirationTime(key); + const entry: CacheEntry = { + data: value, + expiresAt: Date.now() + ttl, + priority, + }; + + this.cache.set(key, entry); + } + + async delete(key: string): Promise { + this.cache.delete(key); + } + + async clear(): Promise { + this.cache.clear(); + } + + async has(key: string): Promise { + const entry = this.cache.get(key) as CacheEntry; + if (!entry) return false; + + if (Date.now() > entry.expiresAt) { + this.cache.delete(key); + return false; + } + + return true; + } + + async prune(): Promise { + const now = Date.now(); + for (const [key, entry] of this.cache.entries()) { + if (now > entry.expiresAt) { + this.cache.delete(key); + } + } + } +} diff --git a/packages/plugin-nft-collections/src/services/coingecko.ts b/packages/plugin-nft-collections/src/services/coingecko.ts new file mode 100644 index 00000000000..a742c628b39 --- /dev/null +++ b/packages/plugin-nft-collections/src/services/coingecko.ts @@ -0,0 +1,215 @@ +import { z } from "zod"; + +export const CoinGeckoNFTDataSchema = z.object({ + id: z.string(), + contract_address: z.string(), + name: z.string(), + asset_platform_id: z.string(), + symbol: z.string(), + + // Market Metrics + market_cap_usd: z.number().optional(), + volume_24h_usd: z.number().optional(), + floor_price_usd: z.number().optional(), + floor_price_eth: z.number().optional(), + + // Supply Metrics + total_supply: z.number().optional(), + max_supply: z.number().optional(), + circulating_supply: z.number().optional(), + + // Price Metrics + current_price_usd: z.number().optional(), + current_price_eth: z.number().optional(), + price_change_percentage_24h: z.number().optional(), + + // Ownership Metrics + number_of_unique_addresses: z.number().optional(), + number_of_unique_currencies: z.number().optional(), + + // Collection Specific + description: z.string().optional(), + homepage: z.string().optional(), + blockchain: z.string().optional(), + + // Historical Performance + all_time_high: z + .object({ + price_usd: z.number().optional(), + timestamp: z.string().optional(), + }) + .optional(), + + // Rarity and Trading + total_volume: z.number().optional(), + total_trades: z.number().optional(), + average_trade_price: z.number().optional(), + + // Social Metrics + twitter_followers: z.number().optional(), + discord_members: z.number().optional(), +}); + +export type CoinGeckoNFTData = z.infer; + +export class CoinGeckoService { + private baseUrl = "https://api.coingecko.com/api/v3"; + private apiKey?: string; + private rateLimitDelay = 1000; // 1 second between requests + + constructor(apiKey?: string) { + this.apiKey = apiKey; + } + + private async delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + private async fetch( + endpoint: string, + params: Record = {}, + options: { + retries?: number; + backoff?: number; + } = { retries: 3, backoff: 1000 } + ): Promise { + await this.delay(this.rateLimitDelay); + + if (this.apiKey) { + params.x_cg_pro_api_key = this.apiKey; + } + + const queryString = new URLSearchParams(params).toString(); + const url = `${this.baseUrl}${endpoint}${queryString ? `?${queryString}` : ""}`; + + for (let attempt = 1; attempt <= (options.retries || 3); attempt++) { + try { + const response = await fetch(url, { + headers: { + accept: "application/json", + }, + }); + + if (!response.ok) { + throw new Error( + `CoinGecko API error: ${response.statusText}` + ); + } + + return await response.json(); + } catch (error) { + if (attempt === (options.retries || 3)) { + console.error( + `Failed to fetch after ${attempt} attempts:`, + error + ); + throw error; + } + await this.delay(options.backoff || 1000 * attempt); + } + } + + throw new Error("Unexpected fetch failure"); + } + + async getNFTMarketData( + contractAddress: string, + options: { + includeDescription?: boolean; + includeHistoricalData?: boolean; + } = {} + ): Promise { + try { + // First, list NFTs to find the specific collection + const collections = await this.fetch( + "/nfts/list", + { + asset_platform: "ethereum", + } + ); + + const nft = collections.find( + (n) => + n.contract_address.toLowerCase() === + contractAddress.toLowerCase() + ); + + if (!nft) return null; + + // Fetch detailed data + const details = await this.fetch( + `/nfts/${nft.id}` + ); + + // Optionally fetch additional details + if (options.includeDescription) { + const additionalInfo = await this.fetch( + `/nfts/${nft.id}/info` + ); + details.description = additionalInfo.description; + details.homepage = additionalInfo.homepage; + } + + // Optionally fetch historical data + if (options.includeHistoricalData) { + const historicalData = await this.fetch( + `/nfts/${nft.id}/historical` + ); + details.all_time_high = { + price_usd: historicalData.all_time_high?.price, + timestamp: historicalData.all_time_high?.timestamp, + }; + } + + return details; + } catch (error) { + console.error("Error fetching CoinGecko NFT data:", error); + return null; + } + } + + async getCollectionTrends( + options: { + timeframe?: "24h" | "7d" | "30d"; + sortBy?: "market_cap" | "volume" | "sales"; + } = {} + ): Promise { + const params = { + order: + options.sortBy === "market_cap" + ? "market_cap_usd_desc" + : options.sortBy === "volume" + ? "volume_24h_usd_desc" + : "market_cap_usd_desc", + per_page: "50", + page: "1", + timeframe: options.timeframe || "24h", + }; + + const trendingCollections = await this.fetch( + "/nfts/list", + params + ); + return trendingCollections; + } + + async getGlobalNFTStats(): Promise<{ + total_market_cap_usd: number; + total_volume_24h_usd: number; + market_cap_change_24h: number; + volume_change_24h: number; + number_of_unique_currencies: number; + number_of_unique_addresses: number; + top_collections: CoinGeckoNFTData[]; + }> { + const data = await this.fetch("/global/nft"); + + // Fetch top collections to enrich global stats + const topCollections = await this.getCollectionTrends(); + + return { + ...data.data, + top_collections: topCollections, + }; + } +} diff --git a/packages/plugin-nft-collections/src/services/market-intelligence.ts b/packages/plugin-nft-collections/src/services/market-intelligence.ts new file mode 100644 index 00000000000..beeb74057c7 --- /dev/null +++ b/packages/plugin-nft-collections/src/services/market-intelligence.ts @@ -0,0 +1,213 @@ +import { MemoryCacheManager } from "./cache-manager"; +import { RateLimiter } from "./rate-limiter"; +import { MarketData, MarketDataSchema } from "../utils/validation"; +import axios from "axios"; +import { z } from "zod"; + +// Enhanced Market Data Schema +const ExtendedMarketDataSchema = MarketDataSchema.extend({ + liquidityScore: z.number().min(0).max(100).optional(), + volatility: z.number().min(0).optional(), + tradeVolume: z + .object({ + total: z.number().min(0), + buy: z.number().min(0).optional(), + sell: z.number().min(0).optional(), + }) + .optional(), + priceHistory: z + .array( + z.object({ + timestamp: z.string().datetime(), + price: z.number().min(0), + }) + ) + .optional(), +}); + +interface MarketIntelligenceConfig { + cacheManager?: MemoryCacheManager; + rateLimiter?: RateLimiter; + openSeaApiKey?: string; + reservoirApiKey?: string; +} + +export class MarketIntelligenceService { + private config: MarketIntelligenceConfig; + private cacheManager: MemoryCacheManager; + private rateLimiter?: RateLimiter; + private static CACHE_TTL = 30 * 60; // 30 minutes cache + + constructor(config: MarketIntelligenceConfig = {}) { + this.config = config; + this.cacheManager = + config.cacheManager || + new MemoryCacheManager({ + ttl: MarketIntelligenceService.CACHE_TTL, + }); + this.rateLimiter = config.rateLimiter; + } + + async getMarketIntelligence(address: string): Promise { + const cacheKey = `market_intelligence:${address}`; + + // Check cache first + const cachedData = this.cacheManager.get(cacheKey); + if (cachedData) return cachedData; + + try { + // Apply rate limiting if configured + if (this.rateLimiter) { + await this.rateLimiter.consume(address); + } + + // Fetch market data from multiple sources + const [openSeaData, reservoirData] = await Promise.allSettled([ + this.fetchOpenSeaMarketData(address), + this.fetchReservoirMarketData(address), + ]); + + const marketData: MarketData = { + lastUpdate: new Date().toISOString(), + floorPrice: this.extractMetric(openSeaData, "floorPrice") || 0, + volume24h: this.extractMetric(openSeaData, "volume24h") || 0, + volume7d: this.extractMetric(reservoirData, "volume7d") || 0, + marketCap: this.calculateMarketCap( + this.extractMetric(openSeaData, "floorPrice") || 0, + this.extractMetric(openSeaData, "totalSupply") || 0 + ), + holders: + this.extractMetric(reservoirData, "uniqueHolders") || 0, + bestOffer: this.extractMetric(openSeaData, "bestOffer") || 0, + }; + + // Validate and cache market data + const validatedData = ExtendedMarketDataSchema.parse({ + ...marketData, + liquidityScore: this.calculateLiquidityScore(marketData), + volatility: this.calculateVolatility(address), + }); + + this.cacheManager.set( + cacheKey, + validatedData, + MarketIntelligenceService.CACHE_TTL + ); + + return validatedData; + } catch (error) { + console.error( + `Market intelligence fetch failed for ${address}:`, + error + ); + throw new Error( + `Failed to retrieve market intelligence: ${error.message}` + ); + } + } + + private async fetchOpenSeaMarketData(address: string) { + if (!this.config.openSeaApiKey) { + console.warn( + "OpenSea API key not provided for market intelligence" + ); + return undefined; + } + + try { + const response = await axios.get( + `https://api.opensea.io/api/v2/collections/${address}`, + { + headers: { + "X-API-KEY": this.config.openSeaApiKey, + }, + } + ); + + return { + floorPrice: response.data.floor_price, + volume24h: response.data.total_volume, + bestOffer: response.data.best_offer, + totalSupply: response.data.total_supply, + }; + } catch (error) { + console.error("OpenSea market data fetch failed", error); + return undefined; + } + } + + private async fetchReservoirMarketData(address: string) { + if (!this.config.reservoirApiKey) { + console.warn( + "Reservoir API key not provided for market intelligence" + ); + return undefined; + } + + try { + const response = await axios.get( + `https://api.reservoir.tools/collections/v6?id=${address}`, + { + headers: { + "X-API-KEY": this.config.reservoirApiKey, + }, + } + ); + + return { + volume7d: response.data.volume7d, + uniqueHolders: response.data.uniqueHolders, + }; + } catch (error) { + console.error("Reservoir market data fetch failed", error); + return undefined; + } + } + + // Utility method to extract specific metrics safely + private extractMetric( + result: PromiseSettledResult, + key: string + ): number | undefined { + if (result.status === "fulfilled" && result.value) { + return result.value[key]; + } + return undefined; + } + + // Placeholder calculations with room for sophistication + private calculateMarketCap( + floorPrice: number, + totalSupply: number + ): number { + return floorPrice * totalSupply; + } + + private calculateLiquidityScore(marketData: MarketData): number { + // Basic liquidity score calculation + const { volume24h, holders } = marketData; + return Math.min((volume24h / (holders || 1)) * 10, 100); + } + + private calculateVolatility(address: string): number { + // Placeholder for volatility calculation + // Would typically involve analyzing price history + return 0; + } + + // Additional analytical methods + async getPriceHistory(address: string, days: number = 30): Promise { + // Placeholder for price history retrieval + return []; + } + + async getTradingInsights(address: string): Promise { + // Placeholder for advanced trading insights + return {}; + } +} + +export const marketIntelligenceService = new MarketIntelligenceService({ + openSeaApiKey: process.env.OPENSEA_API_KEY, + reservoirApiKey: process.env.RESERVOIR_API_KEY, +}); diff --git a/packages/plugin-nft-collections/src/services/opensea.ts b/packages/plugin-nft-collections/src/services/opensea.ts new file mode 100644 index 00000000000..8bc31cd49ca --- /dev/null +++ b/packages/plugin-nft-collections/src/services/opensea.ts @@ -0,0 +1,372 @@ +import { z } from "zod"; + +export const OpenSeaNFTDataSchema = z.object({ + collection: z.object({ + name: z.string(), + description: z.string().optional(), + slug: z.string(), + stats: z + .object({ + floor_price: z.number().optional(), + total_volume: z.number().optional(), + total_sales: z.number().optional(), + average_price: z.number().optional(), + num_owners: z.number().optional(), + market_cap: z.number().optional(), + total_supply: z.number().optional(), + one_day_volume: z.number().optional(), + seven_day_volume: z.number().optional(), + thirty_day_volume: z.number().optional(), + percent_change_volume_7d: z.number().optional(), + total_listed_items: z.number().optional(), + total_unsold_items: z.number().optional(), + }) + .optional(), + primary_asset_contracts: z + .array( + z.object({ + address: z.string(), + schema_name: z.string(), + symbol: z.string().optional(), + description: z.string().optional(), + }) + ) + .optional(), + royalty_fees: z + .object({ + seller_fee_basis_points: z.number().optional(), + opensea_fee_basis_points: z.number().optional(), + total_fee_basis_points: z.number().optional(), + }) + .optional(), + social_links: z + .object({ + twitter: z.string().optional(), + discord: z.string().optional(), + website: z.string().optional(), + instagram: z.string().optional(), + medium: z.string().optional(), + }) + .optional(), + created_date: z.string().optional(), + is_verified: z.boolean().optional(), + is_disabled: z.boolean().optional(), + is_nsfw: z.boolean().optional(), + }), + traits: z + .array( + z.object({ + trait_type: z.string(), + value: z.string(), + count: z.number(), + percentage: z.number().optional(), + rarity_score: z.number().optional(), + }) + ) + .optional(), + rarity: z + .object({ + total_supply: z.number().optional(), + unique_owners_count: z.number().optional(), + rarest_trait_percentage: z.number().optional(), + most_common_trait: z + .object({ + type: z.string().optional(), + value: z.string().optional(), + percentage: z.number().optional(), + }) + .optional(), + }) + .optional(), + performance_metrics: z + .object({ + price_volatility: z.number().optional(), + liquidity_score: z.number().optional(), + trading_frequency: z.number().optional(), + average_hold_time: z.number().optional(), + }) + .optional(), +}); + +export type OpenSeaNFTData = z.infer; + +export class OpenSeaService { + private baseUrl = "https://api.opensea.io/api/v2/"; + private apiKey?: string; + + constructor(apiKey?: string) { + this.apiKey = apiKey; + } + + private async fetch( + endpoint: string, + params: Record = {}, + method: "GET" | "POST" = "GET" + ): Promise { + const headers: Record = { + accept: "application/json", + "x-api-key": this.apiKey || "", + }; + + const queryString = new URLSearchParams(params).toString(); + const url = `${this.baseUrl}${endpoint}${queryString ? `?${queryString}` : ""}`; + + try { + const response = await fetch(url, { + method, + headers, + }); + + if (!response.ok) { + throw new Error(`OpenSea API error: ${response.statusText}`); + } + + return await response.json(); + } catch (error) { + console.error("OpenSea API fetch error:", error); + throw error; + } + } + + async getCollectionDetails( + slug: string, + options: { + includeTraits?: boolean; + includeRarity?: boolean; + includePerformanceMetrics?: boolean; + } = {} + ): Promise { + try { + // Fetch collection details + const collectionData = await this.fetch( + `collections/${slug}` + ); + + // Optionally fetch traits + if (options.includeTraits) { + const traitsData = await this.getCollectionTraits(slug); + (collectionData as any).traits = traitsData; + } + + // Optionally fetch rarity data + if (options.includeRarity) { + const rarityData = await this.getCollectionRarity(slug); + (collectionData as any).rarity = rarityData; + } + + // Optionally fetch performance metrics + if (options.includePerformanceMetrics) { + const performanceData = + await this.getCollectionPerformanceMetrics(slug); + (collectionData as any).performance_metrics = performanceData; + } + + return collectionData; + } catch (error) { + console.error("Error fetching OpenSea collection details:", error); + return null; + } + } + + async getCollectionTraits(slug: string): Promise { + try { + const data = await this.fetch<{ traits: any[] }>( + `collections/${slug}/traits` + ); + return data.traits.map((trait) => ({ + ...trait, + rarity_score: this.calculateTraitRarityScore(trait), + })); + } catch (error) { + console.error("Error fetching OpenSea collection traits:", error); + return []; + } + } + + private calculateTraitRarityScore(trait: any): number { + // Simple rarity score calculation + // Lower percentage means higher rarity + return trait.count && trait.percentage + ? (1 / trait.percentage) * 100 + : 0; + } + + async getCollectionRarity(slug: string): Promise<{ + total_supply?: number; + unique_owners_count?: number; + rarest_trait_percentage?: number; + most_common_trait?: { + type?: string; + value?: string; + percentage?: number; + }; + }> { + try { + const data = await this.fetch(`collections/${slug}/rarity`); + + // Find most common trait + const traits = await this.getCollectionTraits(slug); + const mostCommonTrait = traits.reduce((max, trait) => + (max.percentage || 0) < (trait.percentage || 0) ? trait : max + ); + + return { + total_supply: data.total_supply, + unique_owners_count: data.unique_owners_count, + rarest_trait_percentage: Math.min( + ...traits.map((t) => t.percentage || 0) + ), + most_common_trait: { + type: mostCommonTrait.trait_type, + value: mostCommonTrait.value, + percentage: mostCommonTrait.percentage, + }, + }; + } catch (error) { + console.error("Error fetching OpenSea collection rarity:", error); + return {}; + } + } + + async getCollectionPerformanceMetrics(slug: string): Promise<{ + price_volatility?: number; + liquidity_score?: number; + trading_frequency?: number; + average_hold_time?: number; + }> { + try { + // This would typically require multiple API calls or advanced analytics + const salesHistory = await this.getCollectionSalesHistory(slug); + + // Calculate performance metrics + const prices = salesHistory.map((sale) => sale.price); + const holdTimes = this.calculateHoldTimes(salesHistory); + + return { + price_volatility: this.calculatePriceVolatility(prices), + liquidity_score: this.calculateLiquidityScore(salesHistory), + trading_frequency: salesHistory.length, + average_hold_time: this.calculateAverageHoldTime(holdTimes), + }; + } catch (error) { + console.error("Error calculating performance metrics:", error); + return {}; + } + } + + private calculatePriceVolatility(prices: number[]): number { + if (prices.length < 2) return 0; + + const mean = prices.reduce((a, b) => a + b, 0) / prices.length; + const variance = + prices.reduce((sq, n) => sq + Math.pow(n - mean, 2), 0) / + prices.length; + + return Math.sqrt(variance) / mean; // Coefficient of variation + } + + private calculateLiquidityScore(sales: any[]): number { + // Simple liquidity score based on recent sales volume and frequency + const recentSales = sales.filter( + (sale) => + new Date(sale.timestamp) > + new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) + ); + + return recentSales.length / 30; // Sales per day + } + + private calculateHoldTimes(sales: any[]): number[] { + // Sort sales by timestamp + const sortedSales = sales.sort( + (a, b) => + new Date(a.timestamp).getTime() - + new Date(b.timestamp).getTime() + ); + + // Calculate hold times between sales + return sortedSales.slice(1).map( + (sale, index) => + (new Date(sale.timestamp).getTime() - + new Date(sortedSales[index].timestamp).getTime()) / + (24 * 60 * 60 * 1000) // Convert to days + ); + } + + private calculateAverageHoldTime(holdTimes: number[]): number { + return holdTimes.length > 0 + ? holdTimes.reduce((a, b) => a + b, 0) / holdTimes.length + : 0; + } + + async getCollectionSalesHistory( + slug: string, + options: { + limit?: number; + timeframe?: "7d" | "30d" | "90d"; + } = {} + ): Promise { + try { + const data = await this.fetch<{ sales: any[] }>( + `collections/${slug}/sales`, + { + limit: options.limit || 100, + timeframe: options.timeframe || "30d", + } + ); + return data.sales; + } catch (error) { + console.error( + "Error fetching OpenSea collection sales history:", + error + ); + return []; + } + } + + async getCollectionListings( + slug: string, + options: { + limit?: number; + sortBy?: "price_asc" | "price_desc"; + } = {} + ): Promise { + try { + const data = await this.fetch<{ listings: any[] }>( + `collections/${slug}/listings`, + { + limit: options.limit || 50, + sort: options.sortBy || "price_asc", + } + ); + return data.listings; + } catch (error) { + console.error("Error fetching OpenSea collection listings:", error); + return []; + } + } + + async searchCollections( + query: string, + options: { + limit?: number; + } = {} + ): Promise { + try { + const data = await this.fetch<{ collections: OpenSeaNFTData[] }>( + "collections/search", + { + query, + limit: options.limit || 10, + } + ); + return data.collections; + } catch (error) { + console.error("Error searching OpenSea collections:", error); + return []; + } + } +} + +// Optional: Create a singleton instance +export const openSeaService = new OpenSeaService(process.env.OPENSEA_API_KEY); 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 00000000000..b3a7cb658d6 --- /dev/null +++ b/packages/plugin-nft-collections/src/services/rate-limiter.ts @@ -0,0 +1,98 @@ +import { RateLimiterMemory } from "rate-limiter-flexible"; + +interface RateLimiterConfig { + maxRequests?: number; + windowMs?: number; + maxRetries?: number; + retryDelay?: number; +} + +export class RateLimiter { + private limiter: RateLimiterMemory; + private maxRetries: number; + private retryDelay: number; + + constructor(config: RateLimiterConfig = {}) { + this.limiter = new RateLimiterMemory({ + points: config.maxRequests || 100, + duration: (config.windowMs || 60000) / 1000, // Convert ms to seconds + }); + this.maxRetries = config.maxRetries || 3; + this.retryDelay = config.retryDelay || 1000; + } + + async consume(key: string, points: number = 1): Promise { + try { + await this.limiter.consume(key, points); + } catch (error: any) { + if (error.remainingPoints === 0) { + const retryAfter = Math.ceil(error.msBeforeNext / 1000); + throw new Error( + `Rate limit exceeded. Retry after ${retryAfter} seconds` + ); + } + throw error; + } + } + + async executeWithRetry( + key: string, + operation: () => Promise, + points: number = 1 + ): Promise { + let lastError: Error | null = null; + let retries = 0; + + while (retries <= this.maxRetries) { + try { + await this.consume(key, points); + return await operation(); + } catch (error: any) { + lastError = error; + retries++; + + if (error.message?.includes("Rate limit exceeded")) { + const retryAfter = parseInt( + error.message.match(/\d+/)?.[0] || "1", + 10 + ); + await new Promise((resolve) => + setTimeout(resolve, retryAfter * 1000) + ); + } else if (retries <= this.maxRetries) { + await new Promise((resolve) => + setTimeout(resolve, this.retryDelay * retries) + ); + } else { + break; + } + } + } + + throw new Error( + `Operation failed after ${retries} retries. Last error: ${lastError?.message}` + ); + } + + async cleanup(): Promise { + // Cleanup any resources if needed + } + + async getRemainingPoints(key: string): Promise { + const res = await this.limiter.get(key); + return res?.remainingPoints ?? 0; + } + + async reset(key: string): Promise { + await this.limiter.delete(key); + } + + async isRateLimited(key: string): Promise { + try { + await this.limiter.get(key); + return false; + } catch { + return true; + } + } +} diff --git a/packages/plugin-nft-collections/src/services/reservoir.ts b/packages/plugin-nft-collections/src/services/reservoir.ts new file mode 100644 index 00000000000..43f610f461b --- /dev/null +++ b/packages/plugin-nft-collections/src/services/reservoir.ts @@ -0,0 +1,681 @@ +import pRetry from "p-retry"; +import { PerformanceMonitor } from "../utils/performance"; +import { + ErrorHandler, + NFTErrorFactory, + ErrorType, + ErrorCode, + NFTError, +} from "../utils/error-handler"; +import { MemoryCacheManager } from "./cache-manager"; +import { RateLimiter } from "./rate-limiter"; +import { MarketStats, NFTCollection } from "../types"; +import { IAgentRuntime } from "@elizaos/core"; +import { z } from "zod"; + +// Enhanced error codes specific to Reservoir service +export enum ReservoirErrorCode { + RATE_LIMIT = "RESERVOIR_RATE_LIMIT", + API_KEY_INVALID = "RESERVOIR_API_KEY_INVALID", + INSUFFICIENT_FUNDS = "RESERVOIR_INSUFFICIENT_FUNDS", + COLLECTION_NOT_FOUND = "RESERVOIR_COLLECTION_NOT_FOUND", +} + +// Comprehensive configuration interface +interface ReservoirServiceConfig { + cacheManager?: MemoryCacheManager; + rateLimiter?: RateLimiter; + maxConcurrent?: number; + maxRetries?: number; + batchSize?: number; + apiKey?: string; + baseUrl?: string; + timeout?: number; + retryStrategy?: { + maxRetries?: number; + baseDelay?: number; + jitter?: boolean; + }; + cacheConfig?: { + enabled?: boolean; + defaultTTL?: number; + }; + telemetry?: { + enabled?: boolean; + serviceName?: string; + }; +} + +// Validation schema for configuration +const ReservoirConfigSchema = z.object({ + apiKey: z.string().optional(), + baseUrl: z.string().url().optional().default("https://api.reservoir.tools"), + timeout: z.number().positive().optional().default(10000), + maxRetries: z.number().min(0).optional().default(3), +}); + +export class ReservoirService { + private cacheManager?: MemoryCacheManager; + private rateLimiter?: RateLimiter; + private maxRetries: number; + private batchSize: number; + private performanceMonitor: PerformanceMonitor; + private errorHandler: ErrorHandler; + private config: Required; + + constructor(config: ReservoirServiceConfig = {}) { + // Validate and merge configuration + const validatedConfig = ReservoirConfigSchema.parse(config); + + this.config = { + cacheManager: config.cacheManager, + rateLimiter: config.rateLimiter, + maxConcurrent: config.maxConcurrent || 5, + maxRetries: validatedConfig.maxRetries, + batchSize: config.batchSize || 20, + apiKey: validatedConfig.apiKey || process.env.RESERVOIR_API_KEY, + baseUrl: validatedConfig.baseUrl, + timeout: validatedConfig.timeout, + retryStrategy: { + maxRetries: 3, + baseDelay: 1000, + jitter: true, + ...config.retryStrategy, + }, + cacheConfig: { + enabled: true, + defaultTTL: 300, + ...config.cacheConfig, + }, + telemetry: { + enabled: true, + serviceName: "ikigai-nft-reservoir", + ...config.telemetry, + }, + }; + + this.cacheManager = this.config.cacheManager; + this.rateLimiter = this.config.rateLimiter; + this.maxRetries = this.config.maxRetries; + this.batchSize = this.config.batchSize; + this.performanceMonitor = PerformanceMonitor.getInstance(); + this.errorHandler = ErrorHandler.getInstance(); + + // Setup telemetry and monitoring + this.setupTelemetry(); + } + + // Advanced caching with context-aware invalidation + private async cachedRequest( + endpoint: string, + params: Record, + runtime: IAgentRuntime, + cacheOptions?: { + ttl?: number; + context?: string; + } + ): Promise { + if (!this.config.cacheConfig.enabled) { + return this.makeRequest(endpoint, params, 0, runtime); + } + + const cacheKey = this.generateCacheKey(endpoint, params); + + const cachedResponse = await this.cacheManager?.get(cacheKey); + if (cachedResponse) { + if (this.isCacheFresh(cachedResponse, cacheOptions?.ttl)) { + return cachedResponse; + } + } + + const freshData = await this.makeRequest( + endpoint, + params, + 0, + runtime + ); + + // Only pass ttl to set method + await this.cacheManager?.set( + cacheKey, + freshData, + cacheOptions?.ttl ?? this.config.cacheConfig.defaultTTL + ); + + return freshData; + } + + // Generate deterministic cache key + private generateCacheKey( + endpoint: string, + params: Record + ): string { + const sortedParams = Object.keys(params) + .sort() + .map((key) => `${key}:${params[key]}`) + .join("|"); + return `reservoir:${endpoint}:${sortedParams}`; + } + + // Check cache freshness + private isCacheFresh(cachedResponse: any, ttl?: number): boolean { + const MAX_CACHE_AGE = ttl || this.config.cacheConfig.defaultTTL * 1000; + return Date.now() - cachedResponse.timestamp < MAX_CACHE_AGE; + } + + // Enhanced error handling method + private handleReservoirError( + error: Error, + context: Record + ): NFTError { + if (error.message.includes("rate limit")) { + return NFTErrorFactory.create( + ErrorType.RATE_LIMIT, + ErrorCode.RATE_LIMIT_EXCEEDED, + "Reservoir API rate limit exceeded", + { + details: { + ...context, + retryAfter: this.extractRetryAfter(error), + }, + retryable: true, + severity: "HIGH", + } + ); + } + + if (error.message.includes("API key")) { + return NFTErrorFactory.create( + ErrorType.AUTHENTICATION, + ErrorCode.API_KEY_INVALID, + "Invalid Reservoir API key", + { + details: context, + retryable: false, + severity: "CRITICAL", + } + ); + } + + // Fallback to generic error handling + return NFTErrorFactory.fromError(error); + } + + // Extract retry-after timestamp + private extractRetryAfter(error: Error): number { + // In a real implementation, extract from headers or use exponential backoff + return Date.now() + 60000; // Default 1 minute + } + + // Intelligent retry mechanism + private async retryRequest( + requestFn: () => Promise, + options: { + maxRetries?: number; + baseDelay?: number; + jitter?: boolean; + } = {} + ): Promise { + const { + maxRetries = this.config.retryStrategy.maxRetries, + baseDelay = this.config.retryStrategy.baseDelay, + jitter = this.config.retryStrategy.jitter, + } = options; + + let lastError: Error | null = null; + + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + return await requestFn(); + } catch (error) { + lastError = error; + + // Exponential backoff with optional jitter + const delay = jitter + ? baseDelay * Math.pow(2, attempt) * (1 + Math.random()) + : baseDelay * Math.pow(2, attempt); + + // Log retry attempt + this.performanceMonitor.recordMetric({ + operation: "retryRequest", + duration: delay, + success: false, + metadata: { + attempt, + error: error.message, + }, + }); + + // Optional: Circuit breaker for critical errors + if (this.isCircuitBreakerTripped(error)) { + break; + } + + await new Promise((resolve) => setTimeout(resolve, delay)); + } + } + + // Final error handling + throw lastError || new Error("Max retries exceeded"); + } + + // Circuit breaker logic + private isCircuitBreakerTripped(error: Error): boolean { + const criticalErrors = ["API_KEY_INVALID", "UNAUTHORIZED", "FORBIDDEN"]; + return criticalErrors.some((code) => error.message.includes(code)); + } + + // Telemetry and monitoring setup + private setupTelemetry() { + if (!this.config.telemetry.enabled) return; + + // Track API usage metrics + const usageTracker = { + totalRequests: 0, + successfulRequests: 0, + failedRequests: 0, + endpoints: {} as Record, + }; + + // Performance monitoring hook + this.performanceMonitor.on("alert", (alert) => { + console.log(`Reservoir Service Alert: ${JSON.stringify(alert)}`); + // In a real implementation, send to monitoring service + }); + } + + // Existing makeRequest method with enhanced error handling + async makeRequest( + endpoint: string, + params: Record = {}, + priority: number = 0, + runtime: IAgentRuntime + ): Promise { + const endOperation = this.performanceMonitor.startOperation( + "makeRequest", + { endpoint, params, priority } + ); + + try { + // Check rate limit + if (this.rateLimiter) { + await this.rateLimiter.consume("reservoir", 1); + } + + const reservoirApiKey = + runtime.getSetting("RESERVOIR_API_KEY") || this.config.apiKey; + + // Make the request with retries + const result = await this.retryRequest(async () => { + const response = await fetch( + `${this.config.baseUrl}${endpoint}?${new URLSearchParams(params).toString()}`, + { + headers: { + "x-api-key": reservoirApiKey, + "Content-Type": "application/json", + }, + signal: AbortSignal.timeout(this.config.timeout), + } + ); + + if (!response.ok) { + throw new Error( + `Reservoir API error: ${response.status} ${await response.text()}` + ); + } + + return response.json(); + }); + + endOperation(); + return result; + } catch (error) { + this.performanceMonitor.recordMetric({ + operation: "makeRequest", + duration: 0, + success: false, + metadata: { + error: error.message, + endpoint, + params, + }, + }); + + const nftError = this.handleReservoirError(error, { + endpoint, + params, + }); + this.errorHandler.handleError(nftError); + throw error; + } + } + + // Modify getTopCollections to use the updated cachedRequest + async getTopCollections( + runtime: IAgentRuntime, + limit: number = 10 + ): Promise { + const endOperation = this.performanceMonitor.startOperation( + "getTopCollections", + { limit } + ); + + try { + const batchSize = 20; // Optimal batch size for Reservoir API + const batches = Math.ceil(limit / batchSize); + const promises = []; + + for (let i = 0; i < batches; i++) { + const offset = i * batchSize; + const currentLimit = Math.min(batchSize, limit - offset); + + promises.push( + this.cachedRequest( + "/collections/v6", + { + limit: currentLimit, + offset, + sortBy: "1DayVolume", + }, + runtime, + { + ttl: 3600, // Cache for 1 hour + context: "top_collections", + } + ) + ); + } + + const results = await Promise.all(promises); + const collections = results.flatMap((data) => data.collections); + + const mappedCollections = collections + .slice(0, limit) + .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, + holders: collection.ownerCount || 0, + lastUpdate: new Date().toISOString(), + })); + + endOperation(); + return mappedCollections; + } catch (error) { + // Error handling remains similar to previous implementation + throw error; + } + } + + // Add missing methods + async getFloorListings(options: { + collection: string; + limit: number; + sortBy: "price" | "rarity"; + }): Promise< + Array<{ + tokenId: string; + price: number; + seller: string; + marketplace: string; + }> + > { + const endOperation = this.performanceMonitor.startOperation( + "getFloorListings", + { options } + ); + + try { + if (!options.collection) { + throw new Error("Collection address is required"); + } + + const queryParams = { + collection: options.collection, + limit: options.limit?.toString() || "10", + sortBy: options.sortBy === "price" ? "floorAskPrice" : "rarity", + includeAttributes: + options.sortBy === "rarity" ? "true" : "false", + }; + + const response = await this.makeRequest<{ + asks: Array<{ + token: { + tokenId: string; + collection: { id: string }; + }; + price: { + amount: { + native: number; + usd?: number; + }; + }; + maker: string; + source: { name: string }; + }>; + }>("/collections/floor/v2", queryParams, 1, {} as IAgentRuntime); + + const floorListings = response.asks.map((ask) => ({ + tokenId: ask.token.tokenId, + price: ask.price.amount.native, + seller: ask.maker, + marketplace: ask.source?.name || "Reservoir", + })); + + endOperation(); + return floorListings; + } catch (error) { + this.performanceMonitor.recordMetric({ + operation: "getFloorListings", + duration: 0, + success: false, + metadata: { + error: error.message, + collection: options.collection, + }, + }); + + throw error; + } + } + + 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; + }> { + const endOperation = this.performanceMonitor.startOperation( + "createListing", + { options } + ); + + try { + if ( + !options.tokenId || + !options.collectionAddress || + !options.price + ) { + throw new Error("Missing required listing parameters"); + } + + const listingParams = { + maker: "", + token: `${options.collectionAddress}:${options.tokenId}`, + quantity: (options.quantity || 1).toString(), + price: options.price.toString(), + currency: options.currency || "ETH", + expirationTime: ( + options.expirationTime || + Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60 + ).toString(), + }; + + const response = await this.makeRequest<{ + listing: { + id: string; + status: string; + transactionHash?: string; + }; + }>("/listings/v5/create", listingParams, 1, {} as IAgentRuntime); + + const result = { + listingId: response.listing.id, + status: response.listing.status, + transactionHash: response.listing.transactionHash, + marketplaceUrl: `https://reservoir.market/collections/${options.collectionAddress}/tokens/${options.tokenId}`, + }; + + endOperation(); + return result; + } catch (error) { + this.performanceMonitor.recordMetric({ + operation: "createListing", + duration: 0, + success: false, + metadata: { error: error.message, options }, + }); + + throw error; + } + } + + async executeBuy(options: { + listings: Array<{ + tokenId: string; + price: number; + seller: string; + marketplace: string; + }>; + taker: string; + }): Promise<{ + path: string; + steps: Array<{ + action: string; + status: string; + }>; + }> { + const endOperation = this.performanceMonitor.startOperation( + "executeBuy", + { options } + ); + + try { + const buyParams = { + taker: options.taker, + listings: options.listings.map((listing) => ({ + token: listing.tokenId, + price: listing.price.toString(), + seller: listing.seller, + source: listing.marketplace, + })), + }; + + const response = await this.makeRequest<{ + path: string; + steps: Array<{ + action: string; + status: string; + }>; + }>("/execute/buy/v2", buyParams, 1, {} as IAgentRuntime); + + endOperation(); + return response; + } catch (error) { + this.performanceMonitor.recordMetric({ + operation: "executeBuy", + duration: 0, + success: false, + metadata: { error: error.message, options }, + }); + + throw error; + } + } + + async getOwnedNFTs(owner: string): Promise< + Array<{ + tokenId: string; + collectionAddress: string; + name: string; + imageUrl?: string; + attributes?: Record; + }> + > { + const endOperation = this.performanceMonitor.startOperation( + "getOwnedNFTs", + { owner } + ); + + try { + const params = { + users: owner, + limit: "100", + includeAttributes: "true", + }; + + const response = await this.makeRequest<{ + tokens: Array<{ + token: { + tokenId: string; + collection: { + id: string; + name: string; + }; + image: string; + attributes?: Array<{ + key: string; + value: string; + }>; + }; + }>; + }>("/users/tokens/v1", params, 1, {} as IAgentRuntime); + + const nfts = response.tokens.map((token) => ({ + tokenId: token.token.tokenId, + collectionAddress: token.token.collection.id, + name: token.token.collection.name, + imageUrl: token.token.image, + attributes: token.token.attributes + ? Object.fromEntries( + token.token.attributes.map((attr) => [ + attr.key, + attr.value, + ]) + ) + : undefined, + })); + + endOperation(); + return nfts; + } catch (error) { + this.performanceMonitor.recordMetric({ + operation: "getOwnedNFTs", + duration: 0, + success: false, + metadata: { error: error.message, owner }, + }); + + throw error; + } + } +} 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 00000000000..3c978e25611 --- /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 new file mode 100644 index 00000000000..ddae9ced441 --- /dev/null +++ b/packages/plugin-nft-collections/src/services/social-analytics.ts @@ -0,0 +1,214 @@ +import axios from "axios"; +import { z } from "zod"; +import { SocialMetricsSchema, SocialMetrics } from "../utils/validation"; +import { MemoryCacheManager } from "./cache-manager"; + +// Enhanced Social Metrics Schema +const ExtendedSocialMetricsSchema = SocialMetricsSchema.extend({ + twitter: z + .object({ + followers_count: z.number().int().min(0).optional(), + following_count: z.number().int().min(0).optional(), + tweet_count: z.number().int().min(0).optional(), + engagement_rate: z.number().min(0).max(100).optional(), + }) + .optional(), + coinGecko: z + .object({ + community_score: z.number().min(0).max(100).optional(), + twitter_followers: z.number().int().min(0).optional(), + telegram_users: z.number().int().min(0).optional(), + }) + .optional(), + dune: z + .object({ + total_transactions: z.number().int().min(0).optional(), + unique_wallets: z.number().int().min(0).optional(), + avg_transaction_value: z.number().min(0).optional(), + }) + .optional(), +}); + +export interface SocialAnalyticsConfig { + twitterApiKey?: string; + coinGeckoApiKey?: string; + duneApiKey?: string; +} + +export class SocialAnalyticsService { + private config: SocialAnalyticsConfig; + private cacheManager: MemoryCacheManager; + private static CACHE_TTL = 1 * 60 * 60; // 1 hour cache + + constructor(config: SocialAnalyticsConfig = {}) { + this.config = config; + this.cacheManager = new MemoryCacheManager({ + ttl: SocialAnalyticsService.CACHE_TTL, + }); + } + + async getSocialMetrics(address: string): Promise { + const cacheKey = `social_metrics:${address}`; + + // Check cache first + const cachedMetrics = this.cacheManager.get(cacheKey); + if (cachedMetrics) return cachedMetrics; + + try { + // Fetch metrics from multiple sources with error handling + const [twitterMetrics, coinGeckoMetrics, duneMetrics] = + await Promise.allSettled([ + this.getTwitterMetrics(address), + this.getCoinGeckoSocialMetrics(address), + this.getDuneSocialMetrics(address), + ]); + + const socialMetrics: SocialMetrics = { + lastUpdate: new Date().toISOString(), + twitterFollowers: this.extractMetric( + twitterMetrics, + "followers_count" + ), + twitterEngagement: + this.calculateTwitterEngagementRate(twitterMetrics), + discordMembers: 0, // Placeholder for future implementation + discordActive: 0, + telegramMembers: this.extractMetric( + coinGeckoMetrics, + "telegram_users" + ), + telegramActive: 0, + }; + + // Validate and cache metrics + const validatedMetrics = + ExtendedSocialMetricsSchema.parse(socialMetrics); + this.cacheManager.set( + cacheKey, + validatedMetrics, + SocialAnalyticsService.CACHE_TTL + ); + + return validatedMetrics; + } catch (error) { + console.error(`Social metrics fetch failed for ${address}:`, error); + throw new Error( + `Failed to retrieve social metrics: ${error.message}` + ); + } + } + + private async getTwitterMetrics(address: string) { + if (!this.config.twitterApiKey) { + console.warn("Twitter API key not provided for social metrics"); + return undefined; + } + + try { + const response = await axios.get( + `https://api.twitter.com/2/users/by/username/${address}`, + { + headers: { + Authorization: `Bearer ${this.config.twitterApiKey}`, + }, + } + ); + + return { + followers_count: + response.data.data.public_metrics.followers_count, + following_count: + response.data.data.public_metrics.following_count, + tweet_count: response.data.data.public_metrics.tweet_count, + engagement_rate: this.calculateTwitterEngagementRate( + response.data.data + ), + }; + } catch (error) { + console.error("Twitter metrics fetch failed", error); + return undefined; + } + } + + private async getCoinGeckoSocialMetrics(address: string) { + if (!this.config.coinGeckoApiKey) { + console.warn("CoinGecko API key not provided for social metrics"); + } + + try { + const response = await axios.get( + `https://api.coingecko.com/api/v3/coins/${address}` + ); + return { + community_score: response.data.community_score, + twitter_followers: response.data.twitter_followers, + telegram_users: response.data.telegram_users, + }; + } catch (error) { + console.error("CoinGecko metrics fetch failed", error); + return undefined; + } + } + + private async getDuneSocialMetrics(address: string) { + if (!this.config.duneApiKey) { + console.warn("Dune API key not provided for social metrics"); + return undefined; + } + + try { + const response = await axios.get( + `https://api.dune.com/api/v1/query/${address}/results`, + { + headers: { "X-Dune-API-Key": this.config.duneApiKey }, + } + ); + + return { + total_transactions: response.data.total_transactions, + unique_wallets: response.data.unique_wallets, + avg_transaction_value: response.data.avg_transaction_value, + }; + } catch (error) { + console.error("Dune metrics fetch failed", error); + return undefined; + } + } + + // Utility method to extract specific metrics safely + private extractMetric( + result: PromiseSettledResult, + key: string + ): number | undefined { + if (result.status === "fulfilled" && result.value) { + return result.value[key]; + } + return undefined; + } + + // Placeholder for more sophisticated engagement rate calculation + private calculateTwitterEngagementRate(userData: any): number { + // Basic engagement rate calculation + // Implement more sophisticated logic as needed + const { followers_count, tweet_count } = userData.public_metrics; + return followers_count > 0 + ? Math.min((tweet_count / followers_count) * 100, 100) + : 0; + } + + // Sentiment and growth tracking placeholders + async getSentimentAnalysis(address: string): Promise { + // Placeholder for sentiment analysis + return 0; + } + + async getGrowthRate(address: string): Promise { + // Placeholder for growth rate tracking + return 0; + } +} + +export const socialAnalyticsService = new SocialAnalyticsService({ + twitterApiKey: process.env.TWITTER_API_KEY || "", + duneApiKey: process.env.DUNE_API_KEY || "", +}); diff --git a/packages/plugin-nft-collections/src/templates/floor-sweep.ts b/packages/plugin-nft-collections/src/templates/floor-sweep.ts new file mode 100644 index 00000000000..1c3cb54ade0 --- /dev/null +++ b/packages/plugin-nft-collections/src/templates/floor-sweep.ts @@ -0,0 +1,66 @@ +import { NFTCollection } from "../types"; + +export const floorSweepTemplates = { + successfulSweep: ({ + collection, + quantity, + totalPrice, + averagePrice, + path, + steps, + }: { + collection: NFTCollection | string; + quantity: number; + totalPrice: number; + averagePrice: number; + path: string; + steps: Array<{ action: string; status: string }>; + }) => `Successfully swept ${quantity} NFTs from collection ${typeof collection === "string" ? collection : collection.name}: +• Total Cost: ${totalPrice} ETH +• Average Price: ${averagePrice.toFixed(4)} ETH +• Transaction Path: ${path} +• Status: ${steps.map((step) => `${step.action} - ${step.status}`).join(", ")}`, + + sweepFailed: (error: string) => `Failed to sweep floor NFTs: ${error}`, + + missingCollection: () => "No valid collection address found in message", + + insufficientListings: (available: number, requested: number) => + `Only ${available} NFTs available at floor price (requested ${requested})`, + + sweepInProgress: ({ + collection, + quantity, + }: { + collection: NFTCollection | string; + quantity: number; + }) => + `Sweeping ${quantity} NFTs from collection ${typeof collection === "string" ? collection : collection.name}...`, + + floorPriceUpdate: ({ + collection, + floorPrice, + change24h, + }: { + collection: NFTCollection | string; + floorPrice: number; + change24h: number; + }) => `Current floor price for ${typeof collection === "string" ? collection : collection.name}: +• Price: ${floorPrice} ETH +• 24h Change: ${change24h >= 0 ? "+" : ""}${change24h.toFixed(2)}%`, + + marketplaceBreakdown: ( + marketplaces: Array<{ + name: string; + floorPrice: number; + availableTokens: number; + }> + ) => `Floor prices across marketplaces: +${marketplaces + .sort((a, b) => a.floorPrice - b.floorPrice) + .map( + (m) => + `• ${m.name}: ${m.floorPrice} ETH (${m.availableTokens} available)` + ) + .join("\n")}`, +}; diff --git a/packages/plugin-nft-collections/src/templates/index.ts b/packages/plugin-nft-collections/src/templates/index.ts new file mode 100644 index 00000000000..f9db739751e --- /dev/null +++ b/packages/plugin-nft-collections/src/templates/index.ts @@ -0,0 +1,98 @@ +export { listingTemplates } from "./nft-listing"; +export { floorSweepTemplates } from "./floor-sweep"; +export { marketStatsTemplates } from "./market-stats"; +export { socialAnalyticsTemplates } from "./social-analytics"; + +export const listNftTemplate = `Given the recent messages and NFT information below: + +{{recentMessages}} + +{{nftInfo}} + +Extract the following information about the requested NFT listing: +- Collection address: Must be a valid Ethereum address starting with "0x" +- Token ID: Must be a valid token ID number +- Price in ETH: Must be a string representing the amount in ETH (only number without coin symbol, e.g., "1.5") +- Marketplace: Must be "ikigailabs" + +Respond with a JSON markdown block containing only the extracted values: + +\`\`\`json +{ + "collectionAddress": string, + "tokenId": string, + "price": string, + "marketplace": "ikigailabs" +} +\`\`\` +`; + +export const floorSweepTemplate = `Given the recent messages and NFT information below: + +{{recentMessages}} + +{{nftInfo}} + +Extract the following information about the requested floor sweep: +- Collection address: Must be a valid Ethereum address starting with "0x" +- Quantity: Number of NFTs to sweep +- Maximum price per NFT in ETH: Must be a string representing the amount in ETH +- Sort by: Optional sorting criteria (e.g., "price_asc", "rarity_desc") + +Respond with a JSON markdown block containing only the extracted values: + +\`\`\`json +{ + "collectionAddress": string, + "quantity": number, + "maxPricePerNft": string, + "sortBy": "price_asc" | "price_desc" | "rarity_asc" | "rarity_desc" | null +} +\`\`\` +`; + +export const marketStatsTemplate = `Given the recent messages and NFT information below: + +{{recentMessages}} + +{{nftInfo}} + +Extract the following information about the requested market stats: +- Collection address: Must be a valid Ethereum address starting with "0x" +- Time period: Must be one of ["1h", "24h", "7d", "30d", "all"] +- Stat type: Must be one of ["floor", "volume", "sales", "holders"] + +Respond with a JSON markdown block containing only the extracted values: + +\`\`\`json +{ + "collectionAddress": string, + "timePeriod": "1h" | "24h" | "7d" | "30d" | "all", + "statType": "floor" | "volume" | "sales" | "holders" +} +\`\`\` +`; + +export const socialAnalyticsTemplate = `Given the recent messages and NFT information below: + +{{recentMessages}} + +{{nftInfo}} + +Extract the following information about the requested social analytics: +- Collection address: Must be a valid Ethereum address starting with "0x" +- Platform: Must be one of ["twitter", "discord", "telegram", "all"] +- Metric type: Must be one of ["sentiment", "engagement", "growth", "mentions"] +- Time period: Must be one of ["1h", "24h", "7d", "30d"] + +Respond with a JSON markdown block containing only the extracted values: + +\`\`\`json +{ + "collectionAddress": string, + "platform": "twitter" | "discord" | "telegram" | "all", + "metricType": "sentiment" | "engagement" | "growth" | "mentions", + "timePeriod": "1h" | "24h" | "7d" | "30d" +} +\`\`\` +`; diff --git a/packages/plugin-nft-collections/src/templates/market-stats.ts b/packages/plugin-nft-collections/src/templates/market-stats.ts new file mode 100644 index 00000000000..e58eee1865c --- /dev/null +++ b/packages/plugin-nft-collections/src/templates/market-stats.ts @@ -0,0 +1,145 @@ +import { NFTCollection, MarketIntelligence, MarketStats } from "../types"; + +export const marketStatsTemplates = { + collectionOverview: ({ + collection, + marketIntelligence, + }: { + collection: NFTCollection; + marketIntelligence?: MarketIntelligence; + }) => `${collection.name} Collection Overview: +• Floor Price: ${collection.floorPrice} ETH +• 24h Volume: ${collection.volume24h} ETH +• Market Cap: ${collection.marketCap} ETH +• Holders: ${collection.holders}${ + marketIntelligence + ? `\n\nMarket Intelligence: +• Wash Trading Score: ${marketIntelligence.washTradingMetrics.washTradingScore} +• Suspicious Volume (24h): ${marketIntelligence.washTradingMetrics.suspiciousVolume24h} ETH +• Best Bid: ${marketIntelligence.liquidityMetrics.bestBid} ETH +• Best Ask: ${marketIntelligence.liquidityMetrics.bestAsk} ETH` + : "" + }`, + + globalMarketStats: (stats: MarketStats) => `NFT Market Overview: +• Total Volume (24h): ${stats.totalVolume24h} ETH +• Total Market Cap: ${stats.totalMarketCap} ETH +• Total Collections: ${stats.totalCollections} +• Total Holders: ${stats.totalHolders} +• Average Floor Price: ${stats.averageFloorPrice} ETH`, + + whaleActivity: ({ + collection, + whales, + impact, + }: { + collection: NFTCollection | string; + whales: Array<{ + address: string; + holdings: number; + avgHoldingTime: number; + tradingVolume: number; + lastTrade: number; + }>; + impact: { + priceImpact: number; + volumeShare: number; + holdingsShare: number; + }; + }) => `Whale Activity for ${typeof collection === "string" ? collection : collection.name}: + +Top Whales: +${whales + .slice(0, 5) + .map( + (whale) => `• ${whale.address.slice(0, 6)}...${whale.address.slice(-4)} + Holdings: ${whale.holdings} NFTs + Avg Holding Time: ${(whale.avgHoldingTime / (24 * 60 * 60)).toFixed(1)} days + Trading Volume: ${whale.tradingVolume} ETH` + ) + .join("\n\n")} + +Market Impact: +• Price Impact: ${impact.priceImpact >= 0 ? "+" : ""}${impact.priceImpact.toFixed(2)}% +• Volume Share: ${(impact.volumeShare * 100).toFixed(1)}% +• Holdings Share: ${(impact.holdingsShare * 100).toFixed(1)}%`, + + priceHistory: ({ + collection, + history, + }: { + collection: NFTCollection | string; + history: Array<{ + timestamp: number; + price: number; + volume: number; + }>; + }) => { + const timeframes = [ + { label: "1h", duration: 60 * 60 }, + { label: "24h", duration: 24 * 60 * 60 }, + { label: "7d", duration: 7 * 24 * 60 * 60 }, + ]; + + const now = Date.now() / 1000; + const changes = timeframes.map((tf) => { + const pastPrice = history.find( + (h) => h.timestamp >= now - tf.duration + )?.price; + const currentPrice = history[history.length - 1]?.price || 0; + const change = pastPrice + ? ((currentPrice - pastPrice) / pastPrice) * 100 + : 0; + return `${tf.label}: ${change >= 0 ? "+" : ""}${change.toFixed(2)}%`; + }); + + return `Price History for ${typeof collection === "string" ? collection : collection.name}: + +Price Changes: +${changes.map((change) => `• ${change}`).join("\n")} + +Recent Trades: +${history + .slice(-5) + .reverse() + .map( + (h) => + `• ${new Date(h.timestamp * 1000).toLocaleString()}: ${ + h.price + } ETH (Volume: ${h.volume} ETH)` + ) + .join("\n")}`; + }, + + liquidityAnalysis: ({ + collection, + depth, + metrics, + }: { + collection: NFTCollection | string; + depth: Array<{ + price: number; + quantity: number; + totalValue: number; + }>; + metrics: { + totalLiquidity: number; + averageSpread: number; + volatility24h: number; + }; + }) => `Liquidity Analysis for ${typeof collection === "string" ? collection : collection.name}: + +Market Metrics: +• Total Liquidity: ${metrics.totalLiquidity} ETH +• Average Spread: ${(metrics.averageSpread * 100).toFixed(2)}% +• 24h Volatility: ${(metrics.volatility24h * 100).toFixed(2)}% + +Order Book Depth: +${depth + .slice(0, 5) + .map( + (level) => + `• ${level.price} ETH: ${level.quantity} NFTs (${level.totalValue} ETH)` + ) + .join("\n")}`, +}; diff --git a/packages/plugin-nft-collections/src/templates/nft-listing.ts b/packages/plugin-nft-collections/src/templates/nft-listing.ts new file mode 100644 index 00000000000..b908b7effe1 --- /dev/null +++ b/packages/plugin-nft-collections/src/templates/nft-listing.ts @@ -0,0 +1,59 @@ +import { NFTCollection } from "../types"; + +export const listingTemplates = { + successfulListing: ({ + collection, + tokenId, + purchasePrice, + listingPrice, + isPriceAutomatic, + status, + marketplaceUrl, + transactionHash, + }: { + collection: NFTCollection | string; + tokenId: string; + purchasePrice: number; + listingPrice: number; + isPriceAutomatic: boolean; + status: string; + marketplaceUrl: string; + transactionHash?: string; + }) => `Successfully created listing on ikigailabs.xyz: +• Collection: ${typeof collection === "string" ? collection : collection.name} (${typeof collection === "string" ? collection : collection.address}) +• Token ID: ${tokenId} +• Purchase Price: ${purchasePrice.toFixed(1)} ETH +• Listing Price: ${listingPrice.toFixed(1)} ETH (${isPriceAutomatic ? "2x purchase price" : "user specified"}) +• Status: ${status} +• Listing URL: ${marketplaceUrl}${transactionHash ? `\n• Transaction: ${transactionHash}` : ""}`, + + listingFailed: (error: string) => `Failed to list NFT: ${error}`, + + missingDetails: () => "Please provide the collection address and token ID", + + notOwned: () => "You don't own this NFT", + + noPurchaseHistory: () => + "Could not find purchase history for this NFT. Please specify a listing price.", + + noPurchasePrice: () => + "Could not determine purchase price. Please specify a listing price.", + + listingInProgress: ({ + collection, + tokenId, + }: { + collection: NFTCollection | string; + tokenId: string; + }) => + `Creating listing for Token #${tokenId} from collection ${typeof collection === "string" ? collection : collection.name}...`, + + listingCancelled: ({ + listingId, + transactionHash, + }: { + listingId: string; + transactionHash?: string; + }) => + `Successfully cancelled listing ${listingId}${transactionHash ? `\nTransaction: ${transactionHash}` : ""}`, +}; diff --git a/packages/plugin-nft-collections/src/templates/social-analytics.ts b/packages/plugin-nft-collections/src/templates/social-analytics.ts new file mode 100644 index 00000000000..c4ec4b5cba1 --- /dev/null +++ b/packages/plugin-nft-collections/src/templates/social-analytics.ts @@ -0,0 +1,155 @@ +import { NFTCollection, SocialMetrics, CommunityMetrics } from "../types"; + +export const socialAnalyticsTemplates = { + socialOverview: ({ + collection, + socialMetrics, + communityMetrics, + }: { + collection: NFTCollection | string; + socialMetrics: SocialMetrics; + communityMetrics: CommunityMetrics; + }) => `Social Analytics for ${typeof collection === "string" ? collection : collection.name}: + +Twitter Metrics: +• Followers: ${socialMetrics.twitter.followers} +• Engagement: ${ + socialMetrics.twitter.engagement.likes + + socialMetrics.twitter.engagement.retweets + + socialMetrics.twitter.engagement.replies + } interactions +• Sentiment: ${( + (socialMetrics.twitter.sentiment.positive * 100) / + (socialMetrics.twitter.sentiment.positive + + socialMetrics.twitter.sentiment.neutral + + socialMetrics.twitter.sentiment.negative) + ).toFixed(1)}% positive +• Trending: ${socialMetrics.trending ? "Yes" : "No"} + +Community Stats: +• Total Members: ${communityMetrics.totalMembers} +• Growth Rate: ${communityMetrics.growthRate}% +• Active Users: ${communityMetrics.engagement.activeUsers} +• Messages/Day: ${communityMetrics.engagement.messagesPerDay} + +Platform Breakdown:${ + communityMetrics.discord + ? `\n\nDiscord: +• Members: ${communityMetrics.discord.members} +• Active Users: ${communityMetrics.discord.activity.activeUsers} +• Growth Rate: ${communityMetrics.discord.activity.growthRate}% +• Messages/Day: ${communityMetrics.discord.activity.messagesPerDay} + +Top Channels: +${communityMetrics.discord.channels + .slice(0, 3) + .map( + (channel) => + `• ${channel.name}: ${channel.members} members (${channel.activity} msgs/day)` + ) + .join("\n")}` + : "" + }${ + communityMetrics.telegram + ? `\n\nTelegram: +• Members: ${communityMetrics.telegram.members} +• Active Users: ${communityMetrics.telegram.activity.activeUsers} +• Growth Rate: ${communityMetrics.telegram.activity.growthRate}% +• Messages/Day: ${communityMetrics.telegram.activity.messagesPerDay}` + : "" + }`, + + topInfluencers: ({ + collection, + influencers, + }: { + collection: NFTCollection | string; + influencers: SocialMetrics["influencers"]; + }) => `Top Influencers for ${typeof collection === "string" ? collection : collection.name}: + +${influencers + .slice(0, 5) + .map( + (inf, i) => + `${i + 1}. ${inf.address.slice(0, 6)}...${inf.address.slice(-4)} (${ + inf.platform + }) +• Followers: ${inf.followers} +• Engagement Rate: ${(inf.engagement * 100).toFixed(1)}% +• Sentiment Score: ${(inf.sentiment * 100).toFixed(1)}%` + ) + .join("\n\n")}`, + + recentMentions: ({ + collection, + mentions, + }: { + collection: NFTCollection | string; + mentions: SocialMetrics["mentions"]; + }) => `Recent Mentions for ${typeof collection === "string" ? collection : collection.name}: + +${mentions + .slice(0, 5) + .map( + (mention) => `• ${mention.platform} | ${new Date( + mention.timestamp * 1000 + ).toLocaleString()} + ${mention.content.slice(0, 100)}${mention.content.length > 100 ? "..." : ""} + By: ${mention.author} | Reach: ${mention.reach}` + ) + .join("\n\n")}`, + + communityEngagement: ({ + collection, + topChannels, + }: { + collection: NFTCollection | string; + topChannels: CommunityMetrics["engagement"]["topChannels"]; + }) => `Community Engagement for ${typeof collection === "string" ? collection : collection.name}: + +Most Active Channels: +${topChannels + .map( + (channel) => + `• ${channel.platform} | ${channel.name}: ${channel.activity} messages/day` + ) + .join("\n")}`, + + sentimentAnalysis: ({ + collection, + sentiment, + }: { + collection: NFTCollection | string; + sentiment: { + overall: number; + breakdown: { + positive: number; + neutral: number; + negative: number; + }; + trends: Array<{ + topic: string; + sentiment: number; + volume: number; + }>; + }; + }) => `Sentiment Analysis for ${typeof collection === "string" ? collection : collection.name}: + +Overall Sentiment Score: ${(sentiment.overall * 100).toFixed(1)}% + +Sentiment Breakdown: +• Positive: ${(sentiment.breakdown.positive * 100).toFixed(1)}% +• Neutral: ${(sentiment.breakdown.neutral * 100).toFixed(1)}% +• Negative: ${(sentiment.breakdown.negative * 100).toFixed(1)}% + +Top Topics by Sentiment: +${sentiment.trends + .slice(0, 5) + .map( + (trend) => + `• ${trend.topic}: ${(trend.sentiment * 100).toFixed( + 1 + )}% positive (${trend.volume} mentions)` + ) + .join("\n")}`, +}; diff --git a/packages/plugin-nft-collections/src/tests/actions.test.ts b/packages/plugin-nft-collections/src/tests/actions.test.ts new file mode 100644 index 00000000000..037eab62e69 --- /dev/null +++ b/packages/plugin-nft-collections/src/tests/actions.test.ts @@ -0,0 +1,151 @@ +import { describe, expect, it, vi } from "vitest"; +import { listNFTAction } from "../actions/list-nft"; +import { IAgentRuntime, Memory } from "@elizaos/core"; +import { NFTService } from "../types"; + +describe("NFT Actions", () => { + describe("List NFT Action", () => { + const mockRuntime = { + services: { + get: vi.fn(), + }, + messageManager: { + createMemory: vi.fn(), + }, + agentId: "00000000-0000-0000-0000-000000000000", + } as unknown as IAgentRuntime; + + const mockNftService = { + getOwnedNFTs: vi.fn(), + createListing: vi.fn(), + } as unknown as NFTService & { + getOwnedNFTs: ReturnType; + createListing: ReturnType; + }; + + beforeEach(() => { + vi.clearAllMocks(); + (mockRuntime.services.get as any).mockReturnValue(mockNftService); + }); + + it("should validate list NFT message", async () => { + const message: Memory = { + id: "00000000-0000-0000-0000-000000000001", + content: { + text: "List NFT #123 from collection 0x1234 for 1.5 ETH", + }, + roomId: "00000000-0000-0000-0000-000000000002", + userId: "00000000-0000-0000-0000-000000000003", + agentId: "00000000-0000-0000-0000-000000000000", + }; + + const isValid = await listNFTAction.validate(mockRuntime, message); + expect(isValid).toBe(true); + }); + + it("should not validate invalid message", async () => { + const message: Memory = { + id: "00000000-0000-0000-0000-000000000004", + content: { + text: "Show me floor price", + }, + roomId: "00000000-0000-0000-0000-000000000002", + userId: "00000000-0000-0000-0000-000000000003", + agentId: "00000000-0000-0000-0000-000000000000", + }; + + const isValid = await listNFTAction.validate(mockRuntime, message); + expect(isValid).toBe(false); + }); + + it("should handle list NFT request successfully", async () => { + const message: Memory = { + id: "00000000-0000-0000-0000-000000000005", + content: { + text: "List NFT #123 from collection 0x1234 for 1.5 ETH", + }, + roomId: "00000000-0000-0000-0000-000000000002", + userId: "00000000-0000-0000-0000-000000000003", + agentId: "00000000-0000-0000-0000-000000000000", + }; + + mockNftService.getOwnedNFTs.mockResolvedValueOnce([ + { + collectionAddress: "0x1234", + tokenId: "123", + name: "Test NFT", + imageUrl: "https://example.com/nft.png", + }, + ]); + + mockNftService.createListing.mockResolvedValueOnce({ + listingId: "test-listing", + status: "active", + marketplaceUrl: "https://ikigailabs.xyz/listing/test", + }); + + const result = await listNFTAction.handler(mockRuntime, message); + expect(result).toBe(true); + expect(mockNftService.createListing).toHaveBeenCalledWith( + expect.objectContaining({ + tokenId: "123", + collectionAddress: "0x1234", + price: 1.5, + marketplace: "ikigailabs", + }) + ); + }); + + it("should handle NFT not owned error", async () => { + const message: Memory = { + id: "00000000-0000-0000-0000-000000000006", + content: { + text: "List NFT #123 from collection 0x1234 for 1.5 ETH", + }, + roomId: "00000000-0000-0000-0000-000000000002", + userId: "00000000-0000-0000-0000-000000000003", + agentId: "00000000-0000-0000-0000-000000000000", + }; + + mockNftService.getOwnedNFTs.mockResolvedValueOnce([]); + + const result = await listNFTAction.handler(mockRuntime, message); + expect(result).toBe(false); + expect( + mockRuntime.messageManager.createMemory + ).toHaveBeenCalledWith( + expect.objectContaining({ + content: { + text: expect.stringContaining("You don't own this NFT"), + }, + }) + ); + }); + + it("should handle missing NFT service error", async () => { + const message: Memory = { + id: "00000000-0000-0000-0000-000000000007", + content: { + text: "List NFT #123 from collection 0x1234 for 1.5 ETH", + }, + roomId: "00000000-0000-0000-0000-000000000002", + userId: "00000000-0000-0000-0000-000000000003", + agentId: "00000000-0000-0000-0000-000000000000", + }; + + (mockRuntime.services.get as any).mockReturnValue(null); + + const result = await listNFTAction.handler(mockRuntime, message); + expect(result).toBe(false); + expect( + mockRuntime.messageManager.createMemory + ).toHaveBeenCalledWith( + expect.objectContaining({ + content: { + text: expect.stringContaining("NFT service not found"), + }, + }) + ); + }); + }); +}); diff --git a/packages/plugin-nft-collections/src/tests/providers.test.ts b/packages/plugin-nft-collections/src/tests/providers.test.ts new file mode 100644 index 00000000000..eef25d06802 --- /dev/null +++ b/packages/plugin-nft-collections/src/tests/providers.test.ts @@ -0,0 +1,128 @@ +import { describe, expect, it, vi } from "vitest"; +import { nftCollectionProvider } from "../providers/nft-collections"; +import { IAgentRuntime, Memory } from "@elizaos/core"; +import { NFTService } from "../types"; + +describe("NFT Collections Provider", () => { + const mockRuntime = { + services: { + get: vi.fn(), + }, + messageManager: { + createMemory: vi.fn(), + }, + agentId: "00000000-0000-0000-0000-000000000000", + } as unknown as IAgentRuntime; + + const mockNftService = { + getTopCollections: vi.fn(), + getMarketStats: vi.fn(), + } as unknown as NFTService & { + getTopCollections: ReturnType; + getMarketStats: ReturnType; + }; + + beforeEach(() => { + vi.clearAllMocks(); + (mockRuntime.services.get as any).mockReturnValue(mockNftService); + }); + + it("should get top collections", async () => { + const message: Memory = { + id: "00000000-0000-0000-0000-000000000001", + content: { + text: "Show me top NFT collections", + }, + roomId: "00000000-0000-0000-0000-000000000002", + userId: "00000000-0000-0000-0000-000000000003", + agentId: "00000000-0000-0000-0000-000000000000", + }; + + mockNftService.getTopCollections.mockResolvedValueOnce([ + { + name: "Test Collection", + address: "0x1234", + floorPrice: 1.5, + volume24h: 100, + marketCap: 1000, + holders: 500, + symbol: "TEST", + description: "Test NFT Collection", + imageUrl: "https://example.com/image.png", + }, + ]); + + const result = await nftCollectionProvider.get(mockRuntime, message); + expect(result).toContain("Test Collection"); + expect(result).toContain("1.5 ETH"); + expect(result).toContain("100 ETH"); + }); + + it("should get market stats", async () => { + const message: Memory = { + id: "00000000-0000-0000-0000-000000000004", + content: { + text: "Show me NFT market stats", + }, + roomId: "00000000-0000-0000-0000-000000000002", + userId: "00000000-0000-0000-0000-000000000003", + agentId: "00000000-0000-0000-0000-000000000000", + }; + + mockNftService.getTopCollections.mockResolvedValueOnce([ + { + name: "Test Collection", + address: "0x1234", + floorPrice: 1.5, + volume24h: 100, + marketCap: 1000, + holders: 500, + symbol: "TEST", + description: "Test NFT Collection", + imageUrl: "https://example.com/image.png", + }, + ]); + + const result = await nftCollectionProvider.get(mockRuntime, message); + expect(result).toContain("Test Collection"); + expect(result).toContain("1.5 ETH"); + }); + + it("should handle missing NFT service", async () => { + const message: Memory = { + id: "00000000-0000-0000-0000-000000000005", + content: { + text: "Show me top NFT collections", + }, + roomId: "00000000-0000-0000-0000-000000000002", + userId: "00000000-0000-0000-0000-000000000003", + agentId: "00000000-0000-0000-0000-000000000000", + }; + + (mockRuntime.services.get as any).mockReturnValue(null); + + await expect( + nftCollectionProvider.get(mockRuntime, message) + ).rejects.toThrow("NFT service not found"); + }); + + it("should handle service errors", async () => { + const message: Memory = { + id: "00000000-0000-0000-0000-000000000006", + content: { + text: "Show me top NFT collections", + }, + roomId: "00000000-0000-0000-0000-000000000002", + userId: "00000000-0000-0000-0000-000000000003", + agentId: "00000000-0000-0000-0000-000000000000", + }; + + mockNftService.getTopCollections.mockRejectedValueOnce( + new Error("API error") + ); + + await expect( + nftCollectionProvider.get(mockRuntime, message) + ).rejects.toThrow("API error"); + }); +}); diff --git a/packages/plugin-nft-collections/src/tests/reservoir.integration.test.ts b/packages/plugin-nft-collections/src/tests/reservoir.integration.test.ts new file mode 100644 index 00000000000..e6e1a1bade1 --- /dev/null +++ b/packages/plugin-nft-collections/src/tests/reservoir.integration.test.ts @@ -0,0 +1,148 @@ +import { describe, it, expect } from "vitest"; +import { ReservoirService } from "../services/reservoir"; +import { + IAgentRuntime, + ServiceType, + Service, + IMemoryManager, +} from "@elizaos/core"; +import { MemoryCacheManager } from "../services/cache-manager"; +import { RateLimiter } from "../services/rate-limiter"; + +describe("Reservoir Service Integration", () => { + const mockRuntime = { + getSetting: function (key: string): string | undefined { + if (key === "RESERVOIR_API_KEY") { + const apiKey = process.env.RESERVOIR_API_KEY; + if (!apiKey) { + console.warn("No RESERVOIR_API_KEY found in environment"); + } + return apiKey; + } + return undefined; + }, + services: new Map(), + messageManager: {} as IMemoryManager, + documentsManager: {} as any, + knowledgeManager: {} as any, + ragKnowledgeManager: {} as any, + loreManager: {} as any, + agentId: "test-agent", + serverUrl: "http://localhost", + databaseAdapter: {} as any, + token: "test-token", + modelProvider: {} as any, + imageModelProvider: {} as any, + imageVisionModelProvider: {} as any, + character: {} as any, + providers: new Map(), + actions: new Map(), + evaluators: new Map(), + plugins: new Map(), + descriptionManager: {} as any, + memoryManager: {} as any, + conversationManager: {} as any, + roomManager: {} as any, + userManager: {} as any, + agentManager: {} as any, + pluginManager: {} as any, + settingsManager: {} as any, + fileManager: {} as any, + vectorStoreManager: {} as any, + embeddingProvider: {} as any, + audioModelProvider: {} as any, + audioTranscriptionModelProvider: {} as any, + audioSpeechModelProvider: {} as any, + cacheManager: new MemoryCacheManager(), + clients: new Map(), + initialize: async () => {}, + registerMemoryManager: () => {}, + } as unknown as IAgentRuntime; + + it("should fetch top collections from Reservoir API", async () => { + const apiKey = mockRuntime.getSetting("RESERVOIR_API_KEY"); + if (!apiKey) { + console.warn("Skipping test: No RESERVOIR_API_KEY provided"); + return; + } + + const service = new ReservoirService({ + apiKey, + cacheManager: new MemoryCacheManager(), + rateLimiter: new RateLimiter(), + }); + + const collections = await service.getTopCollections(mockRuntime); + expect(collections).toBeDefined(); + expect(Array.isArray(collections)).toBe(true); + expect(collections.length).toBeGreaterThan(0); + if (collections.length > 0) { + expect(collections[0]).toHaveProperty("name"); + expect(collections[0]).toHaveProperty("address"); + expect(collections[0]).toHaveProperty("floorPrice"); + } + }); + + it("should fetch floor listings for a collection", async () => { + const apiKey = mockRuntime.getSetting("RESERVOIR_API_KEY"); + if (!apiKey) { + console.warn("Skipping test: No RESERVOIR_API_KEY provided"); + return; + } + + const service = new ReservoirService({ + apiKey, + cacheManager: new MemoryCacheManager(), + rateLimiter: new RateLimiter(), + }); + + // First get a collection address + const collections = await service.getTopCollections(mockRuntime); + expect(collections.length).toBeGreaterThan(0); + + const floorListings = await service.getFloorListings({ + collection: collections[0].address, + limit: 3, + sortBy: "price", + }); + + expect(floorListings).toBeDefined(); + expect(Array.isArray(floorListings)).toBe(true); + if (floorListings.length > 0) { + expect(floorListings[0]).toHaveProperty("tokenId"); + expect(floorListings[0]).toHaveProperty("price"); + expect(floorListings[0]).toHaveProperty("seller"); + } + }); + + it("should fetch owned NFTs for a wallet", async () => { + const apiKey = mockRuntime.getSetting("RESERVOIR_API_KEY"); + if (!apiKey) { + console.warn("Skipping test: No RESERVOIR_API_KEY provided"); + return; + } + + const service = new ReservoirService({ + apiKey, + cacheManager: new MemoryCacheManager(), + rateLimiter: new RateLimiter(), + }); + + const ownerAddress = process.env.TEST_WALLET_ADDRESS; + if (!ownerAddress) { + console.log( + "Skipping owned NFTs test - no TEST_WALLET_ADDRESS provided" + ); + return; + } + + const ownedNFTs = await service.getOwnedNFTs(ownerAddress); + expect(ownedNFTs).toBeDefined(); + expect(Array.isArray(ownedNFTs)).toBe(true); + if (ownedNFTs.length > 0) { + expect(ownedNFTs[0]).toHaveProperty("tokenId"); + expect(ownedNFTs[0]).toHaveProperty("collectionAddress"); + expect(ownedNFTs[0]).toHaveProperty("name"); + } + }); +}); diff --git a/packages/plugin-nft-collections/src/tests/services.test.ts b/packages/plugin-nft-collections/src/tests/services.test.ts new file mode 100644 index 00000000000..a7a47abf8e4 --- /dev/null +++ b/packages/plugin-nft-collections/src/tests/services.test.ts @@ -0,0 +1,91 @@ +import { describe, it, expect, vi } from "vitest"; +import { ReservoirService } from "../services/reservoir"; +import { MarketIntelligenceService } from "../services/market-intelligence"; +import { SocialAnalyticsService } from "../services/social-analytics"; +import { IAgentRuntime } from "@elizaos/core"; +import { MemoryCacheManager } from "../services/cache-manager"; +import { RateLimiter } from "../services/rate-limiter"; + +describe("NFT Services", () => { + const mockRuntime = { + getSetting: (key: string): string | undefined => { + if (key === "RESERVOIR_API_KEY") return "test-key"; + return undefined; + }, + } as unknown as IAgentRuntime; + + describe("ReservoirService", () => { + it("should fetch collections", async () => { + const service = new ReservoirService({ apiKey: "test-key" }); + const result = await service.getTopCollections(mockRuntime, 5); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + }); + + it("should fetch floor listings", async () => { + const service = new ReservoirService({ apiKey: "test-key" }); + const result = await service.getFloorListings({ + collection: "0x1234", + limit: 5, + sortBy: "price", + }); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + }); + }); + + describe("MarketIntelligenceService", () => { + const cacheManager = new MemoryCacheManager(); + const rateLimiter = new RateLimiter(); + + it("should initialize correctly", () => { + const service = new MarketIntelligenceService({ + cacheManager, + rateLimiter, + openSeaApiKey: "test-key", + reservoirApiKey: "test-key", + }); + expect(service).toBeDefined(); + }); + + it("should return market intelligence data", async () => { + const service = new MarketIntelligenceService({ + cacheManager, + rateLimiter, + openSeaApiKey: "test-key", + reservoirApiKey: "test-key", + }); + + const result = await service.getMarketIntelligence("0x1234"); + expect(result).toBeDefined(); + expect(result.floorPrice).toBeDefined(); + expect(result.volume24h).toBeDefined(); + }); + }); + + describe("SocialAnalyticsService", () => { + const mockData = { + lastUpdate: new Date().toISOString(), + twitterFollowers: 1000, + discordMembers: 500, + }; + + it("should initialize correctly", () => { + const service = new SocialAnalyticsService({}); + expect(service).toBeDefined(); + }); + + it("should return social metrics", async () => { + const service = new SocialAnalyticsService({}); + vi.spyOn(service as any, "fetchSocialData").mockResolvedValue( + mockData + ); + + const result = await service.getSocialMetrics("0x1234"); + expect(result).toBeDefined(); + expect(result.lastUpdate).toBeDefined(); + expect(result.twitterFollowers).toBeDefined(); + expect(result.discordMembers).toBeDefined(); + }); + }); +}); diff --git a/packages/plugin-nft-collections/src/tests/templates.test.ts b/packages/plugin-nft-collections/src/tests/templates.test.ts new file mode 100644 index 00000000000..ec5e97a2f29 --- /dev/null +++ b/packages/plugin-nft-collections/src/tests/templates.test.ts @@ -0,0 +1,270 @@ +import { describe, expect, it } from "vitest"; +import { + listingTemplates, + floorSweepTemplates, + marketStatsTemplates, + socialAnalyticsTemplates, + listNftTemplate, + floorSweepTemplate, + marketStatsTemplate, + socialAnalyticsTemplate, +} from "../templates"; + +describe("NFT Collection Templates", () => { + describe("Listing Templates", () => { + it("should generate successful listing message", () => { + const result = listingTemplates.successfulListing({ + collection: "0x1234567890abcdef", + tokenId: "123", + purchasePrice: 1.5, + listingPrice: 3.0, + isPriceAutomatic: true, + status: "active", + marketplaceUrl: "https://ikigailabs.xyz/listing/123", + transactionHash: "0xabcdef", + }); + + expect(result).toContain("Successfully created listing"); + expect(result).toContain("0x1234567890abcdef"); + expect(result).toContain("1.5 ETH"); + expect(result).toContain("3.0 ETH"); + expect(result).toContain("0xabcdef"); + }); + + it("should generate listing failed message", () => { + const result = listingTemplates.listingFailed( + "Insufficient balance" + ); + expect(result).toBe("Failed to list NFT: Insufficient balance"); + }); + }); + + describe("Floor Sweep Templates", () => { + it("should generate successful sweep message", () => { + const result = floorSweepTemplates.successfulSweep({ + collection: "0x1234567890abcdef", + quantity: 5, + totalPrice: 10, + averagePrice: 2, + path: "direct", + steps: [ + { action: "approve", status: "completed" }, + { action: "buy", status: "completed" }, + ], + }); + + expect(result).toContain("Successfully swept 5 NFTs"); + expect(result).toContain("10 ETH"); + expect(result).toContain("2.0000 ETH"); + expect(result).toContain("approve - completed"); + }); + + it("should generate insufficient listings message", () => { + const result = floorSweepTemplates.insufficientListings(3, 5); + expect(result).toBe( + "Only 3 NFTs available at floor price (requested 5)" + ); + }); + }); + + describe("Market Stats Templates", () => { + it("should generate collection overview", () => { + const result = marketStatsTemplates.collectionOverview({ + collection: { + name: "Test Collection", + address: "0x1234", + floorPrice: 1.5, + volume24h: 100, + marketCap: 1000, + holders: 500, + symbol: "TEST", + description: "Test NFT Collection", + imageUrl: "https://example.com/image.png", + }, + marketIntelligence: { + washTradingMetrics: { + washTradingScore: 0.1, + suspiciousVolume24h: 10, + suspiciousTransactions24h: 5, + }, + liquidityMetrics: { + bestBid: 1.4, + bestAsk: 1.6, + depth: [ + { price: 1.4, quantity: 2 }, + { price: 1.5, quantity: 3 }, + ], + bidAskSpread: 0.2, + }, + priceHistory: [ + { timestamp: 1234567890, price: 1.2, volume: 50 }, + { timestamp: 1234567891, price: 1.3, volume: 60 }, + ], + marketplaceActivity: { + listings: { + volume24h: 100, + trades24h: 50, + marketShare: 0.3, + }, + sales: { + volume24h: 80, + trades24h: 40, + marketShare: 0.25, + }, + volume: { + volume24h: 180, + trades24h: 90, + marketShare: 0.55, + }, + averagePrice: { + volume24h: 2, + trades24h: 1, + marketShare: 0.1, + }, + }, + whaleActivity: [ + { + address: "0xabc", + type: "buy", + amount: 10, + timestamp: 1234567890, + }, + { + address: "0xdef", + type: "sell", + amount: 5, + timestamp: 1234567891, + }, + ], + }, + }); + + expect(result).toContain("Test Collection"); + expect(result).toContain("1.5 ETH"); + expect(result).toContain("100 ETH"); + expect(result).toContain("500"); + expect(result).toContain("0.1"); + }); + }); + + describe("Social Analytics Templates", () => { + it("should generate social overview", () => { + const result = socialAnalyticsTemplates.socialOverview({ + collection: "Test Collection", + socialMetrics: { + twitter: { + followers: 10000, + engagement: { + likes: 500, + retweets: 200, + replies: 300, + mentions: 150, + }, + sentiment: { + positive: 0.7, + neutral: 0.2, + negative: 0.1, + }, + }, + trending: true, + mentions: [ + { + platform: "twitter", + content: "Great collection!", + author: "user123", + timestamp: 1234567890, + reach: 5000, + }, + ], + influencers: [ + { + address: "0xabc", + platform: "twitter", + followers: 50000, + engagement: 0.05, + sentiment: 0.8, + }, + ], + }, + communityMetrics: { + totalMembers: 5000, + growthRate: 10, + engagement: { + activeUsers: 1000, + messagesPerDay: 500, + topChannels: [ + { + platform: "discord", + name: "general", + activity: 100, + }, + ], + }, + discord: { + members: 3000, + activity: { + messagesPerDay: 1000, + activeUsers: 500, + growthRate: 0.1, + }, + channels: [ + { + name: "general", + members: 2000, + activity: 100, + }, + ], + }, + telegram: { + members: 2000, + activity: { + messagesPerDay: 800, + activeUsers: 300, + growthRate: 0.05, + }, + }, + }, + }); + + expect(result).toContain("Test Collection"); + expect(result).toContain("10000"); + expect(result).toContain("1000 interactions"); + expect(result).toContain("70.0% positive"); + expect(result).toContain("5000"); + }); + }); + + describe("Template Strings", () => { + it("should contain required placeholders in listNftTemplate", () => { + expect(listNftTemplate).toContain("{{recentMessages}}"); + expect(listNftTemplate).toContain("{{nftInfo}}"); + expect(listNftTemplate).toContain("collectionAddress"); + expect(listNftTemplate).toContain("tokenId"); + expect(listNftTemplate).toContain("price"); + }); + + it("should contain required placeholders in floorSweepTemplate", () => { + expect(floorSweepTemplate).toContain("{{recentMessages}}"); + expect(floorSweepTemplate).toContain("{{nftInfo}}"); + expect(floorSweepTemplate).toContain("collectionAddress"); + expect(floorSweepTemplate).toContain("quantity"); + expect(floorSweepTemplate).toContain("maxPricePerNft"); + }); + + it("should contain required placeholders in marketStatsTemplate", () => { + expect(marketStatsTemplate).toContain("{{recentMessages}}"); + expect(marketStatsTemplate).toContain("{{nftInfo}}"); + expect(marketStatsTemplate).toContain("collectionAddress"); + expect(marketStatsTemplate).toContain("timePeriod"); + expect(marketStatsTemplate).toContain("statType"); + }); + + it("should contain required placeholders in socialAnalyticsTemplate", () => { + expect(socialAnalyticsTemplate).toContain("{{recentMessages}}"); + expect(socialAnalyticsTemplate).toContain("{{nftInfo}}"); + expect(socialAnalyticsTemplate).toContain("collectionAddress"); + expect(socialAnalyticsTemplate).toContain("platform"); + expect(socialAnalyticsTemplate).toContain("metricType"); + }); + }); +}); diff --git a/packages/plugin-nft-collections/src/types.ts b/packages/plugin-nft-collections/src/types.ts new file mode 100644 index 00000000000..0b1a3939680 --- /dev/null +++ b/packages/plugin-nft-collections/src/types.ts @@ -0,0 +1,323 @@ +import { Service, ServiceType } from "@elizaos/core"; + +declare module "@elizaos/core" { + interface ServiceTypeMap { + nft: Service & NFTService; + nft_market_intelligence: Service & MarketIntelligenceService; + nft_social_analytics: Service & SocialAnalyticsService; + } +} + +export interface NFTService { + getTopCollections(): Promise; + getMarketStats(): Promise; + getCollectionActivity(collectionAddress: string): Promise; + getCollectionTokens(collectionAddress: string): Promise; + getCollectionAttributes(collectionAddress: string): Promise; + getFloorListings(options: { + collection: string; + limit: number; + sortBy: "price" | "rarity"; + }): Promise< + Array<{ + tokenId: string; + price: number; + seller: string; + marketplace: string; + }> + >; + executeBuy(options: { + listings: Array<{ + tokenId: string; + price: number; + seller: string; + marketplace: string; + }>; + taker: string; + }): Promise<{ + path: string; + steps: Array<{ + action: string; + status: string; + }>; + }>; + createListing(options: { + tokenId: string; + collectionAddress: string; + price: number; + expirationTime?: number; // Unix timestamp + marketplace: "ikigailabs"; + currency?: string; // Default to ETH + quantity?: number; // Default to 1 for ERC721 + }): Promise<{ + listingId: string; + status: string; + transactionHash?: string; + marketplaceUrl: string; + }>; + cancelListing(options: { + listingId: string; + marketplace: "ikigailabs"; + }): Promise<{ + status: string; + transactionHash?: string; + }>; + getOwnedNFTs(owner: string): Promise< + Array<{ + tokenId: string; + collectionAddress: string; + name: string; + imageUrl?: string; + attributes?: Record; + }> + >; +} + +export interface NFTKnowledge { + mentionsCollection: boolean; + mentionsFloorPrice: boolean; + mentionsVolume: boolean; + mentionsRarity: boolean; + mentionsMarketTrends: boolean; + mentionsTraders: boolean; + mentionsSentiment: boolean; + mentionsMarketCap: boolean; + mentionsArtist: boolean; + mentionsOnChainData: boolean; + mentionsNews: boolean; + mentionsSocial: boolean; + mentionsContract: boolean; +} + +export interface MarketIntelligenceService { + getMarketIntelligence( + collectionAddress: string + ): Promise; + getTraitAnalytics(collectionAddress: string): Promise; + detectWashTrading(collectionAddress: string): Promise<{ + suspiciousAddresses: string[]; + suspiciousTransactions: Array<{ + hash: string; + from: string; + to: string; + price: number; + confidence: number; + }>; + }>; + getWhaleActivity(collectionAddress: string): Promise<{ + whales: Array<{ + address: string; + holdings: number; + avgHoldingTime: number; + tradingVolume: number; + lastTrade: number; + }>; + impact: { + priceImpact: number; + volumeShare: number; + holdingsShare: number; + }; + }>; + getLiquidityAnalysis(collectionAddress: string): Promise<{ + depth: Array<{ + price: number; + quantity: number; + totalValue: number; + }>; + metrics: { + totalLiquidity: number; + averageSpread: number; + volatility24h: number; + }; + }>; +} + +export interface SocialAnalyticsService { + getSocialMetrics(collectionAddress: string): Promise; + getNews(collectionAddress: string): Promise; + getCommunityMetrics( + collectionAddress: string, + discordId?: string, + telegramId?: string + ): Promise; + analyzeSentiment(collectionAddress: string): Promise<{ + overall: number; + breakdown: { + positive: number; + neutral: number; + negative: number; + }; + trends: Array<{ + topic: string; + sentiment: number; + volume: number; + }>; + }>; + trackSocialPerformance(collectionAddress: string): Promise<{ + metrics: { + reach: number; + engagement: number; + influence: number; + }; + trends: Array<{ + platform: string; + metric: string; + values: number[]; + }>; + }>; +} + +export interface NFTCollection { + address: string; + name: string; + symbol: string; + description?: string; + imageUrl?: string; + floorPrice: number; + volume24h: number; + marketCap: number; + holders: number; +} + +export interface MarketStats { + totalVolume24h: number; + totalMarketCap: number; + totalCollections: number; + totalHolders: number; + averageFloorPrice: number; +} + +export interface MarketIntelligence { + priceHistory: Array<{ + timestamp: number; + price: number; + volume: number; + }>; + washTradingMetrics: { + suspiciousVolume24h: number; + suspiciousTransactions24h: number; + washTradingScore: number; + }; + marketplaceActivity: { + [marketplace: string]: { + volume24h: number; + trades24h: number; + marketShare: number; + }; + }; + whaleActivity: Array<{ + address: string; + type: "buy" | "sell"; + amount: number; + timestamp: number; + }>; + liquidityMetrics: { + depth: Array<{ + price: number; + quantity: number; + }>; + bidAskSpread: number; + bestBid: number; + bestAsk: number; + }; +} + +export interface TraitAnalytics { + distribution: { + [trait: string]: { + [value: string]: number; + }; + }; + rarityScores: { + [tokenId: string]: number; + }; + combinations: { + total: number; + unique: number; + rarest: Array<{ + traits: { [key: string]: string }; + count: number; + }>; + }; + priceByRarity: Array<{ + rarityRange: [number, number]; + avgPrice: number; + volume: number; + }>; +} + +export 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; + content: string; + author: string; + timestamp: number; + reach: number; + }>; + influencers: Array<{ + address: string; + platform: string; + followers: number; + engagement: number; + sentiment: number; + }>; + trending: boolean; +} + +export interface NewsItem { + title: string; + source: string; + url: string; + timestamp: Date; + sentiment: "positive" | "negative" | "neutral"; + relevance: number; +} + +export interface CommunityMetrics { + discord: { + members: number; + activity: { + messagesPerDay: number; + activeUsers: number; + growthRate: number; + }; + channels: Array<{ + name: string; + members: number; + activity: number; + }>; + } | null; + telegram: { + members: number; + activity: { + messagesPerDay: number; + activeUsers: number; + growthRate: number; + }; + } | null; + totalMembers: number; + growthRate: number; + engagement: { + activeUsers: number; + messagesPerDay: number; + topChannels: Array<{ + platform: string; + name: string; + activity: number; + }>; + }; +} diff --git a/packages/plugin-nft-collections/src/utils/error-handler.ts b/packages/plugin-nft-collections/src/utils/error-handler.ts new file mode 100644 index 00000000000..2c9298290eb --- /dev/null +++ b/packages/plugin-nft-collections/src/utils/error-handler.ts @@ -0,0 +1,280 @@ +import { z } from "zod"; +import { v4 as uuidv4 } from "uuid"; + +// Comprehensive Error Types +export enum ErrorType { + VALIDATION = "VALIDATION", + NETWORK = "NETWORK", + RATE_LIMIT = "RATE_LIMIT", + API = "API", + INTERNAL = "INTERNAL", + AUTHENTICATION = "AUTHENTICATION", + PERMISSION = "PERMISSION", +} + +// Expanded Error Codes +export enum ErrorCode { + // Validation Errors + INVALID_ADDRESS = "INVALID_ADDRESS", + INVALID_TOKEN_ID = "INVALID_TOKEN_ID", + INVALID_PRICE = "INVALID_PRICE", + INVALID_DATA = "INVALID_DATA", + + // Network Errors + REQUEST_TIMEOUT = "REQUEST_TIMEOUT", + NETWORK_ERROR = "NETWORK_ERROR", + DNS_RESOLUTION_ERROR = "DNS_RESOLUTION_ERROR", + + // Rate Limit Errors + RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED", + + // API Errors + API_ERROR = "API_ERROR", + API_KEY_INVALID = "API_KEY_INVALID", + API_RESPONSE_INVALID = "API_RESPONSE_INVALID", + UNSUPPORTED_API_VERSION = "UNSUPPORTED_API_VERSION", + + // Authentication Errors + UNAUTHORIZED = "UNAUTHORIZED", + TOKEN_EXPIRED = "TOKEN_EXPIRED", + + // Permission Errors + INSUFFICIENT_PERMISSIONS = "INSUFFICIENT_PERMISSIONS", + + // Internal Errors + INTERNAL_ERROR = "INTERNAL_ERROR", + CACHE_ERROR = "CACHE_ERROR", + DEPENDENCY_ERROR = "DEPENDENCY_ERROR", +} + +// Enhanced Error Schema +const ErrorSchema = z.object({ + id: z.string().uuid(), + type: z.nativeEnum(ErrorType), + code: z.nativeEnum(ErrorCode), + message: z.string(), + details: z.record(z.unknown()).optional(), + timestamp: z.date(), + retryable: z.boolean(), + severity: z.enum(["LOW", "MEDIUM", "HIGH", "CRITICAL"]).default("MEDIUM"), + correlationId: z.string().optional(), +}); + +export type NFTError = z.infer; + +// Advanced Error Factory +export class NFTErrorFactory { + static create( + type: ErrorType, + code: ErrorCode, + message: string, + options: { + details?: Record; + retryable?: boolean; + severity?: "LOW" | "MEDIUM" | "HIGH" | "CRITICAL"; + correlationId?: string; + } = {} + ): NFTError { + return ErrorSchema.parse({ + id: uuidv4(), + type, + code, + message, + details: options.details, + timestamp: new Date(), + retryable: options.retryable ?? false, + severity: options.severity ?? "MEDIUM", + correlationId: options.correlationId, + }); + } + + static fromError( + error: unknown, + defaultType: ErrorType = ErrorType.INTERNAL + ): NFTError { + if (error instanceof Error) { + return this.create( + defaultType, + ErrorCode.INTERNAL_ERROR, + error.message, + { + details: { + stack: error.stack, + }, + retryable: false, + severity: "HIGH", + } + ); + } + return this.create( + defaultType, + ErrorCode.INTERNAL_ERROR, + "Unknown error occurred", + { + details: { error }, + severity: "CRITICAL", + } + ); + } +} + +// Enhanced Error Handler with Advanced Features +export class ErrorHandler { + private static instance: ErrorHandler; + private errorCallbacks: Array<(error: NFTError) => void> = []; + private telemetryCallbacks: Array<(error: NFTError) => void> = []; + + private constructor() {} + + static getInstance(): ErrorHandler { + if (!ErrorHandler.instance) { + ErrorHandler.instance = new ErrorHandler(); + } + return ErrorHandler.instance; + } + + registerErrorCallback(callback: (error: NFTError) => void): void { + this.errorCallbacks.push(callback); + } + + registerTelemetryCallback(callback: (error: NFTError) => void): void { + this.telemetryCallbacks.push(callback); + } + + handleError(error: NFTError): void { + // Advanced logging + console.error( + JSON.stringify( + { + errorId: error.id, + type: error.type, + code: error.code, + message: error.message, + severity: error.severity, + timestamp: error.timestamp, + }, + null, + 2 + ) + ); + + // Execute registered error callbacks + this.errorCallbacks.forEach((callback) => { + try { + callback(error); + } catch (callbackError) { + console.error("Error in error callback:", callbackError); + } + }); + + // Send to telemetry + this.telemetryCallbacks.forEach((callback) => { + try { + callback(error); + } catch (callbackError) { + console.error("Error in telemetry callback:", callbackError); + } + }); + + // Specialized error handling + this.routeErrorHandling(error); + } + + private routeErrorHandling(error: NFTError): void { + switch (error.type) { + case ErrorType.RATE_LIMIT: + this.handleRateLimitError(error); + break; + case ErrorType.NETWORK: + this.handleNetworkError(error); + break; + case ErrorType.API: + this.handleAPIError(error); + break; + case ErrorType.AUTHENTICATION: + this.handleAuthenticationError(error); + break; + default: + break; + } + } + + private handleRateLimitError(error: NFTError): void { + if (error.retryable) { + const retryDelay = this.calculateExponentialBackoff(error); + console.log(`Rate limit error. Retrying in ${retryDelay}ms`); + } + } + + private handleNetworkError(error: NFTError): void { + if (error.retryable) { + const retryDelay = this.calculateExponentialBackoff(error); + console.log(`Network error. Retrying in ${retryDelay}ms`); + } + } + + private handleAPIError(error: NFTError): void { + switch (error.code) { + case ErrorCode.API_KEY_INVALID: + console.error("Critical: Invalid API key detected"); + break; + case ErrorCode.UNSUPPORTED_API_VERSION: + console.error("API version no longer supported"); + break; + } + } + + private handleAuthenticationError(error: NFTError): void { + switch (error.code) { + case ErrorCode.TOKEN_EXPIRED: + console.log("Attempting token refresh"); + break; + case ErrorCode.UNAUTHORIZED: + console.error("Access denied"); + break; + } + } + + private calculateExponentialBackoff( + error: NFTError, + baseDelay: number = 1000, + maxDelay: number = 30000 + ): number { + // Simulate retry attempt tracking + const attempt = (error.details?.retryAttempt as number) || 0; + return Math.min(baseDelay * Math.pow(2, attempt), maxDelay); + } +} + +// Utility Functions +export function isRetryableError(error: NFTError): boolean { + return error.retryable && error.severity !== "CRITICAL"; +} + +export function shouldRetry( + error: NFTError, + attempt: number, + maxRetries: number = 3 +): boolean { + return isRetryableError(error) && attempt < maxRetries; +} + +// Example Usage +/* +try { + // Your code here +} catch (error) { + const nftError = NFTErrorFactory.create( + ErrorType.API, + ErrorCode.API_ERROR, + 'Detailed API request failure', + { + details: { originalError: error }, + retryable: true, + severity: 'HIGH', + correlationId: 'unique-request-id' + } + ); + ErrorHandler.getInstance().handleError(nftError); +} +*/ diff --git a/packages/plugin-nft-collections/src/utils/performance.ts b/packages/plugin-nft-collections/src/utils/performance.ts new file mode 100644 index 00000000000..22c2a97c249 --- /dev/null +++ b/packages/plugin-nft-collections/src/utils/performance.ts @@ -0,0 +1,251 @@ +import { EventEmitter } from "events"; + +export interface PerformanceMetric { + operation: string; + duration: number; + timestamp: Date; + success: boolean; + metadata?: Record; +} + +export interface PerformanceAlert { + type: "LATENCY" | "ERROR_RATE" | "THROUGHPUT"; + threshold: number; + current: number; + operation: string; + timestamp: Date; +} + +export interface PerformanceConfig { + maxMetrics?: number; + alertThresholds?: { + latency?: number; + errorRate?: number; + throughput?: number; + }; + logFunction?: (message: string, level?: "info" | "warn" | "error") => void; +} + +export class PerformanceMonitor extends EventEmitter { + private static instance: PerformanceMonitor; + private metrics: PerformanceMetric[] = []; + private config: Required = { + maxMetrics: 1000, + alertThresholds: { + latency: 2000, // 2 seconds + errorRate: 0.1, // 10% + throughput: 10, // requests per second + }, + logFunction: console.log, + }; + + private constructor(config?: PerformanceConfig) { + super(); + this.configure(config); + this.startPeriodicCheck(); + } + + static getInstance(config?: PerformanceConfig): PerformanceMonitor { + if (!PerformanceMonitor.instance) { + PerformanceMonitor.instance = new PerformanceMonitor(config); + } + return PerformanceMonitor.instance; + } + + // Configure performance monitor + configure(config?: PerformanceConfig): void { + if (config) { + this.config = { + maxMetrics: config.maxMetrics ?? this.config.maxMetrics, + alertThresholds: { + ...this.config.alertThresholds, + ...config.alertThresholds, + }, + logFunction: config.logFunction ?? this.config.logFunction, + }; + } + } + + // Record a performance metric with improved error handling + recordMetric(metric: Omit): void { + try { + const fullMetric = { + ...metric, + timestamp: new Date(), + }; + + this.metrics.push(fullMetric); + if (this.metrics.length > this.config.maxMetrics) { + this.metrics.shift(); + } + + this.checkThresholds(fullMetric); + } catch (error) { + this.config.logFunction( + `Error recording metric: ${error}`, + "error" + ); + } + } + + // Start measuring operation duration with error tracking + startOperation( + operation: string, + metadata?: Record + ): () => void { + const startTime = performance.now(); + return () => { + try { + const duration = performance.now() - startTime; + this.recordMetric({ + operation, + duration, + success: true, + metadata, + }); + } catch (error) { + this.config.logFunction( + `Error in operation tracking: ${error}`, + "error" + ); + } + }; + } + + // Get average latency for an operation + getAverageLatency(operation: string, timeWindowMs: number = 60000): number { + const relevantMetrics = this.getRecentMetrics(operation, timeWindowMs); + if (relevantMetrics.length === 0) return 0; + + const totalDuration = relevantMetrics.reduce( + (sum, metric) => sum + metric.duration, + 0 + ); + return totalDuration / relevantMetrics.length; + } + + // Get error rate for an operation + getErrorRate(operation: string, timeWindowMs: number = 60000): number { + const relevantMetrics = this.getRecentMetrics(operation, timeWindowMs); + if (relevantMetrics.length === 0) return 0; + + const errorCount = relevantMetrics.filter( + (metric) => !metric.success + ).length; + return errorCount / relevantMetrics.length; + } + + // Get throughput (operations per second) + getThroughput(operation: string, timeWindowMs: number = 60000): number { + const relevantMetrics = this.getRecentMetrics(operation, timeWindowMs); + return (relevantMetrics.length / timeWindowMs) * 1000; + } + + // Get performance summary + getPerformanceSummary(timeWindowMs: number = 60000): Record< + string, + { + averageLatency: number; + errorRate: number; + throughput: number; + } + > { + const operations = new Set(this.metrics.map((m) => m.operation)); + const summary: Record = {}; + + for (const operation of operations) { + summary[operation] = { + averageLatency: this.getAverageLatency(operation, timeWindowMs), + errorRate: this.getErrorRate(operation, timeWindowMs), + throughput: this.getThroughput(operation, timeWindowMs), + }; + } + + return summary; + } + + private getRecentMetrics( + operation: string, + timeWindowMs: number + ): PerformanceMetric[] { + const now = new Date(); + const windowStart = new Date(now.getTime() - timeWindowMs); + return this.metrics.filter( + (metric) => + metric.operation === operation && + metric.timestamp >= windowStart + ); + } + + private checkThresholds(metric: PerformanceMetric): void { + const { alertThresholds } = this.config; + + try { + // Latency check + if (metric.duration > alertThresholds.latency) { + this.emitAlert({ + type: "LATENCY", + threshold: alertThresholds.latency, + current: metric.duration, + operation: metric.operation, + timestamp: new Date(), + }); + } + + // Error rate check + const errorRate = this.getErrorRate(metric.operation); + if (errorRate > alertThresholds.errorRate) { + this.emitAlert({ + type: "ERROR_RATE", + threshold: alertThresholds.errorRate, + current: errorRate, + operation: metric.operation, + timestamp: new Date(), + }); + } + + // Throughput check + const throughput = this.getThroughput(metric.operation); + if (throughput > alertThresholds.throughput) { + this.emitAlert({ + type: "THROUGHPUT", + threshold: alertThresholds.throughput, + current: throughput, + operation: metric.operation, + timestamp: new Date(), + }); + } + } catch (error) { + this.config.logFunction( + `Error in threshold checking: ${error}`, + "error" + ); + } + } + + private emitAlert(alert: PerformanceAlert): void { + this.emit("alert", alert); + } + + private startPeriodicCheck(): void { + setInterval(() => { + const summary = this.getPerformanceSummary(); + this.emit("performance-summary", summary); + }, 60000); // Check every minute + } +} + +// Enhanced usage example +/* +const monitor = PerformanceMonitor.getInstance({ + maxMetrics: 500, + alertThresholds: { + latency: 1500, // more aggressive latency threshold + errorRate: 0.05 // tighter error rate + }, + logFunction: (msg, level) => { + // Custom logging, e.g., to a file or monitoring service + console[level ?? 'log'](msg); + } +}); +*/ diff --git a/packages/plugin-nft-collections/src/utils/response-enhancer.ts b/packages/plugin-nft-collections/src/utils/response-enhancer.ts new file mode 100644 index 00000000000..dc710fa8cec --- /dev/null +++ b/packages/plugin-nft-collections/src/utils/response-enhancer.ts @@ -0,0 +1,82 @@ +import { State } from "@elizaos/core"; +import { NFTKnowledge } from "../types"; + +type EnhancementConfig = { + maxAdditionalPrompts?: number; + separator?: string; +}; + +export function enhanceResponse( + response: string, + state: State, + config: EnhancementConfig = {} +): string { + const { maxAdditionalPrompts = 3, separator = " " } = config; + + const nftKnowledge = state.nftKnowledge as NFTKnowledge; + if (!nftKnowledge) return response; + + const enhancements = [ + { + condition: nftKnowledge.mentionsCollection, + prompt: "Would you like to know more about specific NFT collections?", + }, + { + condition: nftKnowledge.mentionsFloorPrice, + prompt: "I can provide information on floor prices for popular collections.", + }, + { + condition: nftKnowledge.mentionsVolume, + prompt: "I can share recent trading volume data for NFT collections.", + }, + { + condition: nftKnowledge.mentionsRarity, + prompt: "I can explain rarity factors in NFT collections if you're interested.", + }, + { + condition: nftKnowledge.mentionsMarketTrends, + prompt: "I can show you the latest market trends and price movements.", + }, + { + condition: nftKnowledge.mentionsTraders, + prompt: "Would you like to see recent whale activity and notable trades?", + }, + { + condition: nftKnowledge.mentionsSentiment, + prompt: "I can provide current market sentiment analysis and trader mood indicators.", + }, + { + condition: nftKnowledge.mentionsMarketCap, + prompt: "I can show you market cap rankings and valuation metrics.", + }, + { + condition: nftKnowledge.mentionsArtist, + prompt: "I can provide detailed information about the artist, their background, and previous collections.", + }, + { + condition: nftKnowledge.mentionsOnChainData, + prompt: "I can show you detailed on-chain analytics including holder distribution and trading patterns.", + }, + { + condition: nftKnowledge.mentionsNews, + prompt: "I can share the latest news and announcements about this collection.", + }, + { + condition: nftKnowledge.mentionsSocial, + prompt: "I can provide social media metrics and community engagement data.", + }, + { + condition: nftKnowledge.mentionsContract, + prompt: "I can show you contract details including standards, royalties, and verification status.", + }, + ]; + + const additionalPrompts = enhancements + .filter((enhancement) => enhancement.condition) + .slice(0, maxAdditionalPrompts) + .map((enhancement) => enhancement.prompt); + + return additionalPrompts.length > 0 + ? `${response}${separator}${additionalPrompts.join(separator)}` + : response; +} diff --git a/packages/plugin-nft-collections/src/utils/validation.ts b/packages/plugin-nft-collections/src/utils/validation.ts new file mode 100644 index 00000000000..8c0db0daea6 --- /dev/null +++ b/packages/plugin-nft-collections/src/utils/validation.ts @@ -0,0 +1,211 @@ +import { z } from "zod"; + +// Enhanced Ethereum address validation +function isAddress(address: string): boolean { + // More comprehensive Ethereum address validation + if (typeof address !== "string") return false; + + // Check for 0x prefix and exactly 42 characters (0x + 40 hex chars) + if (!/^0x[a-fA-F0-9]{40}$/.test(address)) return false; + + // Additional checksum validation (case-sensitive) + try { + return address === toChecksumAddress(address); + } catch { + return false; + } +} + +// Implement checksum address conversion +function toChecksumAddress(address: string): string { + address = address.toLowerCase().replace("0x", ""); + const hash = hashCode(address); + + return ( + "0x" + + address + .split("") + .map((char, index) => + parseInt(hash[index], 16) >= 8 ? char.toUpperCase() : char + ) + .join("") + ); +} + +// Simple hash function for checksum +function hashCode(address: string): string { + let hash = 0; + for (let i = 0; i < address.length; i++) { + const char = address.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; // Convert to 32-bit integer + } + return Math.abs(hash).toString(16).padStart(40, "0"); +} + +function getAddress(address: string): string { + // Normalize and validate address + if (!isAddress(address)) { + throw new Error("Invalid Ethereum address"); + } + return toChecksumAddress(address); +} + +// Environment Variable Validation Schema +export const EnvConfigSchema = z.object({ + TWITTER_API_KEY: z.string().min(1, "Twitter API key is required"), + DUNE_API_KEY: z.string().min(1, "Dune API key is required"), + OPENSEA_API_KEY: z.string().min(1, "OpenSea API key is required"), + RESERVOIR_API_KEY: z.string().min(1, "Reservoir API key is required"), +}); + +// Function to validate environment variables +export function validateEnvironmentVariables( + env: Record +) { + try { + return EnvConfigSchema.parse(env); + } catch (error) { + if (error instanceof z.ZodError) { + const errorMessages = error.errors + .map((err) => err.message) + .join(", "); + throw new Error( + `Environment Variable Validation Failed: ${errorMessages}` + ); + } + throw error; + } +} + +// Enhanced NFT Collection Schema with strict validation +export const NFTCollectionSchema = z.object({ + address: z.string().refine((val) => isAddress(val), { + message: "Invalid Ethereum address", + }), + name: z.string().min(1).max(100), + symbol: z.string().min(1).max(10).optional(), + description: z.string().max(5000).optional(), + imageUrl: z.string().url().optional(), + externalUrl: z.string().url().optional(), + twitterUsername: z + .string() + .regex(/^[A-Za-z0-9_]{1,15}$/) + .optional(), + discordUrl: z.string().url().optional(), + verified: z.boolean().default(false), + featured: z.boolean().default(false), + createdAt: z.string().datetime().optional(), + floorPrice: z.number().min(0).optional(), + volume24h: z.number().min(0).optional(), + marketCap: z.number().min(0).optional(), + holders: z.number().int().min(0).optional(), + totalSupply: z.number().int().min(0).optional(), + twitterFollowers: z.number().int().min(0).optional(), + discordMembers: z.number().int().min(0).optional(), + supportedMarketplaces: z.array(z.string()).optional(), + hasRoyalties: z.boolean().optional(), + royaltyPercentage: z.number().min(0).max(100).optional(), + traits: z.record(z.string(), z.array(z.string())).optional(), + categories: z.array(z.string()).optional(), + lastUpdate: z.string().datetime().optional(), +}); + +// Market Data Schema +export const MarketDataSchema = z.object({ + floorPrice: z.number().min(0), + bestOffer: z.number().min(0).optional(), + volume24h: z.number().min(0), + volume7d: z.number().min(0).optional(), + volume30d: z.number().min(0).optional(), + marketCap: z.number().min(0), + holders: z.number().int().min(0), + sales24h: z.number().int().min(0).optional(), + averagePrice24h: z.number().min(0).optional(), + lastUpdate: z.string().datetime(), +}); + +// Social Metrics Schema +export const SocialMetricsSchema = z.object({ + twitterFollowers: z.number().int().min(0).optional(), + twitterEngagement: z.number().min(0).optional(), + discordMembers: z.number().int().min(0).optional(), + discordActive: z.number().int().min(0).optional(), + telegramMembers: z.number().int().min(0).optional(), + telegramActive: z.number().int().min(0).optional(), + lastUpdate: z.string().datetime(), +}); + +// Validation Functions +export function validateCollection(data: unknown) { + return NFTCollectionSchema.parse(data); +} + +export function validateMarketData(data: unknown) { + return MarketDataSchema.parse(data); +} + +export function validateSocialMetrics(data: unknown) { + return SocialMetricsSchema.parse(data); +} + +// Type Inference +export type NFTCollection = z.infer; +export type MarketData = z.infer; +export type SocialMetrics = z.infer; + +// Utility Functions +export function isValidEthereumAddress(address: string): boolean { + return isAddress(address); +} + +export function normalizeAddress(address: string): string { + try { + return getAddress(address); + } catch { + throw new Error("Invalid Ethereum address"); + } +} + +export function validateTokenId( + tokenId: string, + collection: NFTCollection +): boolean { + const numericTokenId = BigInt(tokenId); + if (collection.totalSupply) { + return ( + numericTokenId >= 0n && + numericTokenId < BigInt(collection.totalSupply) + ); + } + return numericTokenId >= 0n; +} + +export function validatePriceRange(price: number): boolean { + return price >= 0 && price <= 1000000; // Reasonable price range in ETH +} + +export function sanitizeCollectionData(data: unknown): Partial { + try { + return NFTCollectionSchema.parse(data); + } catch (error) { + // Return only the valid fields + const partial = {}; + const validFields = Object.entries( + data as Record + ).filter(([key, value]) => { + try { + NFTCollectionSchema.shape[key].parse(value); + return true; + } catch { + return false; + } + }); + + for (const [key, value] of validFields) { + partial[key] = value; + } + + return partial; + } +} diff --git a/packages/plugin-nft-collections/tsconfig.json b/packages/plugin-nft-collections/tsconfig.json new file mode 100644 index 00000000000..fc61771fb21 --- /dev/null +++ b/packages/plugin-nft-collections/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "module": "ESNext", + "target": "ESNext", + "moduleResolution": "Node", + "esModuleInterop": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file diff --git a/packages/plugin-nft-collections/vitest.config.ts b/packages/plugin-nft-collections/vitest.config.ts new file mode 100644 index 00000000000..47c872fae34 --- /dev/null +++ b/packages/plugin-nft-collections/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "node", + include: ["src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"], + coverage: { + reporter: ["text", "json", "html"], + include: ["src/**/*.ts"], + exclude: ["src/**/*.{test,spec}.ts"], + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a4da42f2d37..4d2aa79f4e7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,15 +21,24 @@ importers: '@deepgram/sdk': specifier: ^3.9.0 version: 3.9.0(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@6.0.5) + '@tensorflow/tfjs-node': + specifier: ^4.22.0 + version: 4.22.0(encoding@0.1.13)(seedrandom@3.0.5) '@vitest/eslint-plugin': specifier: 1.0.1 - version: 1.0.1(@typescript-eslint/utils@8.19.1(eslint@9.16.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.16.0(jiti@2.4.2))(typescript@5.6.3)(vitest@2.1.5(@types/node@22.10.5)(jsdom@25.0.1(bufferutil@4.0.9)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.5))(terser@5.37.0)) + version: 1.0.1(@typescript-eslint/utils@8.19.1(eslint@9.16.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.16.0(jiti@2.4.2))(typescript@5.6.3)(vitest@2.1.5(@types/node@20.17.9)(jsdom@25.0.1(bufferutil@4.0.9)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.5))(terser@5.37.0)) amqplib: specifier: 0.10.5 version: 0.10.5 + axios: + specifier: ^1.7.9 + version: 1.7.9 csv-parse: specifier: 5.6.0 version: 5.6.0 + langdetect: + specifier: ^0.2.1 + version: 0.2.1 ollama-ai-provider: specifier: 0.16.1 version: 0.16.1(zod@3.24.1) @@ -48,7 +57,7 @@ importers: devDependencies: '@commitlint/cli': specifier: 18.6.1 - version: 18.6.1(@types/node@22.10.5)(typescript@5.6.3) + version: 18.6.1(@types/node@20.17.9)(typescript@5.6.3) '@commitlint/config-conventional': specifier: 18.6.3 version: 18.6.3 @@ -78,7 +87,7 @@ importers: version: 9.1.7 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.10.5)(babel-plugin-macros@3.1.0) + version: 29.7.0(@types/node@20.17.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.10.7(@swc/helpers@0.5.15))(@types/node@20.17.9)(typescript@5.6.3)) lerna: specifier: 8.1.5 version: 8.1.5(@swc/core@1.10.7(@swc/helpers@0.5.15))(babel-plugin-macros@3.1.0)(encoding@0.1.13) @@ -90,7 +99,7 @@ importers: version: 3.4.1 ts-jest: specifier: ^29.1.1 - version: 29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@22.10.5)(babel-plugin-macros@3.1.0))(typescript@5.6.3) + version: 29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@20.17.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.10.7(@swc/helpers@0.5.15))(@types/node@20.17.9)(typescript@5.6.3)))(typescript@5.6.3) turbo: specifier: 2.3.3 version: 2.3.3 @@ -105,10 +114,10 @@ importers: version: 2.21.58(bufferutil@4.0.9)(typescript@5.6.3)(utf-8-validate@6.0.5)(zod@3.24.1) vite: specifier: 5.4.11 - version: 5.4.11(@types/node@22.10.5)(terser@5.37.0) + version: 5.4.11(@types/node@20.17.9)(terser@5.37.0) vitest: specifier: 2.1.5 - version: 2.1.5(@types/node@22.10.5)(jsdom@25.0.1(bufferutil@4.0.9)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.5))(terser@5.37.0) + version: 2.1.5(@types/node@20.17.9)(jsdom@25.0.1(bufferutil@4.0.9)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.5))(terser@5.37.0) agent: dependencies: @@ -2077,6 +2086,8 @@ importers: specifier: 7.1.0 version: 7.1.0 + packages/plugin-nft-collections: {} + packages/plugin-nft-generation: dependencies: '@elizaos/core': @@ -6609,6 +6620,10 @@ packages: resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} hasBin: true + '@mapbox/node-pre-gyp@1.0.9': + resolution: {integrity: sha512-aDF3S3rK9Q2gey/WAttUlISduDItz5BU3306M9Eyv6/oS40aMprnopshtlKTykxRNIBEZuRMaZAnbrQ4QtKGyw==} + hasBin: true + '@massalabs/massa-web3@5.1.0': resolution: {integrity: sha512-fKlOjKD+F0JoUxLUUfweugt9MrM6P1F4WT80TdhgZ1yIKqguN0bNYsXzF9Wf6xVzljP/D+u1kwSDAQpZ/PZ8yg==} @@ -9479,6 +9494,46 @@ packages: '@tensor-oss/tensorswap-sdk@4.5.0': resolution: {integrity: sha512-eNM6k1DT5V/GadxSHm8//z2wlLl8/EcA0KFQXKaxRba/2MirNySsoVGxDXO2UdOI4eZMse8f+8Et3P63WWjsIw==} + '@tensorflow/tfjs-backend-cpu@4.22.0': + resolution: {integrity: sha512-1u0FmuLGuRAi8D2c3cocHTASGXOmHc/4OvoVDENJayjYkS119fcTcQf4iHrtLthWyDIPy3JiPhRrZQC9EwnhLw==} + engines: {yarn: '>= 1.3.2'} + peerDependencies: + '@tensorflow/tfjs-core': 4.22.0 + + '@tensorflow/tfjs-backend-webgl@4.22.0': + resolution: {integrity: sha512-H535XtZWnWgNwSzv538czjVlbJebDl5QTMOth4RXr2p/kJ1qSIXE0vZvEtO+5EC9b00SvhplECny2yDewQb/Yg==} + engines: {yarn: '>= 1.3.2'} + peerDependencies: + '@tensorflow/tfjs-core': 4.22.0 + + '@tensorflow/tfjs-converter@4.22.0': + resolution: {integrity: sha512-PT43MGlnzIo+YfbsjM79Lxk9lOq6uUwZuCc8rrp0hfpLjF6Jv8jS84u2jFb+WpUeuF4K33ZDNx8CjiYrGQ2trQ==} + peerDependencies: + '@tensorflow/tfjs-core': 4.22.0 + + '@tensorflow/tfjs-core@4.22.0': + resolution: {integrity: sha512-LEkOyzbknKFoWUwfkr59vSB68DMJ4cjwwHgicXN0DUi3a0Vh1Er3JQqCI1Hl86GGZQvY8ezVrtDIvqR1ZFW55A==} + engines: {yarn: '>= 1.3.2'} + + '@tensorflow/tfjs-data@4.22.0': + resolution: {integrity: sha512-dYmF3LihQIGvtgJrt382hSRH4S0QuAp2w1hXJI2+kOaEqo5HnUPG0k5KA6va+S1yUhx7UBToUKCBHeLHFQRV4w==} + peerDependencies: + '@tensorflow/tfjs-core': 4.22.0 + seedrandom: ^3.0.5 + + '@tensorflow/tfjs-layers@4.22.0': + resolution: {integrity: sha512-lybPj4ZNj9iIAPUj7a8ZW1hg8KQGfqWLlCZDi9eM/oNKCCAgchiyzx8OrYoWmRrB+AM6VNEeIT+2gZKg5ReihA==} + peerDependencies: + '@tensorflow/tfjs-core': 4.22.0 + + '@tensorflow/tfjs-node@4.22.0': + resolution: {integrity: sha512-uHrXeUlfgkMxTZqHkESSV7zSdKdV0LlsBeblqkuKU9nnfxB1pC6DtoyYVaLxznzZy7WQSegjcohxxCjAf6Dc7w==} + engines: {node: '>=8.11.0'} + + '@tensorflow/tfjs@4.22.0': + resolution: {integrity: sha512-0TrIrXs6/b7FLhLVNmfh8Sah6JgjBPH4mZ8JGb7NU6WW+cx00qK5BcAZxw7NCzxj6N8MRAIfHq+oNbPUNG5VAg==} + hasBin: true + '@tinyhttp/content-disposition@2.2.2': resolution: {integrity: sha512-crXw1txzrS36huQOyQGYFvhTeLeG0Si1xu+/l6kXUVYpE0TjFjEZRqTbuadQLfKGZ0jaI+jJoRyqaWwxOSHW2g==} engines: {node: '>=12.20.0'} @@ -9881,6 +9936,12 @@ packages: '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/offscreencanvas@2019.3.0': + resolution: {integrity: sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q==} + + '@types/offscreencanvas@2019.7.3': + resolution: {integrity: sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==} + '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -9941,6 +10002,9 @@ packages: '@types/sax@1.2.7': resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} + '@types/seedrandom@2.4.34': + resolution: {integrity: sha512-ytDiArvrn/3Xk6/vtylys5tlY6eo7Ane0hvcx++TKo6RxQXuVfW0AF/oeWqAj9dN29SyhtawuXstgmPlwNcv/A==} + '@types/semver@7.5.8': resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} @@ -10546,6 +10610,9 @@ packages: '@webassemblyjs/wast-printer@1.14.1': resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + '@webgpu/types@0.1.38': + resolution: {integrity: sha512-7LrhVKz2PRh+DD7+S+PVaFd5HxaWQvoMqBbsV9fNJO1pjUs1P8bM2vQVNfk+3URTqbuTI7gkXi0rfsN0IadoBA==} + '@xtuc/ieee754@1.2.0': resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} @@ -10672,12 +10739,20 @@ packages: resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} engines: {node: '>= 10.0.0'} + adm-zip@0.5.16: + resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==} + engines: {node: '>=12.0'} + aes-js@3.0.0: resolution: {integrity: sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==} aes-js@4.0.0-beta.5: resolution: {integrity: sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==} + agent-base@4.3.0: + resolution: {integrity: sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==} + engines: {node: '>= 4.0.0'} + agent-base@5.1.1: resolution: {integrity: sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==} engines: {node: '>= 6.0.0'} @@ -12232,6 +12307,9 @@ packages: resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==} deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. + core-js@3.29.1: + resolution: {integrity: sha512-+jwgnhg6cQxKYIIjGtAHq2nwUOolo9eoFZ4sHfUH09BLXBgxnH4gA0zEd+t+BO2cNB8idaBtZFcFTRjQJRJmAw==} + core-js@3.40.0: resolution: {integrity: sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==} @@ -14767,6 +14845,10 @@ packages: https-browserify@1.0.0: resolution: {integrity: sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==} + https-proxy-agent@2.2.4: + resolution: {integrity: sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==} + engines: {node: '>= 4.5.0'} + https-proxy-agent@4.0.0: resolution: {integrity: sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==} engines: {node: '>= 6.0.0'} @@ -17086,6 +17168,15 @@ packages: node-fetch-native@1.6.4: resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==} + node-fetch@2.6.13: + resolution: {integrity: sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + node-fetch@2.6.7: resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} engines: {node: 4.x || >=6.0.0} @@ -19297,6 +19388,9 @@ packages: regenerator-runtime@0.11.1: resolution: {integrity: sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==} + regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} @@ -19474,6 +19568,11 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} deprecated: Rimraf versions prior to v4 are no longer supported @@ -22528,7 +22627,7 @@ snapshots: '@acuminous/bitsyntax@0.1.2': dependencies: buffer-more-ints: 1.0.0 - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.0 safe-buffer: 5.1.2 transitivePeerDependencies: - supports-color @@ -23742,7 +23841,7 @@ snapshots: '@babel/traverse': 7.26.5 '@babel/types': 7.26.5 convert-source-map: 2.0.0 - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.0 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -24522,7 +24621,7 @@ snapshots: '@babel/parser': 7.26.5 '@babel/template': 7.25.9 '@babel/types': 7.26.5 - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.0 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -24675,7 +24774,7 @@ snapshots: dependencies: '@scure/bip32': 1.6.1 abitype: 1.0.8(typescript@5.6.3)(zod@3.24.1) - axios: 1.7.9(debug@4.4.0) + axios: 1.7.9 axios-mock-adapter: 1.22.0(axios@1.7.9) axios-retry: 4.5.0(axios@1.7.9) bip32: 4.0.0 @@ -24703,11 +24802,11 @@ snapshots: '@colors/colors@1.5.0': optional: true - '@commitlint/cli@18.6.1(@types/node@22.10.5)(typescript@5.6.3)': + '@commitlint/cli@18.6.1(@types/node@20.17.9)(typescript@5.6.3)': dependencies: '@commitlint/format': 18.6.1 '@commitlint/lint': 18.6.1 - '@commitlint/load': 18.6.1(@types/node@22.10.5)(typescript@5.6.3) + '@commitlint/load': 18.6.1(@types/node@20.17.9)(typescript@5.6.3) '@commitlint/read': 18.6.1 '@commitlint/types': 18.6.1 execa: 5.1.1 @@ -24757,7 +24856,7 @@ snapshots: '@commitlint/rules': 18.6.1 '@commitlint/types': 18.6.1 - '@commitlint/load@18.6.1(@types/node@22.10.5)(typescript@5.6.3)': + '@commitlint/load@18.6.1(@types/node@20.17.9)(typescript@5.6.3)': dependencies: '@commitlint/config-validator': 18.6.1 '@commitlint/execute-rule': 18.6.1 @@ -24765,7 +24864,7 @@ snapshots: '@commitlint/types': 18.6.1 chalk: 4.1.2 cosmiconfig: 8.3.6(typescript@5.6.3) - cosmiconfig-typescript-loader: 5.1.0(@types/node@22.10.5)(cosmiconfig@8.3.6(typescript@5.6.3))(typescript@5.6.3) + cosmiconfig-typescript-loader: 5.1.0(@types/node@20.17.9)(cosmiconfig@8.3.6(typescript@5.6.3))(typescript@5.6.3) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 @@ -26954,7 +27053,7 @@ snapshots: '@eslint/config-array@0.19.1': dependencies: '@eslint/object-schema': 2.1.5 - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.0 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -26984,7 +27083,7 @@ snapshots: '@eslint/eslintrc@3.2.0': dependencies: ajv: 6.12.6 - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.0 espree: 10.3.0 globals: 14.0.0 ignore: 5.3.2 @@ -28023,6 +28122,41 @@ snapshots: - supports-color - ts-node + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.10.7(@swc/helpers@0.5.15))(@types/node@20.17.9)(typescript@5.6.3))': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.17.9 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@20.17.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.10.7(@swc/helpers@0.5.15))(@types/node@20.17.9)(typescript@5.6.3)) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.10.7(@swc/helpers@0.5.15))(@types/node@20.17.9)(typescript@5.7.3))': dependencies: '@jest/console': 29.7.0 @@ -28842,6 +28976,21 @@ snapshots: - supports-color optional: true + '@mapbox/node-pre-gyp@1.0.9(encoding@0.1.13)': + dependencies: + detect-libc: 2.0.3 + https-proxy-agent: 5.0.1 + make-dir: 3.1.0 + node-fetch: 2.7.0(encoding@0.1.13) + nopt: 5.0.0 + npmlog: 5.0.1 + rimraf: 3.0.2 + semver: 7.6.3 + tar: 6.2.1 + transitivePeerDependencies: + - encoding + - supports-color + '@massalabs/massa-web3@5.1.0': dependencies: '@noble/ed25519': 1.7.3 @@ -33319,6 +33468,82 @@ snapshots: - typescript - utf-8-validate + '@tensorflow/tfjs-backend-cpu@4.22.0(@tensorflow/tfjs-core@4.22.0(encoding@0.1.13))': + dependencies: + '@tensorflow/tfjs-core': 4.22.0(encoding@0.1.13) + '@types/seedrandom': 2.4.34 + seedrandom: 3.0.5 + + '@tensorflow/tfjs-backend-webgl@4.22.0(@tensorflow/tfjs-core@4.22.0(encoding@0.1.13))': + dependencies: + '@tensorflow/tfjs-backend-cpu': 4.22.0(@tensorflow/tfjs-core@4.22.0(encoding@0.1.13)) + '@tensorflow/tfjs-core': 4.22.0(encoding@0.1.13) + '@types/offscreencanvas': 2019.3.0 + '@types/seedrandom': 2.4.34 + seedrandom: 3.0.5 + + '@tensorflow/tfjs-converter@4.22.0(@tensorflow/tfjs-core@4.22.0(encoding@0.1.13))': + dependencies: + '@tensorflow/tfjs-core': 4.22.0(encoding@0.1.13) + + '@tensorflow/tfjs-core@4.22.0(encoding@0.1.13)': + dependencies: + '@types/long': 4.0.2 + '@types/offscreencanvas': 2019.7.3 + '@types/seedrandom': 2.4.34 + '@webgpu/types': 0.1.38 + long: 4.0.0 + node-fetch: 2.6.13(encoding@0.1.13) + seedrandom: 3.0.5 + transitivePeerDependencies: + - encoding + + '@tensorflow/tfjs-data@4.22.0(@tensorflow/tfjs-core@4.22.0(encoding@0.1.13))(encoding@0.1.13)(seedrandom@3.0.5)': + dependencies: + '@tensorflow/tfjs-core': 4.22.0(encoding@0.1.13) + '@types/node-fetch': 2.6.12 + node-fetch: 2.6.13(encoding@0.1.13) + seedrandom: 3.0.5 + string_decoder: 1.3.0 + transitivePeerDependencies: + - encoding + + '@tensorflow/tfjs-layers@4.22.0(@tensorflow/tfjs-core@4.22.0(encoding@0.1.13))': + dependencies: + '@tensorflow/tfjs-core': 4.22.0(encoding@0.1.13) + + '@tensorflow/tfjs-node@4.22.0(encoding@0.1.13)(seedrandom@3.0.5)': + dependencies: + '@mapbox/node-pre-gyp': 1.0.9(encoding@0.1.13) + '@tensorflow/tfjs': 4.22.0(encoding@0.1.13)(seedrandom@3.0.5) + adm-zip: 0.5.16 + google-protobuf: 3.21.4 + https-proxy-agent: 2.2.4 + progress: 2.0.3 + rimraf: 2.7.1 + tar: 6.2.1 + transitivePeerDependencies: + - encoding + - seedrandom + - supports-color + + '@tensorflow/tfjs@4.22.0(encoding@0.1.13)(seedrandom@3.0.5)': + dependencies: + '@tensorflow/tfjs-backend-cpu': 4.22.0(@tensorflow/tfjs-core@4.22.0(encoding@0.1.13)) + '@tensorflow/tfjs-backend-webgl': 4.22.0(@tensorflow/tfjs-core@4.22.0(encoding@0.1.13)) + '@tensorflow/tfjs-converter': 4.22.0(@tensorflow/tfjs-core@4.22.0(encoding@0.1.13)) + '@tensorflow/tfjs-core': 4.22.0(encoding@0.1.13) + '@tensorflow/tfjs-data': 4.22.0(@tensorflow/tfjs-core@4.22.0(encoding@0.1.13))(encoding@0.1.13)(seedrandom@3.0.5) + '@tensorflow/tfjs-layers': 4.22.0(@tensorflow/tfjs-core@4.22.0(encoding@0.1.13)) + argparse: 1.0.10 + chalk: 4.1.2 + core-js: 3.29.1 + regenerator-runtime: 0.13.11 + yargs: 16.2.0 + transitivePeerDependencies: + - encoding + - seedrandom + '@tinyhttp/content-disposition@2.2.2': {} '@tiplink/api@0.3.1(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(sodium-native@3.4.1)(utf-8-validate@5.0.10)': @@ -33806,6 +34031,10 @@ snapshots: '@types/normalize-package-data@2.4.4': {} + '@types/offscreencanvas@2019.3.0': {} + + '@types/offscreencanvas@2019.7.3': {} + '@types/parse-json@4.0.2': {} '@types/parse5@5.0.3': {} @@ -33874,6 +34103,8 @@ snapshots: dependencies: '@types/node': 20.17.9 + '@types/seedrandom@2.4.34': {} + '@types/semver@7.5.8': {} '@types/send@0.17.4': @@ -34044,7 +34275,7 @@ snapshots: '@typescript-eslint/types': 8.16.0 '@typescript-eslint/typescript-estree': 8.16.0(typescript@5.6.3) '@typescript-eslint/visitor-keys': 8.16.0 - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.0 eslint: 9.16.0(jiti@2.4.2) optionalDependencies: typescript: 5.6.3 @@ -34107,7 +34338,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.16.0(typescript@5.6.3) '@typescript-eslint/utils': 8.16.0(eslint@9.16.0(jiti@2.4.2))(typescript@5.6.3) - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.0 eslint: 9.16.0(jiti@2.4.2) ts-api-utils: 1.4.3(typescript@5.6.3) optionalDependencies: @@ -34163,7 +34394,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.16.0 '@typescript-eslint/visitor-keys': 8.16.0 - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.0 fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -34178,7 +34409,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.19.1 '@typescript-eslint/visitor-keys': 8.19.1 - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.0 fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -34359,13 +34590,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/eslint-plugin@1.0.1(@typescript-eslint/utils@8.19.1(eslint@9.16.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.16.0(jiti@2.4.2))(typescript@5.6.3)(vitest@2.1.5(@types/node@22.10.5)(jsdom@25.0.1(bufferutil@4.0.9)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.5))(terser@5.37.0))': + '@vitest/eslint-plugin@1.0.1(@typescript-eslint/utils@8.19.1(eslint@9.16.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.16.0(jiti@2.4.2))(typescript@5.6.3)(vitest@2.1.5(@types/node@20.17.9)(jsdom@25.0.1(bufferutil@4.0.9)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.5))(terser@5.37.0))': dependencies: eslint: 9.16.0(jiti@2.4.2) optionalDependencies: '@typescript-eslint/utils': 8.19.1(eslint@9.16.0(jiti@2.4.2))(typescript@5.6.3) typescript: 5.6.3 - vitest: 2.1.5(@types/node@22.10.5)(jsdom@25.0.1(bufferutil@4.0.9)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.5))(terser@5.37.0) + vitest: 2.1.5(@types/node@20.17.9)(jsdom@25.0.1(bufferutil@4.0.9)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.5))(terser@5.37.0) '@vitest/expect@0.34.6': dependencies: @@ -34414,6 +34645,14 @@ snapshots: optionalDependencies: vite: 5.4.11(@types/node@22.10.5)(terser@5.37.0) + '@vitest/mocker@2.1.5(vite@5.4.11(@types/node@20.17.9)(terser@5.37.0))': + dependencies: + '@vitest/spy': 2.1.5 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 5.4.11(@types/node@20.17.9)(terser@5.37.0) + '@vitest/mocker@2.1.5(vite@5.4.11(@types/node@22.10.5)(terser@5.37.0))': dependencies: '@vitest/spy': 2.1.5 @@ -35274,6 +35513,8 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 + '@webgpu/types@0.1.38': {} + '@xtuc/ieee754@1.2.0': {} '@xtuc/long@4.2.2': {} @@ -35383,15 +35624,21 @@ snapshots: address@1.2.2: {} + adm-zip@0.5.16: {} + aes-js@3.0.0: {} aes-js@4.0.0-beta.5: {} + agent-base@4.3.0: + dependencies: + es6-promisify: 5.0.0 + agent-base@5.1.1: {} agent-base@6.0.2: dependencies: - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.0 transitivePeerDependencies: - supports-color @@ -35919,7 +36166,7 @@ snapshots: axios-mock-adapter@1.22.0(axios@1.7.9): dependencies: - axios: 1.7.9(debug@4.4.0) + axios: 1.7.9 fast-deep-equal: 3.1.3 is-buffer: 2.0.5 @@ -35930,7 +36177,7 @@ snapshots: axios-retry@4.5.0(axios@1.7.9): dependencies: - axios: 1.7.9(debug@4.4.0) + axios: 1.7.9 is-retry-allowed: 2.2.0 axios@0.21.4: @@ -35947,7 +36194,7 @@ snapshots: axios@0.27.2: dependencies: - follow-redirects: 1.15.9(debug@4.4.0) + follow-redirects: 1.15.9 form-data: 4.0.1 transitivePeerDependencies: - debug @@ -35984,6 +36231,14 @@ snapshots: transitivePeerDependencies: - debug + axios@1.7.9: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.1 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axios@1.7.9(debug@4.4.0): dependencies: follow-redirects: 1.15.9(debug@4.4.0) @@ -37392,6 +37647,8 @@ snapshots: core-js@2.6.12: {} + core-js@3.29.1: {} + core-js@3.40.0: {} core-util-is@1.0.2: {} @@ -37411,9 +37668,9 @@ snapshots: dependencies: layout-base: 2.0.1 - cosmiconfig-typescript-loader@5.1.0(@types/node@22.10.5)(cosmiconfig@8.3.6(typescript@5.6.3))(typescript@5.6.3): + cosmiconfig-typescript-loader@5.1.0(@types/node@20.17.9)(cosmiconfig@8.3.6(typescript@5.6.3))(typescript@5.6.3): dependencies: - '@types/node': 22.10.5 + '@types/node': 20.17.9 cosmiconfig: 8.3.6(typescript@5.6.3) jiti: 1.21.7 typescript: 5.6.3 @@ -37505,6 +37762,21 @@ snapshots: - supports-color - ts-node + create-jest@29.7.0(@types/node@20.17.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.10.7(@swc/helpers@0.5.15))(@types/node@20.17.9)(typescript@5.6.3)): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@20.17.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.10.7(@swc/helpers@0.5.15))(@types/node@20.17.9)(typescript@5.6.3)) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + create-jest@29.7.0(@types/node@20.17.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.10.7(@swc/helpers@0.5.15))(@types/node@20.17.9)(typescript@5.7.3)): dependencies: '@jest/types': 29.6.3 @@ -38099,6 +38371,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.0: + dependencies: + ms: 2.1.3 + debug@4.4.0(supports-color@5.5.0): dependencies: ms: 2.1.3 @@ -39166,7 +39442,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.0 escape-string-regexp: 4.0.0 eslint-scope: 8.2.0 eslint-visitor-keys: 4.2.0 @@ -39860,6 +40136,8 @@ snapshots: async: 0.2.10 which: 1.3.1 + follow-redirects@1.15.9: {} + follow-redirects@1.15.9(debug@4.3.7): optionalDependencies: debug: 4.3.7 @@ -40937,7 +41215,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.3 - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.0 transitivePeerDependencies: - supports-color @@ -40983,6 +41261,13 @@ snapshots: https-browserify@1.0.0: {} + https-proxy-agent@2.2.4: + dependencies: + agent-base: 4.3.0 + debug: 3.2.7 + transitivePeerDependencies: + - supports-color + https-proxy-agent@4.0.0: dependencies: agent-base: 5.1.1 @@ -40993,14 +41278,14 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.0 transitivePeerDependencies: - supports-color https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.3 - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.0 transitivePeerDependencies: - supports-color @@ -41611,7 +41896,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.0 istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -41747,6 +42032,25 @@ snapshots: - supports-color - ts-node + jest-cli@29.7.0(@types/node@20.17.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.10.7(@swc/helpers@0.5.15))(@types/node@20.17.9)(typescript@5.6.3)): + dependencies: + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.10.7(@swc/helpers@0.5.15))(@types/node@20.17.9)(typescript@5.6.3)) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@20.17.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.10.7(@swc/helpers@0.5.15))(@types/node@20.17.9)(typescript@5.6.3)) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@20.17.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.10.7(@swc/helpers@0.5.15))(@types/node@20.17.9)(typescript@5.6.3)) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest-cli@29.7.0(@types/node@20.17.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.10.7(@swc/helpers@0.5.15))(@types/node@20.17.9)(typescript@5.7.3)): dependencies: '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.10.7(@swc/helpers@0.5.15))(@types/node@20.17.9)(typescript@5.7.3)) @@ -41866,6 +42170,37 @@ snapshots: - babel-plugin-macros - supports-color + jest-config@29.7.0(@types/node@20.17.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.10.7(@swc/helpers@0.5.15))(@types/node@20.17.9)(typescript@5.6.3)): + dependencies: + '@babel/core': 7.26.0 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.26.0) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0(babel-plugin-macros@3.1.0) + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 20.17.9 + ts-node: 10.9.2(@swc/core@1.10.7(@swc/helpers@0.5.15))(@types/node@20.17.9)(typescript@5.6.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-config@29.7.0(@types/node@20.17.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.10.7(@swc/helpers@0.5.15))(@types/node@20.17.9)(typescript@5.7.3)): dependencies: '@babel/core': 7.26.0 @@ -42222,6 +42557,18 @@ snapshots: - supports-color - ts-node + jest@29.7.0(@types/node@20.17.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.10.7(@swc/helpers@0.5.15))(@types/node@20.17.9)(typescript@5.6.3)): + dependencies: + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.10.7(@swc/helpers@0.5.15))(@types/node@20.17.9)(typescript@5.6.3)) + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@20.17.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.10.7(@swc/helpers@0.5.15))(@types/node@20.17.9)(typescript@5.6.3)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest@29.7.0(@types/node@20.17.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.10.7(@swc/helpers@0.5.15))(@types/node@20.17.9)(typescript@5.7.3)): dependencies: '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.10.7(@swc/helpers@0.5.15))(@types/node@20.17.9)(typescript@5.7.3)) @@ -44275,6 +44622,12 @@ snapshots: node-fetch-native@1.6.4: {} + node-fetch@2.6.13(encoding@0.1.13): + dependencies: + whatwg-url: 5.0.0 + optionalDependencies: + encoding: 0.1.13 + node-fetch@2.6.7(encoding@0.1.13): dependencies: whatwg-url: 5.0.0 @@ -44532,7 +44885,7 @@ snapshots: '@yarnpkg/lockfile': 1.1.0 '@yarnpkg/parsers': 3.0.0-rc.46 '@zkochan/js-yaml': 0.0.7 - axios: 1.7.9(debug@4.4.0) + axios: 1.7.9 chalk: 4.1.0 cli-cursor: 3.1.0 cli-spinners: 2.6.1 @@ -46951,6 +47304,8 @@ snapshots: regenerator-runtime@0.11.1: {} + regenerator-runtime@0.13.11: {} + regenerator-runtime@0.14.1: {} regenerator-transform@0.15.2: @@ -47197,6 +47552,10 @@ snapshots: rfdc@1.4.1: {} + rimraf@2.7.1: + dependencies: + glob: 7.2.3 + rimraf@3.0.2: dependencies: glob: 7.2.3 @@ -47797,7 +48156,7 @@ snapshots: socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.3 - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.0 socks: 2.8.3 transitivePeerDependencies: - supports-color @@ -48854,12 +49213,12 @@ snapshots: '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.26.0) - ts-jest@29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@20.17.9)(babel-plugin-macros@3.1.0))(typescript@5.6.3): + ts-jest@29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@20.17.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.10.7(@swc/helpers@0.5.15))(@types/node@20.17.9)(typescript@5.6.3)))(typescript@5.6.3): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.17.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.10.7(@swc/helpers@0.5.15))(@types/node@20.17.9)(typescript@5.7.3)) + jest: 29.7.0(@types/node@20.17.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.10.7(@swc/helpers@0.5.15))(@types/node@20.17.9)(typescript@5.6.3)) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -48873,12 +49232,12 @@ snapshots: '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.26.0) - ts-jest@29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@22.10.5)(babel-plugin-macros@3.1.0))(typescript@5.6.3): + ts-jest@29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@20.17.9)(babel-plugin-macros@3.1.0))(typescript@5.6.3): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@22.10.5)(babel-plugin-macros@3.1.0) + jest: 29.7.0(@types/node@20.17.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.10.7(@swc/helpers@0.5.15))(@types/node@20.17.9)(typescript@5.7.3)) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -48935,6 +49294,27 @@ snapshots: optionalDependencies: '@swc/core': 1.10.7(@swc/helpers@0.5.15) + ts-node@10.9.2(@swc/core@1.10.7(@swc/helpers@0.5.15))(@types/node@20.17.9)(typescript@5.6.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.17.9 + acorn: 8.14.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.6.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.10.7(@swc/helpers@0.5.15) + optional: true + ts-node@10.9.2(@swc/core@1.10.7(@swc/helpers@0.5.15))(@types/node@20.17.9)(typescript@5.7.3): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -49093,7 +49473,7 @@ snapshots: tuf-js@2.2.1: dependencies: '@tufjs/models': 2.0.1 - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.0 make-fetch-happen: 13.0.1 transitivePeerDependencies: - supports-color @@ -49948,6 +50328,24 @@ snapshots: - supports-color - terser + vite-node@2.1.5(@types/node@20.17.9)(terser@5.37.0): + dependencies: + cac: 6.7.14 + debug: 4.4.0 + es-module-lexer: 1.6.0 + pathe: 1.1.2 + vite: 5.4.11(@types/node@20.17.9)(terser@5.37.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vite-node@2.1.5(@types/node@22.10.5)(terser@5.37.0): dependencies: cac: 6.7.14 @@ -50332,6 +50730,42 @@ snapshots: - supports-color - terser + vitest@2.1.5(@types/node@20.17.9)(jsdom@25.0.1(bufferutil@4.0.9)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.5))(terser@5.37.0): + dependencies: + '@vitest/expect': 2.1.5 + '@vitest/mocker': 2.1.5(vite@5.4.11(@types/node@20.17.9)(terser@5.37.0)) + '@vitest/pretty-format': 2.1.8 + '@vitest/runner': 2.1.5 + '@vitest/snapshot': 2.1.5 + '@vitest/spy': 2.1.5 + '@vitest/utils': 2.1.5 + chai: 5.1.2 + debug: 4.4.0 + expect-type: 1.1.0 + magic-string: 0.30.17 + pathe: 1.1.2 + std-env: 3.8.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.0.2 + tinyrainbow: 1.2.0 + vite: 5.4.11(@types/node@20.17.9)(terser@5.37.0) + vite-node: 2.1.5(@types/node@20.17.9)(terser@5.37.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.17.9 + jsdom: 25.0.1(bufferutil@4.0.9)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.5) + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vitest@2.1.5(@types/node@22.10.5)(jsdom@25.0.1(bufferutil@4.0.9)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.5))(terser@5.37.0): dependencies: '@vitest/expect': 2.1.5