# Directory Structure ``` ├── .env.example ├── .eslintrc │ └── index.js ├── .gitignore ├── .npmignore ├── .npmrc ├── jest.config.js ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── required-methods.md ├── src │ ├── handlers │ │ ├── utils.ts │ │ ├── wallet.ts │ │ └── wallet.types.ts │ ├── index.ts │ ├── tools.ts │ └── types.ts ├── tests │ └── handlers │ ├── network.test.ts │ ├── provider.test.ts │ └── wallet.test.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` node_modules/ build/ .env .DS_Store *.log ``` -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- ``` @dcspark:registry=https://npm.pkg.github.com ``` -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- ``` .eslintrc tests .gitignore .git .DS_Store .vscode .env ``` -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- ``` # Optional private key for wallet operations PRIVATE_KEY=your_private_key_here ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # MCP Crypto Wallet EVM This repository contains a Model Context Protocol (MCP) server that provides Claude with access to Ethereum and EVM-compatible blockchain operations via ethers.js v5. The server enables Claude to perform operations like creating wallets, checking balances, sending transactions, and interacting with smart contracts on EVM-compatible blockchains. <a href="https://glama.ai/mcp/servers/@dcSpark/mcp-cryptowallet-evm"> <img width="380" height="200" src="https://glama.ai/mcp/servers/@dcSpark/mcp-cryptowallet-evm/badge" alt="Crypto Wallet EVM MCP server" /> </a> ## Overview The MCP server exposes the following tools to Claude: ### Wallet Creation and Management - `wallet_create_random`: Create a new wallet with a random private key - `wallet_from_private_key`: Create a wallet from a private key - `wallet_from_mnemonic`: Create a wallet from a mnemonic phrase - `wallet_from_encrypted_json`: Create a wallet by decrypting an encrypted JSON wallet - `wallet_encrypt`: Encrypt a wallet with a password ### Wallet Properties - `wallet_get_address`: Get the wallet address - `wallet_get_public_key`: Get the wallet public key - `wallet_get_private_key`: Get the wallet private key (with appropriate security warnings) - `wallet_get_mnemonic`: Get the wallet mnemonic phrase (if available) ### Blockchain Methods - `wallet_get_balance`: Get the balance of the wallet - `wallet_get_chain_id`: Get the chain ID the wallet is connected to - `wallet_get_gas_price`: Get the current gas price - `wallet_get_transaction_count`: Get the number of transactions sent from this account (nonce) - `wallet_call`: Call a contract method without sending a transaction ### Transaction Methods - `wallet_send_transaction`: Send a transaction - `wallet_sign_transaction`: Sign a transaction without sending it - `wallet_populate_transaction`: Populate a transaction with missing fields ### Signing Methods - `wallet_sign_message`: Sign a message - `wallet_sign_typed_data`: Sign typed data (EIP-712) - `wallet_verify_message`: Verify a signed message - `wallet_verify_typed_data`: Verify signed typed data ### Provider Methods - `provider_get_block`: Get a block by number or hash - `provider_get_transaction`: Get a transaction by hash - `provider_get_transaction_receipt`: Get a transaction receipt - `provider_get_code`: Get the code at an address - `provider_get_storage_at`: Get the storage at a position for an address - `provider_estimate_gas`: Estimate the gas required for a transaction - `provider_get_logs`: Get logs that match a filter - `provider_get_ens_resolver`: Get the ENS resolver for a name - `provider_lookup_address`: Lookup the ENS name for an address - `provider_resolve_name`: Resolve an ENS name to an address ### Network Methods - `network_get_network`: Get the current network information - `network_get_block_number`: Get the current block number - `network_get_fee_data`: Get the current fee data (base fee, max priority fee, etc.) ## Prerequisites - Node.js (v16 or higher) - Claude Desktop application ## Installation ### Option 1: Using npx (Recommended) You can run the MCP server directly without installation using npx: ```bash npx @mcp-dockmaster/mcp-cryptowallet-evm ``` This will download and execute the server directly from npm. ### Option 2: Manual Installation 1. Clone this repository: ```bash git clone https://github.com/dcSpark/mcp-cryptowallet-evm.git cd mcp-cryptowallet-evm ``` 2. Install dependencies: ```bash npm ci ``` 3. Build the project: ```bash npm run build ``` ## Configuration ### Environment Variables The MCP server supports the following environment variables: - `PRIVATE_KEY`: Optional private key to use for wallet operations when no wallet is explicitly provided ### Configure Claude Desktop To configure Claude Desktop to use this MCP server: 1. Open Claude Desktop 2. Navigate to the Claude Desktop configuration file: - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` - Windows: `%APPDATA%\Claude\claude_desktop_config.json` - Linux: `~/.config/Claude/claude_desktop_config.json` 3. Add the MCP server configuration: ```json { "mcpServers": { "mcp-cryptowallet-evm": { "command": "npx", "args": [ "@mcp-dockmaster/mcp-cryptowallet-evm" ] } } } ``` Alternatively, if you installed the package locally: ```json { "mcpServers": { "mcp-cryptowallet-evm": { "command": "node", "args": [ "/path/to/your/mcp-cryptowallet-evm/build/index.js" ] } } } ``` ### Running Locally ```bash node build/index.js ``` ## Usage Once configured, restart Claude Desktop. Claude will now have access to the Ethereum and EVM-compatible blockchain tools. You can ask Claude to: 1. Create a new wallet: ``` Can you create a new Ethereum wallet for me? ``` 2. Check a wallet balance: ``` What's the balance of the Ethereum wallet address 0x742d35Cc6634C0532925a3b844Bc454e4438f44e? ``` 3. Send a transaction: ``` Can you help me send 0.1 ETH to 0x742d35Cc6634C0532925a3b844Bc454e4438f44e? ``` Claude will use the MCP server to interact with the Ethereum blockchain directly. ## Development ### Adding New Tools To add new tools to the MCP server: 1. Define the tool in `src/tools.ts` 2. Create a handler function in the appropriate handler file 3. Add the handler to the `handlers` object in `src/tools.ts` ### Building ```bash npm run build ``` ## License MIT ``` -------------------------------------------------------------------------------- /.eslintrc/index.js: -------------------------------------------------------------------------------- ```javascript module.exports = { root: true, parser: '@typescript-eslint/parser', plugins: ['@typescript-eslint'], extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended' ], rules: { '@typescript-eslint/explicit-module-boundary-types': 'off' } } ``` -------------------------------------------------------------------------------- /src/handlers/wallet.types.ts: -------------------------------------------------------------------------------- ```typescript export type fromPrivateKeyHandlerInput = { privateKey: string; provider?: string; }; export type createMnemonicPhraseHandlerInput = { locale?: string; length?: 12 | 15 | 18 | 21 | 24; }; export type fromMnemonicHandlerInput = { mnemonic: string; provider?: string; path?: string; locale?: string; }; ``` -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- ```javascript /** @type {import('ts-jest').JestConfigWithTsJest} */ export default { preset: 'ts-jest', testEnvironment: 'node', extensionsToTreatAsEsm: ['.ts'], moduleNameMapper: { '^(\\.{1,2}/.*)\\.js$': '$1', }, transform: { '^.+\\.tsx?$': [ 'ts-jest', { useESM: true, }, ], }, }; ``` -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- ```typescript export type ToolResultSchema = { content: ToolResultContent[]; isError?: boolean; } export type ToolResultContent = { type: "text"; text: string; } | { type: "image"; data: string; // base64 encoded image data mimeType: string; } | { type: "resource"; resource: { url: string; mimeType: string; text: string; } } ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2020", "module": "NodeNext", "moduleResolution": "NodeNext", "esModuleInterop": true, "outDir": "build", "strict": true, "declaration": true, "sourceMap": true, "resolveJsonModule": true, "allowSyntheticDefaultImports": true, "skipLibCheck": true }, "include": ["src/**/*"], "exclude": ["node_modules", "build", "tests"] } ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js"; import { handlers, tools } from "./tools.js"; const server = new Server({ name: "mcp-cryptowallet-evm", version: "1.0.0", }, { capabilities: { tools: {} } }); const transport = new StdioServerTransport(); await server.connect(transport); server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { const handler = handlers[request.params.name]; if (handler) { try { const input = request.params.arguments; return await handler(input); } catch (error) { return { toolResult: { error: (error as Error).message }, content: [], isError: true }; } } return { toolResult: { error: "Method not found" }, content: [], isError: true }; }); ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "@mcp-dockmaster/mcp-cryptowallet-evm", "version": "1.0.6", "description": "MCP server for EVM crypto wallet operations using ethers.js v5", "main": "build/index.js", "type": "module", "bin": { "mcp-cryptowallet-evm": "./build/index.js" }, "scripts": { "build": "tsc", "start": "node build/index.js", "test": "node --test ./tests/**/*.test.ts", "lint": "eslint src/**/*.ts" }, "repository": { "type": "git", "url": "git+https://github.com/dcSpark/mcp-cryptowallet-evm.git" }, "keywords": [ "mcp", "ethereum", "evm", "wallet", "ethers" ], "author": "dcSpark", "license": "MIT", "bugs": { "url": "https://github.com/dcSpark/mcp-cryptowallet-evm/issues" }, "homepage": "https://github.com/dcSpark/mcp-cryptowallet-evm#readme", "dependencies": { "@modelcontextprotocol/sdk": "^1.6.1", "@scure/bip39": "^1.5.4", "ethers": "^5.7.2" }, "devDependencies": { "@types/jest": "^29.5.0", "@types/node": "^18.15.11", "@typescript-eslint/eslint-plugin": "^5.57.1", "@typescript-eslint/parser": "^5.57.1", "eslint": "^8.38.0", "jest": "^29.5.0", "ts-jest": "^29.1.0", "typescript": "^5.0.4" } } ``` -------------------------------------------------------------------------------- /tests/handlers/network.test.ts: -------------------------------------------------------------------------------- ```typescript import { ethers } from 'ethers'; import { getNetworkHandler, getBlockNumberHandler, getFeeDataHandler } from '../../src/handlers/wallet.js'; // Mock ethers.js functions jest.mock('ethers', () => { const originalModule = jest.requireActual('ethers'); // Create a mock provider const mockProvider = { getNetwork: jest.fn().mockResolvedValue({ name: 'homestead', chainId: 1, ensAddress: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e' }), getBlockNumber: jest.fn().mockResolvedValue(1000000), getFeeData: jest.fn().mockResolvedValue({ gasPrice: originalModule.utils.parseUnits('50', 'gwei'), maxFeePerGas: originalModule.utils.parseUnits('100', 'gwei'), maxPriorityFeePerGas: originalModule.utils.parseUnits('2', 'gwei') }) }; return { ...originalModule, providers: { JsonRpcProvider: jest.fn().mockReturnValue(mockProvider) }, getDefaultProvider: jest.fn().mockReturnValue(mockProvider), utils: originalModule.utils }; }); describe('Network Methods Handlers', () => { test('getNetworkHandler should return network information', async () => { const result = await getNetworkHandler({ provider: 'https://mainnet.infura.io/v3/your-api-key' }); expect(result.isError).toBe(false); expect(result.toolResult).toHaveProperty('network'); expect(result.toolResult.network).toHaveProperty('name'); expect(result.toolResult.network).toHaveProperty('chainId'); expect(result.toolResult.network).toHaveProperty('ensAddress'); expect(result.toolResult.network.name).toBe('homestead'); expect(result.toolResult.network.chainId).toBe(1); }); test('getBlockNumberHandler should return the current block number', async () => { const result = await getBlockNumberHandler({ provider: 'https://mainnet.infura.io/v3/your-api-key' }); expect(result.isError).toBe(false); expect(result.toolResult).toHaveProperty('blockNumber'); expect(result.toolResult.blockNumber).toBe(1000000); }); test('getFeeDataHandler should return fee data', async () => { const result = await getFeeDataHandler({ provider: 'https://mainnet.infura.io/v3/your-api-key' }); expect(result.isError).toBe(false); expect(result.toolResult).toHaveProperty('feeData'); expect(result.toolResult.feeData).toHaveProperty('gasPrice'); expect(result.toolResult.feeData).toHaveProperty('maxFeePerGas'); expect(result.toolResult.feeData).toHaveProperty('maxPriorityFeePerGas'); }); }); ``` -------------------------------------------------------------------------------- /required-methods.md: -------------------------------------------------------------------------------- ```markdown # Required Methods for EVM Crypto Wallet MCP Server Based on the ethers.js v5 documentation and the example MCP servers, the following methods should be implemented for our crypto wallet MCP server: ## Wallet Creation and Management - `wallet_create_random` - Create a new wallet with a random private key - `wallet_from_private_key` - Create a wallet from a private key - `wallet_from_mnemonic` - Create a wallet from a mnemonic phrase - `wallet_from_encrypted_json` - Create a wallet by decrypting an encrypted JSON wallet - `wallet_encrypt` - Encrypt a wallet with a password ## Wallet Properties - `wallet_get_address` - Get the wallet address - `wallet_get_public_key` - Get the wallet public key - `wallet_get_private_key` - Get the wallet private key (with appropriate security warnings) - `wallet_get_mnemonic` - Get the wallet mnemonic phrase (if available) ## Blockchain Methods - `wallet_get_balance` - Get the balance of the wallet - `wallet_get_chain_id` - Get the chain ID the wallet is connected to - `wallet_get_gas_price` - Get the current gas price - `wallet_get_transaction_count` - Get the number of transactions sent from this account (nonce) - `wallet_call` - Call a contract method without sending a transaction ## Transaction Methods - `wallet_send_transaction` - Send a transaction - `wallet_sign_transaction` - Sign a transaction without sending it - `wallet_populate_transaction` - Populate a transaction with missing fields ## Signing Methods - `wallet_sign_message` - Sign a message - `wallet_sign_typed_data` - Sign typed data (EIP-712) - `wallet_verify_message` - Verify a signed message - `wallet_verify_typed_data` - Verify signed typed data ## Provider Methods - `provider_get_block` - Get a block by number or hash - `provider_get_transaction` - Get a transaction by hash - `provider_get_transaction_receipt` - Get a transaction receipt - `provider_get_code` - Get the code at an address - `provider_get_storage_at` - Get the storage at a position for an address - `provider_estimate_gas` - Estimate the gas required for a transaction - `provider_get_logs` - Get logs that match a filter - `provider_get_ens_resolver` - Get the ENS resolver for a name - `provider_lookup_address` - Lookup the ENS name for an address - `provider_resolve_name` - Resolve an ENS name to an address ## Network Methods - `network_get_network` - Get the current network information - `network_get_block_number` - Get the current block number - `network_get_fee_data` - Get the current fee data (base fee, max priority fee, etc.) These methods cover the core functionality needed for a comprehensive EVM crypto wallet implementation using ethers.js v5. ``` -------------------------------------------------------------------------------- /src/handlers/utils.ts: -------------------------------------------------------------------------------- ```typescript import { ethers } from "ethers"; import { ToolResultSchema } from "../types.js"; let provider: ethers.providers.Provider | undefined; try { provider = process.env.PROVIDER_URL ? ethers.providers.getDefaultProvider(process.env.PROVIDER_URL) : ethers.providers.getDefaultProvider('https://eth.llamarpc.com'); } catch (error) { console.error("Error initializing provider:", error); } /** * Creates a success response with the given message * @param message Optional message to include in the response * @returns A ToolResultSchema with the message */ export const createSuccessResponse = (message?: string): ToolResultSchema => { return { content: [ { type: "text", text: message || "Operation completed successfully" } ], isError: false, }; }; /** * Creates an error response with the given message * @param message The error message * @returns A ToolResultSchema with the error message */ export const createErrorResponse = (message: string): ToolResultSchema => { return { content: [ { type: "text", text: message } ], isError: true }; }; /** * Gets the provider setup in memory * @returns An ethers.js provider */ export const getProvider = (): ethers.providers.Provider => { if (!provider) { throw new Error(`Invalid provider URL: ${process.env.PROVIDER_URL}`); } return provider }; export const setProvider = (providerURL: string) => { try { provider = ethers.providers.getDefaultProvider(providerURL); } catch (error) { throw new Error(`Invalid provider URL: ${providerURL}`); } }; /** * Gets a wallet from a private key, mnemonic, or JSON wallet * @param walletData The wallet data (private key, mnemonic, or JSON) * @param password Optional password for encrypted JSON wallets * @param provider Optional provider to connect the wallet to * @returns An ethers.js wallet */ export const getWallet = async ( walletData?: string, password?: string, ): Promise<ethers.Wallet> => { const provider = getProvider() // If walletData is not provided, check for PRIVATE_KEY environment variable if (!walletData && process.env.PRIVATE_KEY) { const wallet = new ethers.Wallet(process.env.PRIVATE_KEY); return provider ? wallet.connect(provider) : wallet; } // If no walletData and no environment variable, throw an error if (!walletData) { throw new Error("Wallet data is required or set PRIVATE_KEY environment variable"); } try { // Try to parse as JSON first if (walletData.startsWith("{")) { if (!password) { throw new Error("Password is required for encrypted JSON wallets"); } const wallet = await ethers.Wallet.fromEncryptedJson(walletData, password); return provider ? wallet.connect(provider) : wallet; } // Check if it's a mnemonic (12, 15, 18, 21, or 24 words) const words = walletData.trim().split(/\s+/); if ([12, 15, 18, 21, 24].includes(words.length)) { const wallet = ethers.Wallet.fromMnemonic(walletData); return provider ? wallet.connect(provider) : wallet; } // Assume it's a private key const wallet = new ethers.Wallet(walletData); return provider ? wallet.connect(provider) : wallet; } catch (error) { throw new Error(`Invalid wallet data: ${(error as Error).message}`); } }; ``` -------------------------------------------------------------------------------- /tests/handlers/provider.test.ts: -------------------------------------------------------------------------------- ```typescript import { ethers } from 'ethers'; import { getBlockHandler, getTransactionHandler, getTransactionReceiptHandler, getCodeHandler, getStorageAtHandler, estimateGasHandler, getLogsHandler, getEnsResolverHandler, lookupAddressHandler, resolveNameHandler } from '../../src/handlers/wallet.js'; // Mock ethers.js functions jest.mock('ethers', () => { const originalModule = jest.requireActual('ethers'); // Create a mock provider const mockProvider = { getBlock: jest.fn().mockResolvedValue({ hash: '0xblock', number: 1000000, timestamp: Date.now() / 1000, transactions: ['0xtx1', '0xtx2'] }), getTransaction: jest.fn().mockResolvedValue({ hash: '0xtx', from: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', to: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e' }), getTransactionReceipt: jest.fn().mockResolvedValue({ status: 1, blockNumber: 1000000, gasUsed: originalModule.BigNumber.from(21000) }), getCode: jest.fn().mockResolvedValue('0x'), getStorageAt: jest.fn().mockResolvedValue('0x0000000000000000000000000000000000000000000000000000000000000000'), estimateGas: jest.fn().mockResolvedValue(originalModule.BigNumber.from(21000)), getLogs: jest.fn().mockResolvedValue([{ blockNumber: 1000000, blockHash: '0xblock', transactionIndex: 0, removed: false, address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', data: '0x', topics: ['0xtopic1', '0xtopic2'] }]), getResolver: jest.fn().mockResolvedValue({ address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', name: 'test.eth' }), lookupAddress: jest.fn().mockResolvedValue('test.eth'), resolveName: jest.fn().mockResolvedValue('0x742d35Cc6634C0532925a3b844Bc454e4438f44e') }; return { ...originalModule, providers: { JsonRpcProvider: jest.fn().mockReturnValue(mockProvider) }, getDefaultProvider: jest.fn().mockReturnValue(mockProvider), utils: originalModule.utils }; }); describe('Provider Methods Handlers', () => { test('getBlockHandler should return a block', async () => { const result = await getBlockHandler({ provider: 'https://mainnet.infura.io/v3/your-api-key', blockHashOrBlockTag: 'latest', includeTransactions: true }); expect(result.isError).toBe(false); expect(result.toolResult).toHaveProperty('block'); expect(result.toolResult.block).toHaveProperty('hash'); expect(result.toolResult.block).toHaveProperty('number'); expect(result.toolResult.block).toHaveProperty('timestamp'); expect(result.toolResult.block).toHaveProperty('transactions'); }); test('getTransactionHandler should return a transaction', async () => { const result = await getTransactionHandler({ provider: 'https://mainnet.infura.io/v3/your-api-key', transactionHash: '0xtx' }); expect(result.isError).toBe(false); expect(result.toolResult).toHaveProperty('transaction'); expect(result.toolResult.transaction).toHaveProperty('hash'); expect(result.toolResult.transaction).toHaveProperty('from'); expect(result.toolResult.transaction).toHaveProperty('to'); }); test('getTransactionReceiptHandler should return a transaction receipt', async () => { const result = await getTransactionReceiptHandler({ provider: 'https://mainnet.infura.io/v3/your-api-key', transactionHash: '0xtx' }); expect(result.isError).toBe(false); expect(result.toolResult).toHaveProperty('receipt'); expect(result.toolResult.receipt).toHaveProperty('status'); expect(result.toolResult.receipt).toHaveProperty('blockNumber'); expect(result.toolResult.receipt).toHaveProperty('gasUsed'); }); test('getCodeHandler should return code at an address', async () => { const result = await getCodeHandler({ provider: 'https://mainnet.infura.io/v3/your-api-key', address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e' }); expect(result.isError).toBe(false); expect(result.toolResult).toHaveProperty('code'); expect(result.toolResult.code).toBe('0x'); }); test('getStorageAtHandler should return storage at a position', async () => { const result = await getStorageAtHandler({ provider: 'https://mainnet.infura.io/v3/your-api-key', address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', position: '0x0' }); expect(result.isError).toBe(false); expect(result.toolResult).toHaveProperty('storage'); expect(result.toolResult.storage).toBe('0x0000000000000000000000000000000000000000000000000000000000000000'); }); test('estimateGasHandler should estimate gas for a transaction', async () => { const result = await estimateGasHandler({ provider: 'https://mainnet.infura.io/v3/your-api-key', transaction: { to: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', data: '0x' } }); expect(result.isError).toBe(false); expect(result.toolResult).toHaveProperty('gasEstimate'); expect(result.toolResult.gasEstimate).toBe('21000'); }); test('getLogsHandler should return logs matching a filter', async () => { const result = await getLogsHandler({ provider: 'https://mainnet.infura.io/v3/your-api-key', filter: { address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', fromBlock: 'latest', toBlock: 'latest' } }); expect(result.isError).toBe(false); expect(result.toolResult).toHaveProperty('logs'); expect(Array.isArray(result.toolResult.logs)).toBe(true); expect(result.toolResult.logs.length).toBe(1); expect(result.toolResult.logs[0]).toHaveProperty('blockNumber'); expect(result.toolResult.logs[0]).toHaveProperty('topics'); }); test('getEnsResolverHandler should return an ENS resolver', async () => { const result = await getEnsResolverHandler({ provider: 'https://mainnet.infura.io/v3/your-api-key', name: 'test.eth' }); expect(result.isError).toBe(false); expect(result.toolResult).toHaveProperty('resolver'); expect(result.toolResult.resolver).toHaveProperty('address'); expect(result.toolResult.resolver).toHaveProperty('name'); }); test('lookupAddressHandler should lookup an ENS name for an address', async () => { const result = await lookupAddressHandler({ provider: 'https://mainnet.infura.io/v3/your-api-key', address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e' }); expect(result.isError).toBe(false); expect(result.toolResult).toHaveProperty('name'); expect(result.toolResult.name).toBe('test.eth'); }); test('resolveNameHandler should resolve an ENS name to an address', async () => { const result = await resolveNameHandler({ provider: 'https://mainnet.infura.io/v3/your-api-key', name: 'test.eth' }); expect(result.isError).toBe(false); expect(result.toolResult).toHaveProperty('address'); expect(result.toolResult.address).toBe('0x742d35Cc6634C0532925a3b844Bc454e4438f44e'); }); }); ``` -------------------------------------------------------------------------------- /tests/handlers/wallet.test.ts: -------------------------------------------------------------------------------- ```typescript import { ethers } from 'ethers'; import { createWalletHandler, fromPrivateKeyHandler, fromMnemonicHandler, fromEncryptedJsonHandler, encryptWalletHandler, getAddressHandler, getPublicKeyHandler, getPrivateKeyHandler, getMnemonicHandler, getBalanceHandler, getChainIdHandler, getGasPriceHandler, getTransactionCountHandler, callHandler, signMessageHandler, verifyMessageHandler } from '../../src/handlers/wallet.js'; // Mock ethers.js functions jest.mock('ethers', () => { const originalModule = jest.requireActual('ethers'); // Create a mock wallet const mockWallet = { address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', privateKey: '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', publicKey: '0x04a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6', mnemonic: { phrase: 'test test test test test test test test test test test junk', path: "m/44'/60'/0'/0/0", locale: 'en' }, connect: jest.fn().mockImplementation((provider) => { mockWallet.provider = provider; return mockWallet; }), getBalance: jest.fn().mockResolvedValue(originalModule.utils.parseEther('1.0')), getChainId: jest.fn().mockResolvedValue(1), getGasPrice: jest.fn().mockResolvedValue(originalModule.utils.parseUnits('50', 'gwei')), getTransactionCount: jest.fn().mockResolvedValue(5), call: jest.fn().mockResolvedValue('0x0000000000000000000000000000000000000000000000000000000000000001'), signMessage: jest.fn().mockResolvedValue('0xsignature'), encrypt: jest.fn().mockResolvedValue(JSON.stringify({ version: 3, id: 'test', address: '742d35cc6634c0532925a3b844bc454e4438f44e' })), provider: null }; // Create a mock provider const mockProvider = { getBalance: jest.fn().mockResolvedValue(originalModule.utils.parseEther('1.0')), getBlock: jest.fn().mockResolvedValue({ hash: '0xblock', number: 1000000, timestamp: Date.now() / 1000 }), getTransaction: jest.fn().mockResolvedValue({ hash: '0xtx', from: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', to: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e' }), getTransactionReceipt: jest.fn().mockResolvedValue({ status: 1, blockNumber: 1000000 }), getCode: jest.fn().mockResolvedValue('0x'), getStorageAt: jest.fn().mockResolvedValue('0x0000000000000000000000000000000000000000000000000000000000000000'), estimateGas: jest.fn().mockResolvedValue(ethers.BigNumber.from(21000)), getLogs: jest.fn().mockResolvedValue([]), getResolver: jest.fn().mockResolvedValue(null), lookupAddress: jest.fn().mockResolvedValue(null), resolveName: jest.fn().mockResolvedValue(null), getNetwork: jest.fn().mockResolvedValue({ name: 'homestead', chainId: 1 }), getBlockNumber: jest.fn().mockResolvedValue(1000000), getFeeData: jest.fn().mockResolvedValue({ gasPrice: ethers.utils.parseUnits('50', 'gwei'), maxFeePerGas: ethers.utils.parseUnits('100', 'gwei'), maxPriorityFeePerGas: ethers.utils.parseUnits('2', 'gwei') }) }; return { ...originalModule, Wallet: { createRandom: jest.fn().mockReturnValue(mockWallet), fromMnemonic: jest.fn().mockReturnValue(mockWallet), fromEncryptedJson: jest.fn().mockResolvedValue(mockWallet) }, providers: { JsonRpcProvider: jest.fn().mockReturnValue(mockProvider) }, getDefaultProvider: jest.fn().mockReturnValue(mockProvider), utils: originalModule.utils }; }); describe('Wallet Creation and Management Handlers', () => { test('createWalletHandler should create a random wallet', async () => { const result = await createWalletHandler({}); expect(result.isError).toBe(false); expect(result.toolResult).toHaveProperty('address'); expect(result.toolResult).toHaveProperty('privateKey'); expect(result.toolResult).toHaveProperty('publicKey'); expect(result.toolResult).toHaveProperty('mnemonic'); }); test('createWalletHandler should encrypt wallet if password is provided', async () => { const result = await createWalletHandler({ password: 'test123' }); expect(result.isError).toBe(false); expect(result.toolResult).toHaveProperty('encryptedWallet'); }); test('fromPrivateKeyHandler should create a wallet from a private key', async () => { const result = await fromPrivateKeyHandler({ privateKey: '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' }); expect(result.isError).toBe(false); expect(result.toolResult).toHaveProperty('address'); expect(result.toolResult).toHaveProperty('privateKey'); expect(result.toolResult).toHaveProperty('publicKey'); }); test('fromMnemonicHandler should create a wallet from a mnemonic', async () => { const result = await fromMnemonicHandler({ mnemonic: 'test test test test test test test test test test test junk' }); expect(result.isError).toBe(false); expect(result.toolResult).toHaveProperty('address'); expect(result.toolResult).toHaveProperty('privateKey'); expect(result.toolResult).toHaveProperty('publicKey'); expect(result.toolResult).toHaveProperty('mnemonic'); }); test('fromEncryptedJsonHandler should create a wallet from encrypted JSON', async () => { const result = await fromEncryptedJsonHandler({ json: JSON.stringify({ version: 3, id: 'test', address: '742d35cc6634c0532925a3b844bc454e4438f44e' }), password: 'test123' }); expect(result.isError).toBe(false); expect(result.toolResult).toHaveProperty('address'); expect(result.toolResult).toHaveProperty('privateKey'); expect(result.toolResult).toHaveProperty('publicKey'); }); test('encryptWalletHandler should encrypt a wallet', async () => { const result = await encryptWalletHandler({ wallet: '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', password: 'test123' }); expect(result.isError).toBe(false); expect(result.toolResult).toHaveProperty('encryptedWallet'); }); }); describe('Wallet Properties Handlers', () => { test('getAddressHandler should return the wallet address', async () => { const result = await getAddressHandler({ wallet: '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' }); expect(result.isError).toBe(false); expect(result.toolResult).toHaveProperty('address'); expect(result.toolResult.address).toBe('0x742d35Cc6634C0532925a3b844Bc454e4438f44e'); }); test('getPublicKeyHandler should return the wallet public key', async () => { const result = await getPublicKeyHandler({ wallet: '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' }); expect(result.isError).toBe(false); expect(result.toolResult).toHaveProperty('publicKey'); }); test('getPrivateKeyHandler should return the wallet private key', async () => { const result = await getPrivateKeyHandler({ wallet: '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' }); expect(result.isError).toBe(false); expect(result.toolResult).toHaveProperty('privateKey'); expect(result.toolResult.privateKey).toBe('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'); }); test('getMnemonicHandler should return the wallet mnemonic', async () => { const result = await getMnemonicHandler({ wallet: 'test test test test test test test test test test test junk' }); expect(result.isError).toBe(false); expect(result.toolResult).toHaveProperty('mnemonic'); expect(result.toolResult.mnemonic).toBe('test test test test test test test test test test test junk'); }); }); describe('Blockchain Methods Handlers', () => { test('getBalanceHandler should return the wallet balance', async () => { const result = await getBalanceHandler({ wallet: '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', provider: 'https://mainnet.infura.io/v3/your-api-key' }); expect(result.isError).toBe(false); expect(result.toolResult).toHaveProperty('balance'); expect(result.toolResult).toHaveProperty('balanceInEth'); expect(result.toolResult.balanceInEth).toBe('1.0'); }); test('getChainIdHandler should return the chain ID', async () => { const result = await getChainIdHandler({ wallet: '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', provider: 'https://mainnet.infura.io/v3/your-api-key' }); expect(result.isError).toBe(false); expect(result.toolResult).toHaveProperty('chainId'); expect(result.toolResult.chainId).toBe(1); }); test('getGasPriceHandler should return the gas price', async () => { const result = await getGasPriceHandler({ wallet: '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', provider: 'https://mainnet.infura.io/v3/your-api-key' }); expect(result.isError).toBe(false); expect(result.toolResult).toHaveProperty('gasPrice'); expect(result.toolResult).toHaveProperty('gasPriceInGwei'); expect(result.toolResult.gasPriceInGwei).toBe('50.0'); }); test('getTransactionCountHandler should return the transaction count', async () => { const result = await getTransactionCountHandler({ wallet: '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', provider: 'https://mainnet.infura.io/v3/your-api-key' }); expect(result.isError).toBe(false); expect(result.toolResult).toHaveProperty('transactionCount'); expect(result.toolResult.transactionCount).toBe(5); }); test('callHandler should call a contract method', async () => { const result = await callHandler({ wallet: '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', provider: 'https://mainnet.infura.io/v3/your-api-key', transaction: { to: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', data: '0x70a08231000000000000000000000000742d35cc6634c0532925a3b844bc454e4438f44e' } }); expect(result.isError).toBe(false); expect(result.toolResult).toHaveProperty('result'); }); }); describe('Signing Methods Handlers', () => { test('signMessageHandler should sign a message', async () => { const result = await signMessageHandler({ wallet: '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', message: 'Hello, world!' }); expect(result.isError).toBe(false); expect(result.toolResult).toHaveProperty('signature'); expect(result.toolResult).toHaveProperty('message'); expect(result.toolResult.signature).toBe('0xsignature'); expect(result.toolResult.message).toBe('Hello, world!'); }); test('verifyMessageHandler should verify a signed message', async () => { const result = await verifyMessageHandler({ message: 'Hello, world!', signature: '0xsignature', address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e' }); expect(result.isError).toBe(false); expect(result.toolResult).toHaveProperty('isValid'); expect(result.toolResult).toHaveProperty('recoveredAddress'); }); }); ``` -------------------------------------------------------------------------------- /src/tools.ts: -------------------------------------------------------------------------------- ```typescript import { createWalletHandler, fromPrivateKeyHandler, fromMnemonicHandler, fromEncryptedJsonHandler, encryptWalletHandler, getAddressHandler, getPublicKeyHandler, getPrivateKeyHandler, getBalanceHandler, getChainIdHandler, getGasPriceHandler, getTransactionCountHandler, callHandler, sendTransactionHandler, signTransactionHandler, populateTransactionHandler, signMessageHandler, signTypedDataHandler, verifyMessageHandler, verifyTypedDataHandler, getBlockHandler, getTransactionHandler, getTransactionReceiptHandler, getCodeHandler, getStorageAtHandler, estimateGasHandler, getLogsHandler, getEnsResolverHandler, lookupAddressHandler, resolveNameHandler, getNetworkHandler, getBlockNumberHandler, getFeeDataHandler, createMnemonicPhraseHandler, setProviderHandler } from "./handlers/wallet.js"; export const tools = [ { name: "wallet_provider_set", description: "Set the provider URL. By default, the provider URL is set to the ETH mainnet or the URL set in the PROVIDER_URL environment variable.", inputSchema: { type: "object", properties: { providerURL: { type: "string", description: "The provider RPC URL" } }, required: ["providerURL"] } }, // Wallet Creation and Management { name: "wallet_create_random", description: "Create a new wallet with a random private key", inputSchema: { type: "object", properties: { password: { type: "string", description: "Optional password to encrypt the wallet" }, path: { type: "string", description: "Optional HD path" }, locale: { type: "string", description: "Optional locale for the wordlist" } }, required: [] } }, { name: "wallet_from_private_key", description: "Create a wallet from a private key", inputSchema: { type: "object", properties: { privateKey: { type: "string", description: "The private key" } }, required: ["privateKey"] } }, { name: "wallet_create_mnemonic_phrase", description: "Create a mnemonic phrase", inputSchema: { type: "object", properties: { length: { type: "number", description: "The length of the mnemonic phrase", enum: [12, 15, 18, 21, 24] }, locale: { type: "string", description: "Optional locale for the wordlist" } }, required: ["length"] } }, { name: "wallet_from_mnemonic", description: "Create a wallet from a mnemonic phrase", inputSchema: { type: "object", properties: { mnemonic: { type: "string", description: "The mnemonic phrase" }, path: { type: "string", description: "Optional HD path" }, locale: { type: "string", description: "Optional locale for the wordlist" } }, required: ["mnemonic"] } }, { name: "wallet_from_encrypted_json", description: "Create a wallet by decrypting an encrypted JSON wallet", inputSchema: { type: "object", properties: { json: { type: "string", description: "The encrypted JSON wallet" }, password: { type: "string", description: "The password to decrypt the wallet" } }, required: ["json", "password"] } }, { name: "wallet_encrypt", description: "Encrypt a wallet with a password", inputSchema: { type: "object", properties: { wallet: { type: "string", description: "The wallet to encrypt (private key, mnemonic, or JSON)" }, password: { type: "string", description: "The password to encrypt the wallet" }, options: { type: "object", description: "Optional encryption options", properties: { scrypt: { type: "object", properties: { N: { type: "number" }, r: { type: "number" }, p: { type: "number" } } } } } }, required: ["wallet", "password"] } }, // Wallet Properties { name: "wallet_get_address", description: "Get the wallet address", inputSchema: { type: "object", properties: { wallet: { type: "string", description: "The wallet (private key, mnemonic, or JSON). If not provided, uses PRIVATE_KEY environment variable if set." } }, required: [] } }, { name: "wallet_get_public_key", description: "Get the wallet public key", inputSchema: { type: "object", properties: { wallet: { type: "string", description: "The wallet (private key, mnemonic, or JSON). If not provided, uses PRIVATE_KEY environment variable if set." } }, required: [] } }, { name: "wallet_get_private_key", description: "Get the wallet private key (with appropriate security warnings)", inputSchema: { type: "object", properties: { wallet: { type: "string", description: "The wallet (private key, mnemonic, or JSON). If not provided, uses PRIVATE_KEY environment variable if set." }, password: { type: "string", description: "The password to decrypt the wallet if it's encrypted" } }, required: [] } }, // Blockchain Methods { name: "wallet_get_balance", description: "Get the balance of the wallet", inputSchema: { type: "object", properties: { wallet: { type: "string", description: "The wallet (private key, mnemonic, or JSON). If not provided, uses PRIVATE_KEY environment variable if set." }, blockTag: { type: "string", description: "Optional block tag (latest, pending, etc.)" } }, required: [] } }, { name: "wallet_get_chain_id", description: "Get the chain ID the wallet is connected to", inputSchema: { type: "object", properties: { wallet: { type: "string", description: "The wallet (private key, mnemonic, or JSON). If not provided, uses PRIVATE_KEY environment variable if set." } }, required: [] } }, { name: "wallet_get_gas_price", description: "Get the current gas price", inputSchema: { type: "object", properties: { wallet: { type: "string", description: "The wallet (private key, mnemonic, or JSON). If not provided, uses PRIVATE_KEY environment variable if set." } }, required: [] } }, { name: "wallet_get_transaction_count", description: "Get the number of transactions sent from this account (nonce)", inputSchema: { type: "object", properties: { wallet: { type: "string", description: "The wallet (private key, mnemonic, or JSON). If not provided, uses PRIVATE_KEY environment variable if set." }, blockTag: { type: "string", description: "Optional block tag (latest, pending, etc.)" } }, required: [] } }, { name: "wallet_call", description: "Call a contract method without sending a transaction", inputSchema: { type: "object", properties: { wallet: { type: "string", description: "The wallet (private key, mnemonic, or JSON). If not provided, uses PRIVATE_KEY environment variable if set." }, transaction: { type: "object", description: "The transaction to call", properties: { to: { type: "string" }, from: { type: "string" }, data: { type: "string" }, value: { type: "string" }, gasLimit: { type: "string" }, gasPrice: { type: "string" } }, required: ["to"] }, blockTag: { type: "string", description: "Optional block tag (latest, pending, etc.)" } }, required: ["transaction"] } }, // Transaction Methods { name: "wallet_send_transaction", description: "Send a transaction", inputSchema: { type: "object", properties: { wallet: { type: "string", description: "The wallet (private key, mnemonic, or JSON). If not provided, uses PRIVATE_KEY environment variable if set." }, transaction: { type: "object", description: "The transaction to send", properties: { to: { type: "string" }, from: { type: "string" }, data: { type: "string" }, value: { type: "string" }, gasLimit: { type: "string" }, gasPrice: { type: "string" }, nonce: { type: "number" }, type: { type: "number" }, maxFeePerGas: { type: "string" }, maxPriorityFeePerGas: { type: "string" } }, required: ["to"] } }, required: ["transaction"] } }, { name: "wallet_sign_transaction", description: "Sign a transaction without sending it", inputSchema: { type: "object", properties: { wallet: { type: "string", description: "The wallet (private key, mnemonic, or JSON). If not provided, uses PRIVATE_KEY environment variable if set." }, transaction: { type: "object", description: "The transaction to sign", properties: { to: { type: "string" }, from: { type: "string" }, data: { type: "string" }, value: { type: "string" }, gasLimit: { type: "string" }, gasPrice: { type: "string" }, nonce: { type: "number" }, type: { type: "number" }, maxFeePerGas: { type: "string" }, maxPriorityFeePerGas: { type: "string" } }, required: ["to"] } }, required: ["transaction"] } }, { name: "wallet_populate_transaction", description: "Populate a transaction with missing fields", inputSchema: { type: "object", properties: { wallet: { type: "string", description: "The wallet (private key, mnemonic, or JSON). If not provided, uses PRIVATE_KEY environment variable if set." }, transaction: { type: "object", description: "The transaction to populate", properties: { to: { type: "string" }, from: { type: "string" }, data: { type: "string" }, value: { type: "string" }, gasLimit: { type: "string" }, gasPrice: { type: "string" }, nonce: { type: "number" }, type: { type: "number" }, maxFeePerGas: { type: "string" }, maxPriorityFeePerGas: { type: "string" } }, required: ["to"] } }, required: ["transaction"] } }, // Signing Methods { name: "wallet_sign_message", description: "Sign a message", inputSchema: { type: "object", properties: { wallet: { type: "string", description: "The wallet (private key, mnemonic, or JSON). If not provided, uses PRIVATE_KEY environment variable if set." }, message: { type: "string", description: "The message to sign" } }, required: ["message"] } }, { name: "wallet_sign_typed_data", description: "Sign typed data (EIP-712)", inputSchema: { type: "object", properties: { wallet: { type: "string", description: "The wallet (private key, mnemonic, or JSON). If not provided, uses PRIVATE_KEY environment variable if set." }, domain: { type: "object", description: "The domain data" }, types: { type: "object", description: "The type definitions" }, value: { type: "object", description: "The value to sign" } }, required: ["domain", "types", "value"] } }, { name: "wallet_verify_message", description: "Verify a signed message", inputSchema: { type: "object", properties: { message: { type: "string", description: "The original message" }, signature: { type: "string", description: "The signature to verify" }, address: { type: "string", description: "The address that supposedly signed the message" } }, required: ["message", "signature", "address"] } }, { name: "wallet_verify_typed_data", description: "Verify signed typed data", inputSchema: { type: "object", properties: { domain: { type: "object", description: "The domain data" }, types: { type: "object", description: "The type definitions" }, value: { type: "object", description: "The value that was signed" }, signature: { type: "string", description: "The signature to verify" }, address: { type: "string", description: "The address that supposedly signed the data" } }, required: ["domain", "types", "value", "signature", "address"] } }, // Provider Methods { name: "provider_get_block", description: "Get a block by number or hash", inputSchema: { type: "object", properties: { blockHashOrBlockTag: { type: "string", description: "Block hash or block tag (latest, pending, etc.)" }, includeTransactions: { type: "boolean", description: "Whether to include full transactions or just hashes" } }, required: ["blockHashOrBlockTag"] } }, { name: "provider_get_transaction", description: "Get a transaction by hash", inputSchema: { type: "object", properties: { transactionHash: { type: "string", description: "The transaction hash" } }, required: ["transactionHash"] } }, { name: "provider_get_transaction_receipt", description: "Get a transaction receipt", inputSchema: { type: "object", properties: { transactionHash: { type: "string", description: "The transaction hash" } }, required: ["transactionHash"] } }, { name: "provider_get_code", description: "Get the code at an address", inputSchema: { type: "object", properties: { address: { type: "string", description: "The address to get code from" }, blockTag: { type: "string", description: "Optional block tag (latest, pending, etc.)" } }, required: ["address"] } }, { name: "provider_get_storage_at", description: "Get the storage at a position for an address", inputSchema: { type: "object", properties: { address: { type: "string", description: "The address to get storage from" }, position: { type: "string", description: "The storage position" }, blockTag: { type: "string", description: "Optional block tag (latest, pending, etc.)" } }, required: ["address", "position"] } }, { name: "provider_estimate_gas", description: "Estimate the gas required for a transaction", inputSchema: { type: "object", properties: { transaction: { type: "object", description: "The transaction to estimate gas for", properties: { to: { type: "string" }, from: { type: "string" }, data: { type: "string" }, value: { type: "string" } } } }, required: ["transaction"] } }, { name: "provider_get_logs", description: "Get logs that match a filter", inputSchema: { type: "object", properties: { filter: { type: "object", description: "The filter to apply", properties: { address: { type: "string" }, topics: { type: "array", items: { type: "string" } }, fromBlock: { type: "string" }, toBlock: { type: "string" } } } }, required: ["filter"] } }, { name: "provider_get_ens_resolver", description: "Get the ENS resolver for a name", inputSchema: { type: "object", properties: { name: { type: "string", description: "The ENS name" } }, required: ["name"] } }, { name: "provider_lookup_address", description: "Lookup the ENS name for an address", inputSchema: { type: "object", properties: { address: { type: "string", description: "The address to lookup" } }, required: ["address"] } }, { name: "provider_resolve_name", description: "Resolve an ENS name to an address", inputSchema: { type: "object", properties: { name: { type: "string", description: "The ENS name to resolve" } }, required: ["name"] } }, // Network Methods { name: "network_get_network", description: "Get the current network information", inputSchema: { type: "object", properties: {}, required: [] } }, { name: "network_get_block_number", description: "Get the current block number", inputSchema: { type: "object", properties: {}, required: [] } }, { name: "network_get_fee_data", description: "Get the current fee data (base fee, max priority fee, etc.)", inputSchema: { type: "object", properties: {}, required: [] } } ]; type HandlerDictionary = Record<string, (input: any) => any>; export const handlers: HandlerDictionary = { // Provider Methods "wallet_provider_set": setProviderHandler, // Wallet Creation and Management "wallet_create_random": createWalletHandler, "wallet_from_private_key": fromPrivateKeyHandler, "wallet_from_mnemonic": fromMnemonicHandler, "wallet_from_encrypted_json": fromEncryptedJsonHandler, "wallet_encrypt": encryptWalletHandler, // Wallet Properties "wallet_get_address": getAddressHandler, "wallet_get_public_key": getPublicKeyHandler, "wallet_get_private_key": getPrivateKeyHandler, // Blockchain Methods "wallet_get_balance": getBalanceHandler, "wallet_get_chain_id": getChainIdHandler, "wallet_get_gas_price": getGasPriceHandler, "wallet_get_transaction_count": getTransactionCountHandler, "wallet_call": callHandler, // Transaction Methods "wallet_send_transaction": sendTransactionHandler, "wallet_sign_transaction": signTransactionHandler, "wallet_populate_transaction": populateTransactionHandler, // Signing Methods "wallet_sign_message": signMessageHandler, "wallet_sign_typed_data": signTypedDataHandler, "wallet_verify_message": verifyMessageHandler, "wallet_verify_typed_data": verifyTypedDataHandler, // Provider Methods "provider_get_block": getBlockHandler, "provider_get_transaction": getTransactionHandler, "provider_get_transaction_receipt": getTransactionReceiptHandler, "provider_get_code": getCodeHandler, "provider_get_storage_at": getStorageAtHandler, "provider_estimate_gas": estimateGasHandler, "provider_get_logs": getLogsHandler, "provider_get_ens_resolver": getEnsResolverHandler, "provider_lookup_address": lookupAddressHandler, "provider_resolve_name": resolveNameHandler, // Network Methods "network_get_network": getNetworkHandler, "network_get_block_number": getBlockNumberHandler, "network_get_fee_data": getFeeDataHandler, // Mnemonic Methods "wallet_create_mnemonic_phrase": createMnemonicPhraseHandler }; ``` -------------------------------------------------------------------------------- /src/handlers/wallet.ts: -------------------------------------------------------------------------------- ```typescript import { ethers, providers } from "ethers"; import { ToolResultSchema } from "../types.js"; import { createSuccessResponse, createErrorResponse, getProvider, getWallet, setProvider } from "./utils.js"; import { fromPrivateKeyHandlerInput, createMnemonicPhraseHandlerInput } from "./wallet.types.js"; import { generateMnemonic, } from '@scure/bip39'; // Provider handlers export const setProviderHandler = async (input: any): Promise<ToolResultSchema> => { try { setProvider(input.providerURL); return createSuccessResponse(`Provider set successfully: Provider URL: ${input.providerURL} `); } catch (error) { return createErrorResponse(`Failed to set provider: ${(error as Error).message}`); } }; // Wallet Creation and Management export const createWalletHandler = async (input: any): Promise<ToolResultSchema> => { try { const options: any = {}; if (input.path) { options.path = input.path; } if (input.locale) { options.locale = input.locale; } const wallet = ethers.Wallet.createRandom(options); let result: any = { address: wallet.address, mnemonic: wallet.mnemonic?.phrase, privateKey: wallet.privateKey, publicKey: wallet.publicKey }; if (input.password) { // If a password is provided, encrypt the wallet const encryptedWallet = await wallet.encrypt(input.password); result.encryptedWallet = encryptedWallet; } return createSuccessResponse(`Wallet created successfully: Address: ${wallet.address} Private Key: ${wallet.privateKey} Public Key: ${wallet.publicKey} Mnemonic: ${wallet.mnemonic?.phrase} Encrypted Wallet: ${result.encryptedWallet ? "Yes" : "No"} `); } catch (error) { return createErrorResponse(`Failed to create wallet: ${(error as Error).message}`); } }; export const fromPrivateKeyHandler = async (input: fromPrivateKeyHandlerInput): Promise<ToolResultSchema> => { try { if (!input.privateKey) { return createErrorResponse("Private key is required"); } const provider = getProvider() const wallet = new ethers.Wallet(input.privateKey, provider); return createSuccessResponse( `Wallet created from private key successfully: Address: ${wallet.address} Private Key: ${wallet.privateKey} Public Key: ${wallet.publicKey} `); } catch (error) { return createErrorResponse(`Failed to create wallet from private key: ${(error as Error).message}`); } }; export const createMnemonicPhraseHandler = async (input: createMnemonicPhraseHandlerInput): Promise<ToolResultSchema> => { try { const { wordlist } = await import(`@scure/bip39/wordlists/${input.locale || 'english'}`); if (!wordlist) { return createErrorResponse("Invalid locale"); } // Convert length to entropy bits (12 words = 128 bits, 15 words = 160 bits, etc) const entropyBits = ((input.length ?? 12) / 3) * 32; const mnemonic = generateMnemonic(wordlist, entropyBits); return createSuccessResponse( `Mnemonic phrase created successfully: Mnemonic: "${mnemonic}" `); } catch (error) { return createErrorResponse(`Failed to create mnemonic phrase: ${(error as Error).message}`); } }; export const fromMnemonicHandler = async (input: any): Promise<ToolResultSchema> => { try { if (!input.mnemonic) { return createErrorResponse("Mnemonic is required"); } const options: any = { path: input.path, wordlist: (input.locale && ethers.wordlists[input.locale]) || ethers.wordlists.en }; const provider = getProvider(); const wallet = ethers.Wallet.fromMnemonic(input.mnemonic, options.path, options.wordlist); if (provider) wallet.connect(provider); return createSuccessResponse( `Wallet created from mnemonic successfully: Address: ${wallet.address} Mnemonic: ${input.mnemonic} Private Key: ${wallet.privateKey} Public Key: ${wallet.publicKey} `); } catch (error) { return createErrorResponse(`Failed to create wallet from mnemonic: ${(error as Error).message}`); } }; export const fromEncryptedJsonHandler = async (input: any): Promise<ToolResultSchema> => { try { if (!input.json) { return createErrorResponse("Encrypted JSON is required"); } if (!input.password) { return createErrorResponse("Password is required"); } const wallet = await ethers.Wallet.fromEncryptedJson(input.json, input.password); const provider = getProvider() if (provider) { wallet.connect(provider); } return createSuccessResponse( `Wallet created from encrypted JSON successfully Address: ${wallet.address} Private Key: ${wallet.privateKey} Public Key: ${wallet.publicKey} `); } catch (error) { return createErrorResponse(`Failed to create wallet from encrypted JSON: ${(error as Error).message}`); } }; export const encryptWalletHandler = async (input: any): Promise<ToolResultSchema> => { try { if (!input.wallet) { return createErrorResponse("Wallet is required"); } if (!input.password) { return createErrorResponse("Password is required"); } const wallet = await getWallet(input.wallet); const encryptedWallet = await wallet.encrypt(input.password, input.options); return createSuccessResponse( `Wallet encrypted successfully Encrypted Wallet: ${encryptedWallet} `); } catch (error) { return createErrorResponse(`Failed to encrypt wallet: ${(error as Error).message}`); } }; // Wallet Properties export const getAddressHandler = async (input: any): Promise<ToolResultSchema> => { try { const wallet = await getWallet(input.wallet); return createSuccessResponse( `Wallet address retrieved successfully: Address: ${wallet.address} `); } catch (error) { return createErrorResponse(`Failed to get wallet address: ${(error as Error).message}`); } }; export const getPublicKeyHandler = async (input: any): Promise<ToolResultSchema> => { try { const wallet = await getWallet(input.wallet); return createSuccessResponse( `Wallet public key retrieved successfully: Public Key: ${wallet.publicKey} `); } catch (error) { return createErrorResponse(`Failed to get wallet public key: ${(error as Error).message}`); } }; export const getPrivateKeyHandler = async (input: any): Promise<ToolResultSchema> => { try { const wallet = await getWallet(input.wallet, input.password); return createSuccessResponse( `Wallet private key retrieved successfully: Private Key: ${wallet.privateKey} `); } catch (error) { return createErrorResponse(`Failed to get wallet private key: ${(error as Error).message}`); } }; // Blockchain Methods export const getBalanceHandler = async (input: any): Promise<ToolResultSchema> => { try { const wallet = await getWallet(input.wallet, input.password); const balance = await wallet.getBalance(input.blockTag ?? "latest"); return createSuccessResponse( `Wallet balance retrieved successfully Balance: ${balance.toString()} Balance in ETH: ${ethers.utils.formatEther(balance)} `); } catch (error) { return createErrorResponse(`Failed to get wallet balance: ${(error as Error).message}`); } }; export const getChainIdHandler = async (input: any): Promise<ToolResultSchema> => { try { const wallet = await getWallet(input.wallet, input.password); if (!wallet.provider) { return createErrorResponse("Provider is required to get chain ID, please set the provider URL"); } const chainId = await wallet.getChainId(); return createSuccessResponse( `Chain ID retrieved successfully Chain ID: ${chainId.toString()} `); } catch (error) { return createErrorResponse(`Failed to get chain ID: ${(error as Error).message}`); } }; export const getGasPriceHandler = async (input: any): Promise<ToolResultSchema> => { try { const wallet = await getWallet(input.wallet, input.password); if (!wallet.provider) { return createErrorResponse("Provider is required to get gas price, please set the provider URL"); } const gasPrice = await wallet.getGasPrice(); return createSuccessResponse( `Gas price retrieved successfully Gas price: ${gasPrice.toString()} Gas price in Gwei: ${ethers.utils.formatUnits(gasPrice, "gwei")} `); } catch (error) { return createErrorResponse(`Failed to get gas price: ${(error as Error).message}`); } }; export const getTransactionCountHandler = async (input: any): Promise<ToolResultSchema> => { try { const wallet = await getWallet(input.wallet, input.password); if (!wallet.provider) { return createErrorResponse("Provider is required to get transaction count, please set the provider URL"); } const transactionCount = await wallet.getTransactionCount(input.blockTag); return createSuccessResponse( `Transaction count retrieved successfully Transaction count: ${transactionCount.toString()} `); } catch (error) { return createErrorResponse(`Failed to get transaction count: ${(error as Error).message}`); } }; export const callHandler = async (input: any): Promise<ToolResultSchema> => { try { if (!input.transaction) { return createErrorResponse("Transaction is required"); } const wallet = await getWallet(input.wallet, input.password); if (!wallet.provider) { return createErrorResponse("Provider is required to call a contract, please set the provider URL"); } const result = await wallet.call(input.transaction, input.blockTag); return createSuccessResponse( `Contract call executed successfully Result: ${result} `); } catch (error) { return createErrorResponse(`Failed to call contract: ${(error as Error).message}`); } }; // Transaction Methods export const sendTransactionHandler = async (input: any): Promise<ToolResultSchema> => { try { if (!input.transaction) { return createErrorResponse("Transaction is required"); } const wallet = await getWallet(input.wallet, input.password); if (!wallet.provider) { return createErrorResponse("Provider is required to send a transaction, please set the provider URL"); } const tx = await wallet.sendTransaction(input.transaction); return createSuccessResponse( `Transaction sent successfully Hash: ${tx.hash} Nonce: ${tx.nonce.toString()} Gas limit: ${tx.gasLimit.toString()} Gas price: ${tx.gasPrice?.toString()} Data: ${tx.data} `); } catch (error) { return createErrorResponse(`Failed to send transaction: ${(error as Error).message}`); } }; export const signTransactionHandler = async (input: any): Promise<ToolResultSchema> => { try { if (!input.transaction) { return createErrorResponse("Transaction is required"); } const wallet = await getWallet(input.wallet, input.password); // For signing a transaction, we need to populate it first const populatedTx = await wallet.populateTransaction(input.transaction); const signedTx = await wallet.signTransaction(populatedTx); return createSuccessResponse( `Transaction signed successfully Signed transaction: ${signedTx} `); } catch (error) { return createErrorResponse(`Failed to sign transaction: ${(error as Error).message}`); } }; export const populateTransactionHandler = async (input: any): Promise<ToolResultSchema> => { try { if (!input.transaction) { return createErrorResponse("Transaction is required"); } const wallet = await getWallet(input.wallet, input.password); if (!wallet.provider) { return createErrorResponse("Provider is required to populate a transaction, please set the provider URL"); } const populatedTx = await wallet.populateTransaction(input.transaction); return createSuccessResponse( `Transaction populated successfully To: ${populatedTx.to} From: ${populatedTx.from} Nonce: ${populatedTx.nonce?.toString() ?? "Not specified"} Gas limit: ${populatedTx.gasLimit?.toString() ?? "Not specified"} Gas price: ${populatedTx.gasPrice?.toString() ?? "Not specified"} `); } catch (error) { return createErrorResponse(`Failed to populate transaction: ${(error as Error).message}`); } }; // Signing Methods export const signMessageHandler = async (input: any): Promise<ToolResultSchema> => { try { if (!input.message) { return createErrorResponse("Message is required"); } const wallet = await getWallet(input.wallet, input.password); const signature = await wallet.signMessage(input.message); return createSuccessResponse(`Message signed successfully Signature: ${signature} Message: "${input.message}" `); } catch (error) { return createErrorResponse(`Failed to sign message: ${(error as Error).message}`); } }; export const signTypedDataHandler = async (input: any): Promise<ToolResultSchema> => { try { if (!input.wallet) { return createErrorResponse("Wallet is required"); } if (!input.domain || !input.types || !input.value) { return createErrorResponse("Domain, types, and value are required"); } const wallet = await getWallet(input.wallet, input.password); // Use ethers._signTypedData for EIP-712 signing // @ts-ignore - _signTypedData is not in the type definitions but is available const signature = await wallet._signTypedData(input.domain, input.types, input.value); return createSuccessResponse( `Typed data signed successfully Signature: ${signature} Domain: ${input.domain} Types: ${input.types} Value: ${input.value} `); } catch (error) { return createErrorResponse(`Failed to sign typed data: ${(error as Error).message}`); } }; export const verifyMessageHandler = async (input: any): Promise<ToolResultSchema> => { try { if (!input.message || !input.signature || !input.address) { return createErrorResponse("Message, signature, and address are required"); } const recoveredAddress = ethers.utils.verifyMessage(input.message, input.signature); const isValid = recoveredAddress.toLowerCase() === input.address.toLowerCase(); return createSuccessResponse( isValid ? `Signature verified successfully Message: "${input.message}" Signature: ${input.signature} Address: ${input.address} ` : `Signature verification failed Message: "${input.message}" Signature: ${input.signature} Address: ${input.address} `); } catch (error) { return createErrorResponse(`Failed to verify message: ${(error as Error).message}`); } }; export const verifyTypedDataHandler = async (input: any): Promise<ToolResultSchema> => { try { if (!input.domain || !input.types || !input.value || !input.signature || !input.address) { return createErrorResponse("Domain, types, value, signature, and address are required"); } // Use ethers.utils.verifyTypedData for EIP-712 verification const recoveredAddress = ethers.utils.verifyTypedData( input.domain, input.types, input.value, input.signature ); const isValid = recoveredAddress.toLowerCase() === input.address.toLowerCase(); return createSuccessResponse( isValid ? `Typed data signature is valid Domain: ${input.domain} Types: ${input.types} Value: ${input.value} Signature: ${input.signature} Address: ${input.address} ` : "Typed data signature is invalid"); } catch (error) { return createErrorResponse(`Failed to verify typed data: ${(error as Error).message}`); } }; // Provider Methods export const getBlockHandler = async (input: any): Promise<ToolResultSchema> => { try { if (!input.blockHashOrBlockTag) { return createErrorResponse("Block hash or block tag is required"); } const provider = getProvider(); // In ethers.js v5, getBlock can take includeTransactions as a second parameter // but TypeScript definitions might not reflect this const block = await (provider as any).getBlock(input.blockHashOrBlockTag, input.includeTransactions); return createSuccessResponse( `Block retrieved successfully Block hash: ${block.hash} Block number: ${block.number?.toString() ?? "Not specified"} Block timestamp: ${block.timestamp?.toString() ?? "Not specified"} Block transactions: ${block.transactions?.length ?? "Not specified"} `); } catch (error) { return createErrorResponse(`Failed to get block: ${(error as Error).message}`); } }; export const getTransactionHandler = async (input: any): Promise<ToolResultSchema> => { try { if (!input.transactionHash) { return createErrorResponse("Transaction hash is required"); } const provider = getProvider(); const transaction = await provider.getTransaction(input.transactionHash); return createSuccessResponse( `Transaction retrieved successfully Transaction hash: ${input.transactionHash} Transaction: ${transaction} `); } catch (error) { return createErrorResponse(`Failed to get transaction: ${(error as Error).message}`); } }; export const getTransactionReceiptHandler = async (input: any): Promise<ToolResultSchema> => { try { if (!input.transactionHash) { return createErrorResponse("Transaction hash is required"); } const provider = getProvider(); const receipt = await provider.getTransactionReceipt(input.transactionHash); return createSuccessResponse( `Transaction receipt retrieved successfully Transaction hash: ${input.transactionHash} Transaction receipt: ${receipt} `); } catch (error) { return createErrorResponse(`Failed to get transaction receipt: ${(error as Error).message}`); } }; export const getCodeHandler = async (input: any): Promise<ToolResultSchema> => { try { if (!input.address) { return createErrorResponse("Address is required"); } const provider = getProvider(); const code = await provider.getCode(input.address, input.blockTag); return createSuccessResponse( `Code retrieved successfully Address: ${input.address} Code: ${code} `); } catch (error) { return createErrorResponse(`Failed to get code: ${(error as Error).message}`); } }; export const getStorageAtHandler = async (input: any): Promise<ToolResultSchema> => { try { if (!input.address) { return createErrorResponse("Address is required"); } if (!input.position) { return createErrorResponse("Position is required"); } const provider = getProvider(); const storage = await provider.getStorageAt(input.address, input.position, input.blockTag); return createSuccessResponse( `Storage retrieved successfully Address: ${input.address} Position: ${input.position} Storage: ${storage} `); } catch (error) { return createErrorResponse(`Failed to get storage: ${(error as Error).message}`); } }; export const estimateGasHandler = async (input: any): Promise<ToolResultSchema> => { try { if (!input.transaction) { return createErrorResponse("Transaction is required"); } const provider = getProvider(); if (!provider) { return createErrorResponse("Provider is required to estimate gas, please set the provider URL"); } const gasEstimate = await provider.estimateGas(input.transaction); return createSuccessResponse( `Gas estimate retrieved successfully Gas estimate: ${gasEstimate.toString()} `); } catch (error) { return createErrorResponse(`Failed to estimate gas: ${(error as Error).message}`); } }; export const getLogsHandler = async (input: any): Promise<ToolResultSchema> => { try { if (!input.filter) { return createErrorResponse("Filter is required"); } const provider = getProvider(); if (!provider) { return createErrorResponse("Provider is required to get logs, please set the provider URL"); } const logs = await provider.getLogs(input.filter); return createSuccessResponse( `Logs retrieved successfully Logs: ${logs} `); } catch (error) { return createErrorResponse(`Failed to get logs: ${(error as Error).message}`); } }; export const getEnsResolverHandler = async (input: any): Promise<ToolResultSchema> => { try { if (!input.name) { return createErrorResponse("ENS name is required"); } const provider = getProvider(); if (!provider) { return createErrorResponse("Provider is required to get ENS resolver, please set the provider URL"); } // In ethers.js v5, getResolver might not be directly on the provider type // but it's available in the implementation const resolver = await (provider as any).getResolver(input.name); return createSuccessResponse( resolver ? `ENS resolver retrieved successfully Address: ${resolver.address} Name: ${resolver.name} ` : "No resolver found for this ENS name"); } catch (error) { return createErrorResponse(`Failed to get ENS resolver: ${(error as Error).message}`); } }; export const lookupAddressHandler = async (input: any): Promise<ToolResultSchema> => { try { if (!input.address) { return createErrorResponse("Address is required"); } const provider = getProvider(); if (!provider) { return createErrorResponse("Provider is required to lookup ENS name, please set the provider URL"); } const name = await provider.lookupAddress(input.address); return createSuccessResponse( name ? `ENS name retrieved successfully Name: ${name} ` : "No ENS name found for this address"); } catch (error) { return createErrorResponse(`Failed to lookup ENS name: ${(error as Error).message}`); } }; export const resolveNameHandler = async (input: any): Promise<ToolResultSchema> => { try { if (!input.name) { return createErrorResponse("ENS name is required"); } const provider = getProvider(); if (!provider) { return createErrorResponse("Provider is required to resolve ENS name, please set the provider URL"); } const address = await provider.resolveName(input.name); return createSuccessResponse( address ? `ENS name resolved successfully Name: ${input.name} Address: ${address} ` : "Could not resolve this ENS name"); } catch (error) { return createErrorResponse(`Failed to resolve ENS name: ${(error as Error).message}`); } }; // Network Methods export const getNetworkHandler = async (input: any): Promise<ToolResultSchema> => { try { const provider = getProvider(); if (!provider) { return createErrorResponse("Provider is required to get network information, please set the provider URL"); } const network = await provider.getNetwork(); return createSuccessResponse(`Network information retrieved successfully Network name: ${network.name} Chain ID: ${network.chainId} ENS address: ${network.ensAddress} `); } catch (error) { return createErrorResponse(`Failed to get network information: ${(error as Error).message}`); } }; export const getBlockNumberHandler = async (input: any): Promise<ToolResultSchema> => { try { const provider = getProvider(); if (!provider) { return createErrorResponse("Provider is required to get block number, please set the provider URL"); } const blockNumber = await provider.getBlockNumber(); return createSuccessResponse( `Block number retrieved successfully Block number: ${blockNumber.toString()} `); } catch (error) { return createErrorResponse(`Failed to get block number: ${(error as Error).message}`); } }; export const getFeeDataHandler = async (input: any): Promise<ToolResultSchema> => { try { const provider = getProvider(); if (!provider) { return createErrorResponse("Provider is required to get fee data, please set the provider URL"); } // getFeeData is available in ethers v5.5.0+ // @ts-ignore - getFeeData might not be in the type definitions depending on the version const feeData = await provider.getFeeData(); return createSuccessResponse(`Fee data retrieved successfully Gas price: ${feeData.gasPrice?.toString()} Max fee per gas: ${feeData.maxFeePerGas?.toString()} Max priority fee per gas: ${feeData.maxPriorityFeePerGas?.toString()} `); } catch (error) { return createErrorResponse(`Failed to get fee data: ${(error as Error).message}`); } }; ```