From 8cbf6324fa864119503cecfef5a100cee31c8c07 Mon Sep 17 00:00:00 2001 From: Alex Cheng Date: Fri, 7 Feb 2025 14:02:45 -0600 Subject: [PATCH] Base, Arbitrum, Optimism, BSC, Polygon, Avalanche query support --- src/chains/ethereum.ts | 249 ++++++++++++++++++++ src/chains/solana.ts | 396 ++++++++++++++++++++++++++++++++ src/index.ts | 504 +---------------------------------------- tsconfig.json | 16 +- 4 files changed, 658 insertions(+), 507 deletions(-) create mode 100644 src/chains/ethereum.ts create mode 100644 src/chains/solana.ts diff --git a/src/chains/ethereum.ts b/src/chains/ethereum.ts new file mode 100644 index 0000000..925c255 --- /dev/null +++ b/src/chains/ethereum.ts @@ -0,0 +1,249 @@ +import { JsonRpcProvider, formatEther, formatUnits, Contract } from 'ethers'; +import { z } from "zod"; +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; + +// Network configurations +interface NetworkConfig { + name: string; + rpc: string; + chainId: number; + currencySymbol: string; + explorer: string; +} + +const NETWORKS: { [key: string]: NetworkConfig } = { + ethereum: { + name: "Ethereum", + rpc: "https://eth-mainnet.g.alchemy.com/v2/demo", + chainId: 1, + currencySymbol: "ETH", + explorer: "https://etherscan.io" + }, + base: { + name: "Base", + rpc: "https://mainnet.base.org", + chainId: 8453, + currencySymbol: "ETH", + explorer: "https://basescan.org" + }, + arbitrum: { + name: "Arbitrum", + rpc: "https://arb1.arbitrum.io/rpc", + chainId: 42161, + currencySymbol: "ETH", + explorer: "https://arbiscan.io" + }, + optimism: { + name: "Optimism", + rpc: "https://mainnet.optimism.io", + chainId: 10, + currencySymbol: "ETH", + explorer: "https://optimistic.etherscan.io" + }, + bsc: { + name: "BNB Smart Chain", + rpc: "https://bsc-dataseed.binance.org", + chainId: 56, + currencySymbol: "BNB", + explorer: "https://bscscan.com" + }, + polygon: { + name: "Polygon", + rpc: "https://polygon-rpc.com", + chainId: 137, + currencySymbol: "MATIC", + explorer: "https://polygonscan.com" + }, + avalanche: { + name: "Avalanche", + rpc: "https://api.avax.network/ext/bc/C/rpc", + chainId: 43114, + currencySymbol: "AVAX", + explorer: "https://snowtrace.io" + }, + tron: { + name: "Tron", + rpc: "https://api.trongrid.io", + chainId: 728126428, // TRON's chainId + currencySymbol: "TRX", + explorer: "https://tronscan.org" + } +}; + +// Initialize providers for each network +const providers: { [key: string]: JsonRpcProvider } = {}; +for (const [network, config] of Object.entries(NETWORKS)) { + if (network !== 'tron') { // Tron needs special handling + providers[network] = new JsonRpcProvider(config.rpc); + } +} + +// ERC-20 minimal ABI +const ERC20_ABI = [ + "function balanceOf(address owner) view returns (uint256)", + "function decimals() view returns (uint8)", + "function symbol() view returns (string)" +]; + +export function registerEthereumTools(server: McpServer) { + // Get native token balance for any EVM network + server.tool( + "getEvmBalance", + "Get native token balance for an EVM address on any supported network", + { + address: z.string().describe("EVM account address"), + network: z.string().describe("Network name (ethereum, base, arbitrum, optimism, bsc, polygon, avalanche)"), + }, + async ({ address, network }) => { + try { + if (!NETWORKS[network]) { + return { + content: [ + { + type: "text", + text: `Unsupported network: ${network}. Supported networks are: ${Object.keys(NETWORKS).join(", ")}`, + }, + ], + }; + } + + const provider = providers[network]; + const balance = await provider.getBalance(address); + const formattedBalance = formatEther(balance); + const networkConfig = NETWORKS[network]; + + return { + content: [ + { + type: "text", + text: `Balance on ${networkConfig.name}:\n${formattedBalance} ${networkConfig.currencySymbol}`, + }, + ], + }; + } catch (err) { + const error = err as Error; + return { + content: [ + { + type: "text", + text: `Failed to retrieve balance: ${error.message}`, + }, + ], + }; + } + } + ); + + // Get ERC-20 token balance for any EVM network + server.tool( + "getEvmTokenBalance", + "Get ERC-20 token balance for an address on any supported EVM network", + { + address: z.string().describe("EVM account address"), + tokenAddress: z.string().describe("ERC-20 token contract address"), + network: z.string().describe("Network name (ethereum, base, arbitrum, optimism, bsc, polygon, avalanche)"), + }, + async ({ address, tokenAddress, network }) => { + try { + if (!NETWORKS[network]) { + return { + content: [ + { + type: "text", + text: `Unsupported network: ${network}. Supported networks are: ${Object.keys(NETWORKS).join(", ")}`, + }, + ], + }; + } + + const provider = providers[network]; + const contract = new Contract(tokenAddress, ERC20_ABI, provider); + + const [balance, decimals, symbol] = await Promise.all([ + contract.balanceOf(address), + contract.decimals(), + contract.symbol() + ]); + + const formattedBalance = formatUnits(balance, decimals); + const networkConfig = NETWORKS[network]; + + return { + content: [ + { + type: "text", + text: `Token Balance on ${networkConfig.name}:\n${formattedBalance} ${symbol} (${tokenAddress})\nExplorer: ${networkConfig.explorer}/token/${tokenAddress}` + }, + ], + }; + } catch (err) { + const error = err as Error; + return { + content: [ + { + type: "text", + text: `Failed to retrieve token balance: ${error.message}`, + }, + ], + }; + } + } + ); + + // Get gas price for any EVM network + server.tool( + "getGasPrice", + "Get current gas price for any supported EVM network", + { + network: z.string().describe("Network name (ethereum, base, arbitrum, optimism, bsc, polygon, avalanche)"), + }, + async ({ network }) => { + try { + if (!NETWORKS[network]) { + return { + content: [ + { + type: "text", + text: `Unsupported network: ${network}. Supported networks are: ${Object.keys(NETWORKS).join(", ")}`, + }, + ], + }; + } + + const provider = providers[network]; + const feeData = await provider.getFeeData(); + const gasPrice = formatUnits(feeData.gasPrice || 0, 'gwei'); + const maxFeePerGas = feeData.maxFeePerGas ? formatUnits(feeData.maxFeePerGas, 'gwei') : null; + const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas ? formatUnits(feeData.maxPriorityFeePerGas, 'gwei') : null; + const networkConfig = NETWORKS[network]; + + let response = `Gas Prices on ${networkConfig.name}:\nGas Price: ${gasPrice} Gwei`; + if (maxFeePerGas) { + response += `\nMax Fee: ${maxFeePerGas} Gwei`; + } + if (maxPriorityFeePerGas) { + response += `\nMax Priority Fee: ${maxPriorityFeePerGas} Gwei`; + } + + return { + content: [ + { + type: "text", + text: response, + }, + ], + }; + } catch (err) { + const error = err as Error; + return { + content: [ + { + type: "text", + text: `Failed to retrieve gas price: ${error.message}`, + }, + ], + }; + } + } + ); +} \ No newline at end of file diff --git a/src/chains/solana.ts b/src/chains/solana.ts new file mode 100644 index 0000000..36ba4a5 --- /dev/null +++ b/src/chains/solana.ts @@ -0,0 +1,396 @@ +import { + Connection, + PublicKey, + Keypair, + Transaction, + SystemProgram, + sendAndConfirmTransaction, + LAMPORTS_PER_SOL, +} from '@solana/web3.js'; +import { + TOKEN_PROGRAM_ID, + AccountLayout, +} from '@solana/spl-token'; +import { z } from "zod"; +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import bs58 from 'bs58'; + +const SOLANA_RPC = "https://api.mainnet-beta.solana.com"; + +// Initialize Solana connection +const solanaConnection = new Connection(SOLANA_RPC, 'confirmed'); + +// Helper function to get token accounts for a wallet +async function getTokenAccounts(walletAddress: string) { + try { + const owner = new PublicKey(walletAddress); + const tokenAccounts = await solanaConnection.getParsedTokenAccountsByOwner( + owner, + { + programId: TOKEN_PROGRAM_ID + } + ); + + return tokenAccounts.value.map(account => { + const parsedAccountInfo = account.account.data.parsed.info; + return { + mint: new PublicKey(parsedAccountInfo.mint), + amount: parsedAccountInfo.tokenAmount.uiAmount, + decimals: parsedAccountInfo.tokenAmount.decimals, + tokenAccount: account.pubkey.toString() + }; + }); + } catch (error) { + console.error('Error fetching token accounts:', error); + return []; + } +} + +export function registerSolanaTools(server: McpServer) { + server.tool( + "getSlot", + "Get the current slot", + {}, + async () => { + try { + const slot = await solanaConnection.getSlot(); + return { + content: [ + { + type: "text", + text: `Current slot: ${slot}`, + }, + ], + }; + } catch (err) { + const error = err as Error; + return { + content: [ + { + type: "text", + text: `Failed to retrieve current slot: ${error.message}`, + }, + ], + }; + } + } + ); + + server.tool( + "getBalance", + "Get balance for a Solana address", + { + address: z.string().describe("Solana account address"), + }, + async ({ address }) => { + try { + const publicKey = new PublicKey(address); + const balance = await solanaConnection.getBalance(publicKey); + const solBalance = balance / LAMPORTS_PER_SOL; + + return { + content: [ + { + type: "text", + text: `Balance for ${address}:\n${solBalance} SOL`, + }, + ], + }; + } catch (err) { + const error = err as Error; + return { + content: [ + { + type: "text", + text: `Failed to retrieve balance for address: ${error.message}`, + }, + ], + }; + } + } + ); + + server.tool( + "getKeypairInfo", + "Get information about a keypair from its secret key", + { + secretKey: z.string().describe("Base58 encoded secret key or array of bytes"), + }, + async ({ secretKey }) => { + try { + let keypair: Keypair; + try { + const decoded = Uint8Array.from(secretKey.split(',').map(num => parseInt(num.trim()))); + keypair = Keypair.fromSecretKey(decoded); + } catch { + keypair = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(secretKey))); + } + + const publicKey = keypair.publicKey; + const balance = await solanaConnection.getBalance(publicKey); + const accountInfo = await solanaConnection.getAccountInfo(publicKey); + + return { + content: [ + { + type: "text", + text: `Keypair Information: +Public Key: ${publicKey.toBase58()} +Balance: ${balance / LAMPORTS_PER_SOL} SOL +Account Program Owner: ${accountInfo?.owner?.toBase58() || 'N/A'} +Account Size: ${accountInfo?.data.length || 0} bytes +Is Executable: ${accountInfo?.executable || false} +Rent Epoch: ${accountInfo?.rentEpoch || 0}`, + }, + ], + }; + } catch (err) { + const error = err as Error; + return { + content: [ + { + type: "text", + text: `Failed to retrieve keypair information: ${error.message}`, + }, + ], + }; + } + } + ); + + server.tool( + "getAccountInfo", + "Get detailed account information for a Solana address", + { + address: z.string().describe("Solana account address"), + encoding: z.enum(['base58', 'base64', 'jsonParsed']).optional().describe("Data encoding format"), + }, + async ({ address, encoding = 'base64' }) => { + try { + const publicKey = new PublicKey(address); + const accountInfo = await solanaConnection.getAccountInfo( + publicKey, + 'confirmed' + ); + + if (!accountInfo) { + return { + content: [ + { + type: "text", + text: `No account found for address: ${address}`, + }, + ], + }; + } + + let formattedData: string; + if (encoding === 'base58') { + formattedData = bs58.encode(accountInfo.data); + } else if (encoding === 'base64') { + formattedData = Buffer.from(accountInfo.data).toString('base64'); + } else { + formattedData = Buffer.from(accountInfo.data).toString('base64'); + } + + return { + content: [ + { + type: "text", + text: `Account Information for ${address}: +Lamports: ${accountInfo.lamports} (${accountInfo.lamports / LAMPORTS_PER_SOL} SOL) +Owner: ${accountInfo.owner.toBase58()} +Executable: ${accountInfo.executable} +Rent Epoch: ${accountInfo.rentEpoch} +Data Length: ${accountInfo.data.length} bytes +Data (${encoding}): ${formattedData}`, + }, + ], + }; + } catch (err) { + const error = err as Error; + return { + content: [ + { + type: "text", + text: `Failed to retrieve account information: ${error.message}`, + }, + ], + }; + } + } + ); + + server.tool( + "transfer", + "Transfer SOL from your keypair to another address", + { + secretKey: z.string().describe("Your keypair's secret key (as comma-separated numbers or JSON array)"), + toAddress: z.string().describe("Destination wallet address"), + amount: z.number().positive().describe("Amount of SOL to send"), + }, + async ({ secretKey, toAddress, amount }) => { + try { + let fromKeypair: Keypair; + try { + const decoded = Uint8Array.from(secretKey.split(',').map(num => parseInt(num.trim()))); + fromKeypair = Keypair.fromSecretKey(decoded); + } catch { + fromKeypair = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(secretKey))); + } + + const lamports = amount * LAMPORTS_PER_SOL; + + const transaction = new Transaction().add( + SystemProgram.transfer({ + fromPubkey: fromKeypair.publicKey, + toPubkey: new PublicKey(toAddress), + lamports, + }) + ); + + const signature = await sendAndConfirmTransaction( + solanaConnection, + transaction, + [fromKeypair] + ); + + return { + content: [ + { + type: "text", + text: `Transfer successful! +From: ${fromKeypair.publicKey.toBase58()} +To: ${toAddress} +Amount: ${amount} SOL +Transaction signature: ${signature} +Explorer URL: https://explorer.solana.com/tx/${signature}`, + }, + ], + }; + } catch (err) { + const error = err as Error; + return { + content: [ + { + type: "text", + text: `Failed to transfer SOL: ${error.message}`, + }, + ], + }; + } + } + ); + + server.tool( + "getSplTokenBalances", + "Get SPL token balances for a Solana address", + { + address: z.string().describe("Solana account address"), + }, + async ({ address }) => { + try { + const tokenAccounts = await getTokenAccounts(address); + + if (tokenAccounts.length === 0) { + return { + content: [ + { + type: "text", + text: `No token accounts found for address: ${address}`, + }, + ], + }; + } + + const balancesList = tokenAccounts + .filter(account => account.amount > 0) + .map(account => `Mint: ${account.mint.toString()}\nBalance: ${account.amount}\nDecimals: ${account.decimals}\nToken Account: ${account.tokenAccount}`) + .join('\n\n'); + + return { + content: [ + { + type: "text", + text: `Token Balances for ${address}:\n\n${balancesList}`, + }, + ], + }; + } catch (err) { + const error = err as Error; + return { + content: [ + { + type: "text", + text: `Failed to retrieve token balances: ${error.message}`, + }, + ], + }; + } + } + ); + + server.tool( + "getSplTokenInfo", + "Get detailed information about a specific SPL token account", + { + tokenMint: z.string().describe("Token mint address"), + ownerAddress: z.string().describe("Token account owner address"), + }, + async ({ tokenMint, ownerAddress }) => { + try { + const mint = new PublicKey(tokenMint); + const owner = new PublicKey(ownerAddress); + + const tokenAccounts = await solanaConnection.getParsedTokenAccountsByOwner( + owner, + { + mint: mint, + } + ); + + if (tokenAccounts.value.length === 0) { + return { + content: [ + { + type: "text", + text: `No token account found for mint ${tokenMint} owned by ${ownerAddress}`, + }, + ], + }; + } + + const accountInfo = tokenAccounts.value[0].account; + const parsedInfo = accountInfo.data.parsed.info; + + return { + content: [ + { + type: "text", + text: `Token Account Information: +Token Account: ${tokenAccounts.value[0].pubkey.toString()} +Mint: ${parsedInfo.mint} +Owner: ${parsedInfo.owner} +Balance: ${parsedInfo.tokenAmount.uiAmount} +Decimals: ${parsedInfo.tokenAmount.decimals} +State: ${parsedInfo.state} +Is Native: ${parsedInfo.isNative} +Delegation: ${parsedInfo.delegate || 'None'} +Close Authority: ${parsedInfo.closeAuthority || 'None'}`, + }, + ], + }; + } catch (err) { + const error = err as Error; + return { + content: [ + { + type: "text", + text: `Failed to retrieve token information: ${error.message}`, + }, + ], + }; + } + } + ); +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 1d1978e..b7d9c5c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,31 +1,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { - Connection, - PublicKey, - Keypair, - Transaction, - SystemProgram, - sendAndConfirmTransaction, - LAMPORTS_PER_SOL, -} from '@solana/web3.js'; -import { - TOKEN_PROGRAM_ID, - AccountLayout, -} from '@solana/spl-token'; -import { z } from "zod"; -import bs58 from 'bs58'; -import { JsonRpcProvider, formatEther, formatUnits, Contract, Interface } from 'ethers'; - -const SOLANA_RPC = "https://api.mainnet-beta.solana.com"; -const ETH_RPC = "https://eth-mainnet.g.alchemy.com/v2/demo"; - -// ERC-20 minimal ABI for balance checking -const ERC20_ABI = [ - "function balanceOf(address owner) view returns (uint256)", - "function decimals() view returns (uint8)", - "function symbol() view returns (string)" -]; +import { registerSolanaTools } from "./chains/solana.js"; +import { registerEthereumTools } from "./chains/ethereum.js"; // Create server instance const server = new McpServer({ @@ -33,450 +9,9 @@ const server = new McpServer({ version: "1.0.0", }); -// Initialize blockchain connections -const solanaConnection = new Connection(SOLANA_RPC, 'confirmed'); -const ethProvider = new JsonRpcProvider(ETH_RPC); - -// Register Solana tools -server.tool( - "getSlot", - "Get the current slot", - {}, - async () => { - try { - const slot = await solanaConnection.getSlot(); - return { - content: [ - { - type: "text", - text: `Current slot: ${slot}`, - }, - ], - }; - } catch (err) { - const error = err as Error; - return { - content: [ - { - type: "text", - text: `Failed to retrieve current slot: ${error.message}`, - }, - ], - }; - } - } -); - -server.tool( - "getBalance", - "Get balance for a Solana address", - { - address: z.string().describe("Solana account address"), - }, - async ({ address }) => { - try { - const publicKey = new PublicKey(address); - const balance = await solanaConnection.getBalance(publicKey); - const solBalance = balance / LAMPORTS_PER_SOL; - - return { - content: [ - { - type: "text", - text: `Balance for ${address}:\n${solBalance} SOL`, - }, - ], - }; - } catch (err) { - const error = err as Error; - return { - content: [ - { - type: "text", - text: `Failed to retrieve balance for address: ${error.message}`, - }, - ], - }; - } - } -); - -server.tool( - "getKeypairInfo", - "Get information about a keypair from its secret key", - { - secretKey: z.string().describe("Base58 encoded secret key or array of bytes"), - }, - async ({ secretKey }) => { - try { - // Handle both base58 encoded strings and byte arrays - let keypair: Keypair; - try { - // First try parsing as comma-separated string - const decoded = Uint8Array.from(secretKey.split(',').map(num => parseInt(num.trim()))); - keypair = Keypair.fromSecretKey(decoded); - } catch { - // If that fails, try as a byte array string - keypair = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(secretKey))); - } - - // Get account info and balance - const publicKey = keypair.publicKey; - const balance = await solanaConnection.getBalance(publicKey); - const accountInfo = await solanaConnection.getAccountInfo(publicKey); - - return { - content: [ - { - type: "text", - text: `Keypair Information: -Public Key: ${publicKey.toBase58()} -Balance: ${balance / LAMPORTS_PER_SOL} SOL -Account Program Owner: ${accountInfo?.owner?.toBase58() || 'N/A'} -Account Size: ${accountInfo?.data.length || 0} bytes -Is Executable: ${accountInfo?.executable || false} -Rent Epoch: ${accountInfo?.rentEpoch || 0}`, - }, - ], - }; - } catch (err) { - const error = err as Error; - return { - content: [ - { - type: "text", - text: `Failed to retrieve keypair information: ${error.message}`, - }, - ], - }; - } - } -); - -server.tool( - "getAccountInfo", - "Get detailed account information for a Solana address", - { - address: z.string().describe("Solana account address"), - encoding: z.enum(['base58', 'base64', 'jsonParsed']).optional().describe("Data encoding format"), - }, - async ({ address, encoding = 'base64' }) => { - try { - const publicKey = new PublicKey(address); - const accountInfo = await solanaConnection.getAccountInfo( - publicKey, - 'confirmed' - ); - - if (!accountInfo) { - return { - content: [ - { - type: "text", - text: `No account found for address: ${address}`, - }, - ], - }; - } - - // Format the data based on encoding - let formattedData: string; - if (encoding === 'base58') { - formattedData = bs58.encode(accountInfo.data); - } else if (encoding === 'base64') { - formattedData = Buffer.from(accountInfo.data).toString('base64'); - } else { - // For jsonParsed, we'll still return base64 but note that it's not parsed - formattedData = Buffer.from(accountInfo.data).toString('base64'); - } - - return { - content: [ - { - type: "text", - text: `Account Information for ${address}: -Lamports: ${accountInfo.lamports} (${accountInfo.lamports / LAMPORTS_PER_SOL} SOL) -Owner: ${accountInfo.owner.toBase58()} -Executable: ${accountInfo.executable} -Rent Epoch: ${accountInfo.rentEpoch} -Data Length: ${accountInfo.data.length} bytes -Data (${encoding}): ${formattedData}`, - }, - ], - }; - } catch (err) { - const error = err as Error; - return { - content: [ - { - type: "text", - text: `Failed to retrieve account information: ${error.message}`, - }, - ], - }; - } - } -); - -server.tool( - "transfer", - "Transfer SOL from your keypair to another address", - { - secretKey: z.string().describe("Your keypair's secret key (as comma-separated numbers or JSON array)"), - toAddress: z.string().describe("Destination wallet address"), - amount: z.number().positive().describe("Amount of SOL to send"), - }, - async ({ secretKey, toAddress, amount }) => { - try { - // Parse the secret key and create keypair - let fromKeypair: Keypair; - try { - // First try parsing as comma-separated string - const decoded = Uint8Array.from(secretKey.split(',').map(num => parseInt(num.trim()))); - fromKeypair = Keypair.fromSecretKey(decoded); - } catch { - // If that fails, try as a JSON array string - fromKeypair = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(secretKey))); - } - - // Convert SOL amount to lamports - const lamports = amount * LAMPORTS_PER_SOL; - - // Create transfer instruction - const transaction = new Transaction().add( - SystemProgram.transfer({ - fromPubkey: fromKeypair.publicKey, - toPubkey: new PublicKey(toAddress), - lamports, - }) - ); - - // Send and confirm transaction - const signature = await sendAndConfirmTransaction( - solanaConnection, - transaction, - [fromKeypair] - ); - - return { - content: [ - { - type: "text", - text: `Transfer successful! -From: ${fromKeypair.publicKey.toBase58()} -To: ${toAddress} -Amount: ${amount} SOL -Transaction signature: ${signature} -Explorer URL: https://explorer.solana.com/tx/${signature}`, - }, - ], - }; - } catch (err) { - const error = err as Error; - return { - content: [ - { - type: "text", - text: `Failed to transfer SOL: ${error.message}`, - }, - ], - }; - } - } -); - -// Register Ethereum tools -server.tool( - "getEthBalance", - "Get balance for an Ethereum address", - { - address: z.string().describe("Ethereum account address"), - }, - async ({ address }) => { - try { - const balance = await ethProvider.getBalance(address); - const ethBalance = formatEther(balance); // Convert from wei to ETH - - return { - content: [ - { - type: "text", - text: `Balance for ${address}:\n${ethBalance} ETH`, - }, - ], - }; - } catch (err) { - const error = err as Error; - return { - content: [ - { - type: "text", - text: `Failed to retrieve ETH balance: ${error.message}`, - }, - ], - }; - } - } -); - -// Register token balance checking tool -server.tool( - "getTokenBalance", - "Get ERC-20 token balance for an Ethereum address", - { - address: z.string().describe("Ethereum account address"), - tokenAddress: z.string().describe("ERC-20 token contract address") - }, - async ({ address, tokenAddress }) => { - try { - // Create contract interface - const contract = new Contract(tokenAddress, ERC20_ABI, ethProvider); - - // Get token details and balance - const [balance, decimals, symbol] = await Promise.all([ - contract.balanceOf(address), - contract.decimals(), - contract.symbol() - ]); - - // Format the balance using the correct number of decimals - const formattedBalance = formatUnits(balance, decimals); - - return { - content: [ - { - type: "text", - text: `Token Balance for ${address}:\n${formattedBalance} ${symbol} (${tokenAddress})` - }, - ], - }; - } catch (err) { - const error = err as Error; - return { - content: [ - { - type: "text", - text: `Failed to retrieve token balance: ${error.message}`, - }, - ], - }; - } - } -); - -server.tool( - "getSplTokenBalances", - "Get SPL token balances for a Solana address", - { - address: z.string().describe("Solana account address"), - }, - async ({ address }) => { - try { - const tokenAccounts = await getTokenAccounts(address); - - if (tokenAccounts.length === 0) { - return { - content: [ - { - type: "text", - text: `No token accounts found for address: ${address}`, - }, - ], - }; - } - - // Create formatted output of all token balances - const balancesList = tokenAccounts - .filter(account => account.amount > 0) // Only show non-zero balances - .map(account => `Mint: ${account.mint.toString()}\nBalance: ${account.amount}\nDecimals: ${account.decimals}\nToken Account: ${account.tokenAccount}`) - .join('\n\n'); - - return { - content: [ - { - type: "text", - text: `Token Balances for ${address}:\n\n${balancesList}`, - }, - ], - }; - } catch (err) { - const error = err as Error; - return { - content: [ - { - type: "text", - text: `Failed to retrieve token balances: ${error.message}`, - }, - ], - }; - } - } -); - -server.tool( - "getSplTokenInfo", - "Get detailed information about a specific SPL token account", - { - tokenMint: z.string().describe("Token mint address"), - ownerAddress: z.string().describe("Token account owner address"), - }, - async ({ tokenMint, ownerAddress }) => { - try { - const mint = new PublicKey(tokenMint); - const owner = new PublicKey(ownerAddress); - - // Find the token account - const tokenAccounts = await solanaConnection.getParsedTokenAccountsByOwner( - owner, - { - mint: mint, - } - ); - - if (tokenAccounts.value.length === 0) { - return { - content: [ - { - type: "text", - text: `No token account found for mint ${tokenMint} owned by ${ownerAddress}`, - }, - ], - }; - } - - const accountInfo = tokenAccounts.value[0].account; - const parsedInfo = accountInfo.data.parsed.info; - - return { - content: [ - { - type: "text", - text: `Token Account Information: -Token Account: ${tokenAccounts.value[0].pubkey.toString()} -Mint: ${parsedInfo.mint} -Owner: ${parsedInfo.owner} -Balance: ${parsedInfo.tokenAmount.uiAmount} -Decimals: ${parsedInfo.tokenAmount.decimals} -State: ${parsedInfo.state} -Is Native: ${parsedInfo.isNative} -Delegation: ${parsedInfo.delegate || 'None'} -Close Authority: ${parsedInfo.closeAuthority || 'None'}`, - }, - ], - }; - } catch (err) { - const error = err as Error; - return { - content: [ - { - type: "text", - text: `Failed to retrieve token information: ${error.message}`, - }, - ], - }; - } - } -); +// Register chain-specific tools +registerSolanaTools(server); +registerEthereumTools(server); async function main() { const transport = new StdioServerTransport(); @@ -488,31 +23,4 @@ main().catch((err: unknown) => { const error = err as Error; console.error("Fatal error in main():", error.message); process.exit(1); -}); - - -// Helper function to get token accounts for a wallet -async function getTokenAccounts(walletAddress: string) { - try { - const owner = new PublicKey(walletAddress); - const tokenAccounts = await solanaConnection.getParsedTokenAccountsByOwner( - owner, - { - programId: TOKEN_PROGRAM_ID - } - ); - - return tokenAccounts.value.map(account => { - const parsedAccountInfo = account.account.data.parsed.info; - return { - mint: new PublicKey(parsedAccountInfo.mint), - amount: parsedAccountInfo.tokenAmount.uiAmount, - decimals: parsedAccountInfo.tokenAmount.decimals, - tokenAccount: account.pubkey.toString() - }; - }); - } catch (error) { - console.error('Error fetching token accounts:', error); - return []; - } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 2ccff2e..635ca58 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,17 +1,15 @@ { "compilerOptions": { - "target": "ES2020", - "module": "ES2020", + "target": "es2020", + "module": "es2020", "moduleResolution": "node", - "outDir": "./build", - "rootDir": "./src", - "strict": true, + "allowSyntheticDefaultImports": true, "esModuleInterop": true, + "strict": true, "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "allowJs": true + "outDir": "build", + "rootDir": "src" }, "include": ["src/**/*"], - "exclude": ["node_modules"] + "exclude": ["node_modules", "build"] } \ No newline at end of file