UNPKG

arbitrum-mcp-tools

Version:

A comprehensive collection of Model Context Protocol (MCP) tools for interacting with the Arbitrum blockchain. Enables AI assistants like Claude, Cursor, and Windsurf to perform blockchain operations including account analysis, contract interaction, cross

635 lines (634 loc) • 29.5 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { z } from "zod"; import { ethers, Interface } from "ethers"; import { handleError } from "../common.js"; import { alchemy } from "../../index.js"; import { AssetTransfersCategory, SortingOrder } from "alchemy-sdk"; export function registerContractInteractionTools(server) { // 1. Code Analysis server.tool("getContractCode", "Retrieve the bytecode of a contract at a specific address and optionally at a given block.", { contractAddress: z .string() .describe("The address or ENS name of the account to get the code for."), blockTag: z .string() .optional() .describe("The optional block number, hash, or tag (e.g., 'latest', 'pending', 'safe', 'finalized', 'earliest') to get the code for. Defaults to 'latest' if unspecified."), }, (_a) => __awaiter(this, [_a], void 0, function* ({ contractAddress, blockTag }) { try { const code = yield alchemy.core.getCode(contractAddress, blockTag || "latest"); if (code === "0x") { return { content: [ { type: "text", text: "No contract deployed at this address or block.", }, ], }; } return { content: [ { type: "text", text: `Contract code:\n${code}`, }, ], }; } catch (error) { return { content: [{ type: "text", text: `Error: ${handleError(error)}` }], }; } })); // 2. ABI Decoding server.tool("decodeTransactionCalldata", "Decode transaction input data using Arbitrum contract ABIs. Automatically detects the target contract from transaction hash.", { transactionHash: z .string() .describe("Transaction hash to decode (automatically detects contract address from transaction)"), }, (_a) => __awaiter(this, [_a], void 0, function* ({ transactionHash }) { var _b; try { const apiKey = process.env.ARBISCAN_API_KEY; if (!apiKey) { return { content: [ { type: "text", text: "ARBISCAN_API_KEY is not configured. Please set it in your environment variables.", }, ], }; } let inputData; let contractAddress; let txDetails = null; // Get transaction details to extract input data and contract address try { const tx = yield alchemy.core.getTransaction(transactionHash); if (!tx) { return { content: [ { type: "text", text: `Transaction ${transactionHash} not found on Arbitrum network`, }, ], }; } inputData = tx.data; txDetails = tx; // Auto-detect contract address from transaction if (tx.to) { contractAddress = tx.to; } else { return { content: [ { type: "text", text: `Transaction ${transactionHash} appears to be a contract creation. No target contract address available for ABI lookup.`, }, ], }; } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: `Failed to fetch transaction: ${errorMessage}. Make sure the transaction exists on Arbitrum.`, }, ], }; } // Validate input data if (!inputData || inputData === "0x") { return { content: [ { type: "text", text: "No input data to decode. This might be a simple ETH transfer.", }, ], }; } // Validate hex format if (!inputData.startsWith("0x") || inputData.length < 10) { return { content: [ { type: "text", text: "Invalid input data format. Expected hex string starting with 0x and at least 4 bytes long.", }, ], }; } // Get contract ABI from Arbiscan let abiData; try { const abiResponse = yield fetch(`https://api.arbiscan.io/api?module=contract&action=getabi&address=${contractAddress}&apikey=${apiKey}`, { headers: { "User-Agent": "Arbitrum-ABI-Decoder/1.0", }, }); if (!abiResponse.ok) { throw new Error(`Arbiscan API returned ${abiResponse.status}: ${abiResponse.statusText}`); } abiData = (yield abiResponse.json()); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: `Failed to fetch contract ABI: ${errorMessage}. Check if the contract address is correct and verified on Arbiscan.`, }, ], }; } // Check ABI response if (abiData.status !== "1" || !abiData.result) { const errorMsg = abiData.message || "Unknown error"; return { content: [ { type: "text", text: `No verified ABI found for contract ${contractAddress}. Error: ${errorMsg}. Make sure the contract is verified on Arbiscan.`, }, ], }; } // Parse and validate ABI let abi; try { abi = JSON.parse(abiData.result); if (!Array.isArray(abi)) { throw new Error("ABI is not a valid array"); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: `Invalid ABI format received from Arbiscan: ${errorMessage}`, }, ], }; } // Decode function call let decoded; try { const iface = new Interface(abi); decoded = iface.parseTransaction({ data: inputData }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); // Try to extract function selector for better error reporting const selector = inputData.slice(0, 10); return { content: [ { type: "text", text: `Could not decode transaction data. Function selector: ${selector}. Error: ${errorMessage}. This might be a function not present in the contract ABI or invalid calldata.`, }, ], }; } if (!decoded) { return { content: [ { type: "text", text: "Could not decode transaction data. The function signature might not match the contract ABI.", }, ], }; } // Format the decoded arguments nicely const formatArguments = (args, inputs) => { const formatted = {}; if (inputs && inputs.length > 0) { inputs.forEach((input, index) => { const value = args[index]; formatted[input.name || `param${index}`] = { type: input.type, value: (value === null || value === void 0 ? void 0 : value.toString()) || String(value || ""), }; }); } else { // Fallback if no input info available args.forEach((arg, index) => { formatted[`param${index}`] = { type: "unknown", value: (arg === null || arg === void 0 ? void 0 : arg.toString()) || String(arg || ""), }; }); } return formatted; }; // Find function definition for parameter names const functionDefinition = abi.find((item) => item.type === "function" && item.name === decoded.name); const formattedArguments = formatArguments(decoded.args, functionDefinition === null || functionDefinition === void 0 ? void 0 : functionDefinition.inputs); // Build comprehensive response let responseText = `āœ… Successfully decoded transaction calldata\n\n`; responseText += `šŸ”— Transaction Hash: ${transactionHash}\n`; responseText += `šŸ“ Target Contract: ${contractAddress}\n`; responseText += `⚔ Function Called: ${decoded.name}\n`; responseText += `šŸ”§ Function Selector: ${inputData.slice(0, 10)}\n\n`; responseText += `šŸ“Š Function Parameters:\n`; if (Object.keys(formattedArguments).length > 0) { Object.entries(formattedArguments).forEach(([paramName, paramInfo]) => { // Handle long values by truncating if necessary let displayValue = paramInfo.value; if (paramInfo.type === "bytes" && displayValue.length > 100) { displayValue = `${displayValue.substring(0, 100)}... (${displayValue.length} chars total)`; } responseText += ` • ${paramName} (${paramInfo.type}): ${displayValue}\n`; }); } else { responseText += ` • No parameters required\n`; } responseText += `\nšŸ’° Transaction Value: ${txDetails.value ? `${parseInt(txDetails.value.toString()) / 1e18} ETH` : "0 ETH"}\n`; responseText += `⛽ Gas Limit: ${((_b = txDetails.gasLimit) === null || _b === void 0 ? void 0 : _b.toString()) || "Unknown"}\n`; responseText += `šŸ‘¤ From Address: ${txDetails.from || "Unknown"}\n`; return { content: [ { type: "text", text: responseText, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: `āŒ Decoding failed: ${errorMessage}\n\nTroubleshooting:\n1. Verify the transaction exists on Arbitrum\n2. Check if the contract is verified on Arbiscan\n3. Ensure the transaction hash is correct`, }, ], }; } })); // 3. Event Monitoring server.tool("getContractEvents", "Query specific events from Arbitrum contracts with intelligent block range management and auto-discovery", { contractAddress: z.string().describe("Contract address"), eventSignature: z .string() .optional() .describe("Event signature (e.g., 'Transfer(address,address,uint256)')"), fromBlock: z.number().optional().describe("Starting block number"), toBlock: z.number().optional().describe("Ending block number"), maxBlocks: z .number() .optional() .default(500) .describe("Maximum blocks per query (default: 500)"), limit: z .number() .optional() .default(1000) .describe("Maximum number of events to return (default: 1000)"), topics: z .array(z.string()) .optional() .describe("Additional topic filters"), autoDiscover: z .boolean() .optional() .default(true) .describe("Automatically find active periods if no events found (default: true)"), searchDepth: z .number() .optional() .default(5) .describe("Number of block ranges to search when auto-discovering (default: 5)"), }, (_a) => __awaiter(this, [_a], void 0, function* ({ contractAddress, eventSignature, fromBlock, toBlock, maxBlocks = 500, limit = 1000, topics, autoDiscover = true, searchDepth = 5, }) { try { // Ensure maxBlocks doesn't exceed API limit const safeMaxBlocks = Math.min(maxBlocks, 500); // Get current block const currentBlock = yield alchemy.core.getBlockNumber(); // Smart block range determination let startBlock; let endBlock; if (fromBlock && toBlock) { // User specified both blocks if (toBlock - fromBlock > safeMaxBlocks) { // Split into chunks and query the most recent chunk first endBlock = toBlock; startBlock = Math.max(fromBlock, endBlock - safeMaxBlocks); } else { startBlock = fromBlock; endBlock = toBlock; } } else if (fromBlock) { // Only fromBlock specified - search forward startBlock = fromBlock; endBlock = Math.min(fromBlock + safeMaxBlocks, currentBlock); } else if (toBlock) { // Only toBlock specified - search backward endBlock = toBlock; startBlock = Math.max(0, endBlock - safeMaxBlocks); } else { // No blocks specified - start with recent blocks endBlock = currentBlock; startBlock = Math.max(0, currentBlock - safeMaxBlocks); } // Build topics array let topicsArray = []; if (eventSignature) { const eventId = ethers.id(eventSignature); topicsArray.push(eventId); } if (topics && topics.length > 0) { if (topicsArray.length > 0) { topicsArray = topicsArray.concat(topics); } else { topicsArray = topics; } } // Helper function to query a specific range const queryRange = (from, to) => __awaiter(this, void 0, void 0, function* () { try { const logs = yield alchemy.core.getLogs({ address: contractAddress, topics: topicsArray.length > 0 ? topicsArray : undefined, fromBlock: `0x${from.toString(16)}`, toBlock: `0x${to.toString(16)}`, }); return { logs, from, to, error: null }; } catch (error) { return { logs: [], from, to, error: error }; } }); // Initial query let result = yield queryRange(startBlock, endBlock); let allLogs = result.logs; const queriedRanges = [ { from: startBlock, to: endBlock, found: result.logs.length }, ]; // Auto-discovery: if no events found and autoDiscover is enabled if (allLogs.length === 0 && autoDiscover && !result.error) { console.log("No events found in initial range, starting auto-discovery..."); // Try to find the contract's first transaction to get a better range try { const txHistory = yield alchemy.core.getAssetTransfers({ fromAddress: contractAddress, category: [ AssetTransfersCategory.EXTERNAL, AssetTransfersCategory.ERC20, AssetTransfersCategory.ERC721, AssetTransfersCategory.ERC1155, ], maxCount: 1, order: SortingOrder.ASCENDING, }); if (txHistory.transfers.length > 0) { const firstTxBlock = txHistory.transfers[0].blockNum; if (typeof firstTxBlock === "string") { const firstBlock = parseInt(firstTxBlock, 16); console.log(`Found first transaction at block ${firstBlock}`); // Search from first transaction forward in chunks for (let i = 0; i < searchDepth && allLogs.length === 0; i++) { const searchStart = firstBlock + i * safeMaxBlocks; const searchEnd = Math.min(searchStart + safeMaxBlocks, currentBlock); if (searchStart >= currentBlock) break; const searchResult = yield queryRange(searchStart, searchEnd); if (searchResult.logs.length > 0) { allLogs = searchResult.logs; queriedRanges.push({ from: searchStart, to: searchEnd, found: searchResult.logs.length, }); break; } queriedRanges.push({ from: searchStart, to: searchEnd, found: 0, }); } } } } catch (e) { console.log("Could not get transaction history for auto-discovery"); } // If still no events, try recent blocks in reverse chronological order if (allLogs.length === 0) { for (let i = 1; i <= searchDepth && allLogs.length === 0; i++) { const searchEnd = currentBlock - (i - 1) * safeMaxBlocks; const searchStart = Math.max(0, searchEnd - safeMaxBlocks); if (searchStart < 0 || searchEnd <= searchStart) break; // Skip if we already queried this range const alreadyQueried = queriedRanges.some((r) => r.from === searchStart && r.to === searchEnd); if (!alreadyQueried) { const searchResult = yield queryRange(searchStart, searchEnd); if (searchResult.logs.length > 0) { allLogs = searchResult.logs; queriedRanges.push({ from: searchStart, to: searchEnd, found: searchResult.logs.length, }); break; } queriedRanges.push({ from: searchStart, to: searchEnd, found: 0, }); } } } } // Handle the original query error if (result.error) { throw result.error; } // Limit results if too many const limitedLogs = allLogs.slice(0, limit); const truncated = allLogs.length > limit; // Enhanced response with metadata const response = { contract: contractAddress, queriedRanges, eventSignature: eventSignature || "All events", totalFound: allLogs.length, returned: limitedLogs.length, truncated, autoDiscoveryUsed: autoDiscover && queriedRanges.length > 1, events: limitedLogs.map((log, index) => ({ index, blockNumber: log.blockNumber, transactionHash: log.transactionHash, logIndex: log.logIndex, topics: log.topics, data: log.data, removed: log.removed, })), }; // Build result text let resultText = `Contract Events Query Results ===================================== Contract: ${contractAddress} Event Filter: ${eventSignature || "All events"} Total Found: ${allLogs.length} events Returned: ${limitedLogs.length} events`; // Show queried ranges resultText += `\n\nQueried Ranges:`; queriedRanges.forEach((range, i) => { resultText += `\n ${i + 1}. Blocks ${range.from} to ${range.to} (${range.to - range.from} blocks) - Found: ${range.found} events`; }); if (response.autoDiscoveryUsed) { resultText += `\n\nšŸ” Auto-discovery was used to find active periods`; } if (truncated) { resultText += `\n\nāš ļø Results truncated to ${limit} events. Use smaller block ranges or more specific filters for complete results.`; } if (limitedLogs.length === 0) { resultText += `\n\nNo events found in any of the queried ranges.`; // Provide helpful suggestions based on what was tried resultText += `\n\nSuggestions:`; if (autoDiscover) { resultText += `\n - The contract may not emit events, or events may be in a different time period`; resultText += `\n - Try specifying a custom block range if you know when the contract was active`; resultText += `\n - Verify the contract address is correct: ${contractAddress}`; } else { resultText += `\n - Try enabling auto-discovery to search for active periods`; resultText += `\n - Expand your block range or try different time periods`; resultText += `\n - Verify the event signature format if specified`; } resultText += `\n - Check if this is the correct contract address`; resultText += `\n - Some contracts may have very low activity or only emit events during specific operations`; } else { resultText += `\n\nEvents:\n${JSON.stringify(response.events, null, 2)}`; // Add helpful context about the found events if (response.events.length > 0) { const blockNumbers = response.events.map((e) => e.blockNumber); const minBlock = Math.min(...blockNumbers); const maxBlock = Math.max(...blockNumbers); resultText += `\n\nšŸ“Š Event Distribution: - Block range: ${minBlock} to ${maxBlock} - Unique transactions: ${new Set(response.events.map((e) => e.transactionHash)).size} - Event topics: ${new Set(response.events.flatMap((e) => e.topics)).size} unique`; } } return { content: [ { type: "text", text: resultText, }, ], }; } catch (error) { // Enhanced error handling const errorMessage = handleError(error); let helpfulError = `Error querying contract events: ${errorMessage}`; // Provide specific guidance for common errors if (errorMessage.includes("block range") || errorMessage.includes("500 block")) { helpfulError += `\n\nšŸ’” Block Range Issue: - The query exceeded the 500-block limit - Try specifying smaller block ranges (e.g., 250 blocks at a time) - Use the auto-discovery feature to find active periods automatically`; } else if (errorMessage.includes("invalid address")) { helpfulError += `\n\nšŸ’” Address Issue: - Ensure the contract address is a valid Ethereum address - Check for typos in the address: ${contractAddress}`; } else if (errorMessage.includes("topic") || errorMessage.includes("signature")) { helpfulError += `\n\nšŸ’” Event Signature Issue: - Check the event signature format - Example: 'Transfer(address,address,uint256)' - Ensure parameter types match exactly`; } else if (errorMessage.includes("timeout") || errorMessage.includes("rate limit")) { helpfulError += `\n\nšŸ’” API Issue: - The request may have timed out or hit rate limits - Try reducing the block range or adding delays between requests`; } return { content: [ { type: "text", text: helpfulError, }, ], }; } })); // 4. Allowance Checks server.tool("getTokenAllowance", "Get ERC-20 token allowance for an owner and spender", { tokenAddress: z.string().describe("ERC-20 token contract address"), owner: z.string().describe("Owner address"), spender: z.string().describe("Spender address"), }, (_a) => __awaiter(this, [_a], void 0, function* ({ tokenAddress, owner, spender }) { try { // Define minimal ABI for allowance const abi = [ "function allowance(address owner, address spender) view returns (uint256)", ]; const iface = new Interface(abi); const data = iface.encodeFunctionData("allowance", [owner, spender]); const result = yield alchemy.core.call({ to: tokenAddress, data }); const [allowance] = iface.decodeFunctionResult("allowance", result); return { content: [ { type: "text", text: `Token allowance: ${allowance.toString()}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error: ${handleError(error)}`, }, ], }; } })); }