@noves/eliza-plugin-noves
Version:
ElizaOS plugin for blockchain data using Noves Intents
443 lines (431 loc) • 19.4 kB
JavaScript
// 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