UNPKG

@noves/eliza-plugin-noves

Version:

ElizaOS plugin for blockchain data using Noves Intents

443 lines (431 loc) 19.4 kB
// src/index.ts import { logger as logger5 } from "@elizaos/core"; // src/actions/getRecentTxs.ts import { logger as logger2 } from "@elizaos/core"; import { IntentProvider } from "@noves/intent-ethers-provider"; // src/types.ts import { z } from "zod"; var addressSchema = z.string().regex(/^0x[a-fA-F0-9]{40}$/, "Invalid Ethereum address"); var txHashSchema = z.string().regex(/^0x[a-fA-F0-9]{64}$/, "Invalid transaction hash"); var chainSchema = z.enum(["ethereum", "polygon", "base", "arbitrum", "optimism", "bsc"]); // src/services/utils.ts function extractBlockchainData(text) { const addressRegex = /0x[a-fA-F0-9]{40}/g; const txHashRegex = /0x[a-fA-F0-9]{64}/g; const chainRegex = /\b(ethereum|polygon|base|arbitrum|optimism|bsc|eth|matic)\b/gi; const addresses = [...text.matchAll(addressRegex)].map((match) => match[0]); const txHashes = [...text.matchAll(txHashRegex)].map((match) => match[0]); const chains = [...text.matchAll(chainRegex)].map((match) => { const chain = match[0].toLowerCase(); return chain === "eth" ? "ethereum" : chain === "matic" ? "polygon" : chain; }); return { addresses, txHashes, chains }; } // src/services/rateLimiter.ts import { logger } from "@elizaos/core"; var RateLimiter = class { requests = []; lastRequest = 0; maxRequests = 30; // per minute windowMs = 60 * 1e3; // 1 minute minInterval = 2e3; // 2 seconds async waitForNextRequest() { const now = Date.now(); this.requests = this.requests.filter((timestamp) => now - timestamp < this.windowMs); if (this.requests.length >= this.maxRequests) { const oldestRequest = Math.min(...this.requests); const waitTime = this.windowMs - (now - oldestRequest); logger.warn(`Rate limit reached, waiting ${waitTime}ms`); await new Promise((resolve) => setTimeout(resolve, waitTime)); return this.waitForNextRequest(); } const timeSinceLastRequest = now - this.lastRequest; if (timeSinceLastRequest < this.minInterval) { const waitTime = this.minInterval - timeSinceLastRequest; logger.debug(`Waiting ${waitTime}ms for rate limit interval`); await new Promise((resolve) => setTimeout(resolve, waitTime)); } this.lastRequest = Date.now(); this.requests.push(this.lastRequest); } }; var rateLimiter = new RateLimiter(); // src/actions/getRecentTxs.ts var getRecentTxsAction = { name: "GET_RECENT_TXS", similes: ["WALLET_ACTIVITY", "RECENT_TRANSACTIONS", "WALLET_HISTORY"], description: "Gets recent transactions for a wallet address with human-readable descriptions", validate: async (_runtime, message, _state) => { const text = message.content.text.toLowerCase(); const { addresses, chains } = extractBlockchainData(message.content.text); const hasActivityKeywords = ["activity", "transactions", "recent", "history", "wallet", "happened"].some( (keyword) => text.includes(keyword) ); return hasActivityKeywords && addresses.length > 0 && chains.length > 0; }, handler: async (_runtime, message, _state, _options, callback, _responses) => { logger2.info("[GET_RECENT_TXS] Starting handler"); try { const { addresses, chains } = extractBlockchainData(message.content.text); logger2.info(`[GET_RECENT_TXS] Extracted data - addresses: ${JSON.stringify(addresses)}, chains: ${JSON.stringify(chains)}`); if (addresses.length === 0 || chains.length === 0) { logger2.warn("[GET_RECENT_TXS] Missing addresses or chains"); await callback({ text: "I need a valid wallet address and chain to check recent transactions. Please provide an address like 0x... and specify the chain (ethereum, polygon, etc.)", source: message.content.source }); return; } const address = addresses[0]; const chain = chains[0]; logger2.info(`[GET_RECENT_TXS] Using address: ${address}, chain: ${chain}`); const validAddress = addressSchema.safeParse(address); const validChain = chainSchema.safeParse(chain); logger2.info(`[GET_RECENT_TXS] Validation results - address: ${validAddress.success}, chain: ${validChain.success}`); if (!validAddress.success || !validChain.success) { logger2.warn(`[GET_RECENT_TXS] Validation failed - address: ${JSON.stringify(validAddress.error)}, chain: ${JSON.stringify(validChain.error)}`); await callback({ text: `Invalid address or chain. Please provide a valid Ethereum address (0x...) and supported chain (ethereum, polygon, base, arbitrum, optimism, bsc).`, source: message.content.source }); return; } logger2.info(`[GET_RECENT_TXS] Getting recent transactions for ${address} on ${chain}`); logger2.info("[GET_RECENT_TXS] Waiting for rate limiter..."); await rateLimiter.waitForNextRequest(); logger2.info("[GET_RECENT_TXS] Rate limiter passed"); logger2.info("[GET_RECENT_TXS] Initializing IntentProvider..."); const provider = new IntentProvider(); logger2.info("[GET_RECENT_TXS] Provider initialized, calling getRecentTxs..."); const txs = await provider.getRecentTxs(validChain.data, validAddress.data); logger2.info(`[GET_RECENT_TXS] API response received: ${txs?.length || 0} transactions`); if (!txs || txs.length === 0) { logger2.warn("[GET_RECENT_TXS] No transactions found"); await callback({ text: `No recent transactions found for ${address} on ${chain}.`, actions: ["GET_RECENT_TXS"], source: message.content.source }); return; } logger2.info("[GET_RECENT_TXS] Formatting response..."); const recentTxs = txs.slice(0, 5); let response = `\u{1F50D} **Recent activity for ${address} on ${chain}:** `; recentTxs.forEach((tx, index) => { const description = tx.classificationData?.description || "Unknown transaction"; const hash = tx.rawTransactionData?.transactionHash ? `${tx.rawTransactionData.transactionHash.slice(0, 10)}...` : "N/A"; const timestamp = tx.rawTransactionData?.timestamp ? new Date(tx.rawTransactionData.timestamp * 1e3).toLocaleString() : "N/A"; response += `${index + 1}. **${description}** `; response += ` \u2022 Hash: ${hash} `; response += ` \u2022 Time: ${timestamp} `; }); if (txs.length > 5) { response += `... and ${txs.length - 5} more transactions.`; } logger2.info(`[GET_RECENT_TXS] Calling callback with response: ${response}`); await callback({ text: response, actions: ["GET_RECENT_TXS"], source: message.content.source }); logger2.info("[GET_RECENT_TXS] Callback completed successfully"); } catch (error) { logger2.error("[GET_RECENT_TXS] Error in action:", error); logger2.error("[GET_RECENT_TXS] Error stack:", error.stack); logger2.error("[GET_RECENT_TXS] Error message:", error.message); const { addresses: errorAddresses, chains: errorChains } = extractBlockchainData(message.content.text); await callback({ text: `Sorry, I encountered an error while fetching recent transactions for ${errorAddresses?.[0] || "the address"} on ${errorChains?.[0] || "the chain"}. This could be due to API rate limiting, network issues, or missing API credentials. Error: ${error.message}`, source: message.content.source }); } }, examples: [ [ { name: "User", content: { text: "what was the activity of 0x625758C705bf970375fF780f3544C1ddc8eeb6Ab on ethereum?" } }, { name: "Assistant", content: { text: "\u{1F50D} **Recent activity for 0x625758C705bf970375fF780f3544C1ddc8eeb6Ab on ethereum:**\n\n1. **Swapped ETH for USDC**\n \u2022 Hash: 0x1234567890...\n \u2022 Time: 12/13/2025, 2:30:45 PM", actions: ["GET_RECENT_TXS"] } } ] ] }; // src/actions/getTranslatedTx.ts import { logger as logger3 } from "@elizaos/core"; import { IntentProvider as IntentProvider2 } from "@noves/intent-ethers-provider"; var getTranslatedTxAction = { name: "GET_TRANSLATED_TX", similes: ["EXPLAIN_TRANSACTION", "TRANSACTION_DETAILS", "WHAT_HAPPENED"], description: "Gets detailed human-readable information about a specific transaction", validate: async (_runtime, message, _state) => { const text = message.content.text.toLowerCase(); const { txHashes, chains } = extractBlockchainData(message.content.text); const hasTransactionKeywords = ["transaction", "happened", "understand", "explain", "details"].some( (keyword) => text.includes(keyword) ); return hasTransactionKeywords && txHashes.length > 0 && chains.length > 0; }, handler: async (_runtime, message, _state, _options, callback, _responses) => { logger3.info("[GET_TRANSLATED_TX] Starting handler"); try { const { txHashes, chains } = extractBlockchainData(message.content.text); logger3.info(`[GET_TRANSLATED_TX] Extracted data - txHashes: ${JSON.stringify(txHashes)}, chains: ${JSON.stringify(chains)}`); if (txHashes.length === 0 || chains.length === 0) { logger3.warn("[GET_TRANSLATED_TX] Missing transaction hash or chains"); await callback({ text: "I need a valid transaction hash and chain to explain the transaction. Please provide a transaction hash like 0x... and specify the chain.", source: message.content.source }); return; } const txHash = txHashes[0]; const chain = chains[0]; logger3.info(`[GET_TRANSLATED_TX] Using txHash: ${txHash}, chain: ${chain}`); const validTxHash = txHashSchema.safeParse(txHash); const validChain = chainSchema.safeParse(chain); logger3.info(`[GET_TRANSLATED_TX] Validation results - txHash: ${validTxHash.success}, chain: ${validChain.success}`); if (!validTxHash.success || !validChain.success) { logger3.warn(`[GET_TRANSLATED_TX] Validation failed - txHash: ${JSON.stringify(validTxHash.error)}, chain: ${JSON.stringify(validChain.error)}`); await callback({ text: `Invalid transaction hash or chain. Please provide a valid transaction hash (0x...) and supported chain.`, source: message.content.source }); return; } logger3.info(`[GET_TRANSLATED_TX] Getting transaction details for ${txHash} on ${chain}`); logger3.info("[GET_TRANSLATED_TX] Waiting for rate limiter..."); await rateLimiter.waitForNextRequest(); logger3.info("[GET_TRANSLATED_TX] Rate limiter passed"); logger3.info("[GET_TRANSLATED_TX] Initializing IntentProvider..."); const provider = new IntentProvider2(); logger3.info("[GET_TRANSLATED_TX] Provider initialized, calling getTranslatedTx..."); const tx = await provider.getTranslatedTx(validChain.data, validTxHash.data); logger3.info(`[GET_TRANSLATED_TX] API response received: ${JSON.stringify(tx, null, 2)}`); if (!tx) { logger3.warn("[GET_TRANSLATED_TX] Transaction not found"); await callback({ text: `Transaction ${txHash} not found on ${chain}.`, actions: ["GET_TRANSLATED_TX"], source: message.content.source }); return; } logger3.info("[GET_TRANSLATED_TX] Formatting response..."); const description = tx.classificationData?.description || "Unknown transaction"; const timestamp = tx.rawTransactionData?.timestamp ? new Date(tx.rawTransactionData.timestamp * 1e3).toLocaleString() : "N/A"; let response = `\u{1F50D} **Transaction Analysis for ${txHash}** `; response += `\u{1F4CB} **Description:** ${description} `; response += `\u23F0 **Time:** ${timestamp} `; response += `\u26D3\uFE0F **Chain:** ${chain} `; if (tx.rawTransactionData?.gasUsed && tx.rawTransactionData?.gasPrice) { const gasCost = parseFloat(tx.rawTransactionData.gasUsed.toString()) * parseFloat(tx.rawTransactionData.gasPrice.toString()) / 1e18; response += `\u26FD **Gas Cost:** ${gasCost.toFixed(6)} ETH `; } if (tx.classificationData?.type) { response += `\u{1F3F7}\uFE0F **Type:** ${tx.classificationData.type} `; } logger3.info(`[GET_TRANSLATED_TX] Calling callback with response: ${response}`); await callback({ text: response, actions: ["GET_TRANSLATED_TX"], source: message.content.source }); logger3.info("[GET_TRANSLATED_TX] Callback completed successfully"); } catch (error) { logger3.error("[GET_TRANSLATED_TX] Error in action:", error); logger3.error("[GET_TRANSLATED_TX] Error stack:", error.stack); logger3.error("[GET_TRANSLATED_TX] Error message:", error.message); await callback({ text: `Sorry, I encountered an error while analyzing the transaction: ${error.message}`, source: message.content.source }); } }, examples: [ [ { name: "User", content: { text: "what happened in 0x700d06dc473f95530a0dfa04c1fe679aecd722d2a14e07170704fb7a8d2381f6 on ethereum?" } }, { name: "Assistant", content: { text: "\u{1F50D} **Transaction Analysis for 0x700d06dc473f95530a0dfa04c1fe679aecd722d2a14e07170704fb7a8d2381f6**\n\n\u{1F4CB} **Description:** Swapped 1.5 ETH for 3,240 USDC\n\u23F0 **Time:** 12/13/2025, 2:30:45 PM\n\u{1F4CA} **Status:** \u2705 Success", actions: ["GET_TRANSLATED_TX"] } } ] ] }; // src/actions/getTokenPrice.ts import { logger as logger4 } from "@elizaos/core"; import { IntentProvider as IntentProvider3 } from "@noves/intent-ethers-provider"; var getTokenPriceAction = { name: "GET_TOKEN_PRICE", similes: ["TOKEN_PRICE", "PRICE_CHECK", "TOKEN_VALUE"], description: "Gets current or historical price information for a token", validate: async (_runtime, message, _state) => { const text = message.content.text.toLowerCase(); const { addresses, chains } = extractBlockchainData(message.content.text); const hasPriceKeywords = ["price", "value", "cost", "worth", "usd"].some( (keyword) => text.includes(keyword) ); return hasPriceKeywords && addresses.length > 0 && chains.length > 0; }, handler: async (_runtime, message, _state, _options, callback, _responses) => { try { const { addresses, chains } = extractBlockchainData(message.content.text); const text = message.content.text.toLowerCase(); if (addresses.length === 0 || chains.length === 0) { await callback({ text: "I need a valid token address and chain to check the price. Please provide a token address like 0x... and specify the chain.", source: message.content.source }); return; } const tokenAddress = addresses[0]; const chain = chains[0]; const validAddress = addressSchema.safeParse(tokenAddress); const validChain = chainSchema.safeParse(chain); if (!validAddress.success || !validChain.success) { await callback({ text: `Invalid token address or chain. Please provide a valid token address and supported chain.`, source: message.content.source }); return; } const isHistorical = text.includes("ago") || text.includes("was") || text.includes("month") || text.includes("week") || text.includes("day"); let timestamp; if (isHistorical) { const thirtyDaysAgo = Math.floor((Date.now() - 30 * 24 * 60 * 60 * 1e3) / 1e3); timestamp = thirtyDaysAgo.toString(); } logger4.info(`Getting ${isHistorical ? "historical" : "current"} price for ${tokenAddress} on ${chain}`); await rateLimiter.waitForNextRequest(); const provider = new IntentProvider3(); const priceParams = { chain: validChain.data, token_address: validAddress.data, ...timestamp && { timestamp } }; const priceData = await provider.getTokenPrice(priceParams); if (!priceData || !priceData.price) { await callback({ text: `Price data not available for token ${tokenAddress} on ${chain}.`, actions: ["GET_TOKEN_PRICE"], source: message.content.source }); return; } const price = parseFloat(priceData.price.amount); const currency = priceData.price.currency || "USD"; const symbol = priceData.token?.symbol || "Unknown Token"; const name = priceData.token?.name || tokenAddress; let response = `\u{1F4B0} **Token Price Information** `; response += `\u{1F3F7}\uFE0F **Token:** ${name} (${symbol}) `; response += `\u{1F4CD} **Address:** ${tokenAddress} `; response += `\u26D3\uFE0F **Chain:** ${chain} `; response += `\u{1F4B5} **Price:** $${price.toLocaleString()} ${currency} `; if (timestamp) { const date = new Date(parseInt(timestamp) * 1e3); response += `\u{1F4C5} **Date:** ${date.toLocaleDateString()} (Historical) `; } else { response += `\u23F0 **Updated:** Just now (Current) `; } if (priceData.pricedBy?.liquidity) { response += `\u{1F4A7} **Liquidity:** $${parseFloat(priceData.pricedBy.liquidity.toString()).toLocaleString()} `; } if (priceData.pricedBy?.exchange?.name) { response += `\u{1F3EA} **Exchange:** ${priceData.pricedBy.exchange.name} `; } await callback({ text: response, actions: ["GET_TOKEN_PRICE"], source: message.content.source }); } catch (error) { logger4.error("Error in GET_TOKEN_PRICE action:", error); await callback({ text: `Sorry, I encountered an error while fetching token price: ${error.message}`, source: message.content.source }); } }, examples: [ [ { name: "User", content: { text: "what is the price of the 0xae7ab96520de3a18e5e111b5eaab095312d7fe84 token on ethereum?" } }, { name: "Assistant", content: { text: "\u{1F4B0} **Token Price Information**\n\n\u{1F3F7}\uFE0F **Token:** Lido Staked ETH (stETH)\n\u{1F4CD} **Address:** 0xae7ab96520de3a18e5e111b5eaab095312d7fe84\n\u26D3\uFE0F **Chain:** ethereum\n\u{1F4B5} **Price:** $3,245.67 USD", actions: ["GET_TOKEN_PRICE"] } } ] ] }; // src/index.ts var novesPlugin = { name: "plugin-noves", description: "ElizaOS plugin for blockchain data using Noves Intents", repository: "https://github.com/Noves-Inc/eliza-plugin-noves", actions: [getRecentTxsAction, getTranslatedTxAction, getTokenPriceAction], // No providers, services, models, routes, or events needed for this plugin providers: [], services: [], // Simple initialization async init() { logger5.info("\u{1F680} Noves blockchain plugin initialized successfully!"); logger5.info("\u2705 Available actions: GET_RECENT_TXS, GET_TRANSLATED_TX, GET_TOKEN_PRICE"); logger5.info("\u26A1 Rate limiting: 30 requests/minute, 2-second intervals"); } }; var index_default = novesPlugin; export { RateLimiter, index_default as default, extractBlockchainData, getRecentTxsAction, getTokenPriceAction, getTranslatedTxAction, novesPlugin, rateLimiter }; //# sourceMappingURL=index.js.map