MCP EVM Signer

  • src
import { ethers } from 'ethers'; import config from './config.js'; import * as crypto from './crypto.js'; /** * Get Ethereum provider for a specific network */ export function getProvider(network: string = config.infura.defaultNetwork): ethers.JsonRpcProvider { const rpcUrl = `https://${network}.infura.io/v3/${config.infura.apiKey}`; return new ethers.JsonRpcProvider(rpcUrl); } /** * Get a signer for the specified address */ export async function getSigner( address: string, network: string = config.infura.defaultNetwork ): Promise<ethers.Wallet | null> { const wallet = await crypto.loadWallet(address); if (!wallet) return null; const provider = getProvider(network); return wallet.connect(provider); } /** * Check the ETH balance for an address */ export async function checkBalance( address: string, network: string = config.infura.defaultNetwork ): Promise<string> { const provider = getProvider(network); const balance = await provider.getBalance(address); return ethers.formatEther(balance); } /** * Get transactions for an address */ export async function getTransactions( address: string, limit: number = 10, network: string = config.infura.defaultNetwork ): Promise<any[]> { const provider = getProvider(network); // Get the current block number const currentBlock = await provider.getBlockNumber(); // Get recent blocks const blockPromises = []; const blockCount = Math.min(limit * 2, 100); // Look at up to 100 blocks for (let i = 0; i < blockCount; i++) { if (currentBlock - i > 0) { blockPromises.push(provider.getBlock(currentBlock - i, true)); } } const blocks = await Promise.all(blockPromises); const transactions: any[] = []; const normalizedAddress = address.toLowerCase(); // Filter transactions involving the address for (const block of blocks) { if (!block || !block.transactions) continue; for (const tx of block.transactions) { // Handle both string type and object type transactions for compatibility const txFrom = typeof tx === 'string' ? '' : (tx.from || ''); const txTo = typeof tx === 'string' ? '' : (tx.to || ''); if ( (txFrom.toLowerCase && txFrom.toLowerCase() === normalizedAddress) || (txTo.toLowerCase && txTo.toLowerCase() === normalizedAddress) ) { if (typeof tx === 'string') { // If tx is just a transaction hash, fetch the full transaction const fullTx = await provider.getTransaction(tx); if (fullTx) { transactions.push({ hash: fullTx.hash, from: fullTx.from, to: fullTx.to, value: fullTx.value ? ethers.formatEther(fullTx.value) : '0', blockNumber: fullTx.blockNumber, timestamp: block.timestamp, }); } } else { // If tx is already a full transaction object transactions.push({ hash: tx.hash, from: tx.from || '', to: tx.to || '', value: tx.value ? ethers.formatEther(tx.value) : '0', blockNumber: tx.blockNumber, timestamp: block.timestamp, }); } if (transactions.length >= limit) { return transactions; } } } } return transactions; } /** * Send ETH to an address */ export async function sendTransaction( fromAddress: string, toAddress: string, etherAmount: string, network: string = config.infura.defaultNetwork ): Promise<{hash: string; explorer: string}> { const signer = await getSigner(fromAddress, network); if (!signer) { throw new Error(`Unable to load wallet for address ${fromAddress}`); } const tx = await signer.sendTransaction({ to: toAddress, value: ethers.parseEther(etherAmount) }); // Get the network-specific explorer URL let explorerUrl = ''; switch (network) { case 'mainnet': explorerUrl = 'https://etherscan.io/tx/'; break; case 'goerli': explorerUrl = 'https://goerli.etherscan.io/tx/'; break; case 'sepolia': explorerUrl = 'https://sepolia.etherscan.io/tx/'; break; default: explorerUrl = 'https://etherscan.io/tx/'; } return { hash: tx.hash, explorer: `${explorerUrl}${tx.hash}` }; } /** * Deploy a contract */ export async function deployContract( fromAddress: string, abi: any[], bytecode: string, constructorArgs: any[] = [], network: string = config.infura.defaultNetwork ): Promise<{ address: string; deploymentHash: string; explorer: string; }> { const signer = await getSigner(fromAddress, network); if (!signer) { throw new Error(`Unable to load wallet for address ${fromAddress}`); } // Create the factory const factory = new ethers.ContractFactory(abi, bytecode, signer); // Deploy the contract const contract = await factory.deploy(...constructorArgs); await contract.waitForDeployment(); const contractAddress = await contract.getAddress(); const receipt = await contract.deploymentTransaction()?.wait(); const deploymentHash = receipt?.hash || ''; // Get the network-specific explorer URL let explorerUrl = ''; switch (network) { case 'mainnet': explorerUrl = 'https://etherscan.io/address/'; break; case 'goerli': explorerUrl = 'https://goerli.etherscan.io/address/'; break; case 'sepolia': explorerUrl = 'https://sepolia.etherscan.io/address/'; break; default: explorerUrl = 'https://etherscan.io/address/'; } return { address: contractAddress, deploymentHash, explorer: `${explorerUrl}${contractAddress}` }; } /** * Call a contract method (read-only) */ export async function callContractMethod( contractAddress: string, abi: any[], methodName: string, methodArgs: any[] = [], network: string = config.infura.defaultNetwork ): Promise<any> { const provider = getProvider(network); const contract = new ethers.Contract(contractAddress, abi, provider); return await contract[methodName](...methodArgs); } /** * Execute a contract method (write) */ export async function executeContractMethod( fromAddress: string, contractAddress: string, abi: any[], methodName: string, methodArgs: any[] = [], network: string = config.infura.defaultNetwork ): Promise<{ hash: string; explorer: string; }> { const signer = await getSigner(fromAddress, network); if (!signer) { throw new Error(`Unable to load wallet for address ${fromAddress}`); } const contract = new ethers.Contract(contractAddress, abi, signer); const tx = await contract[methodName](...methodArgs); await tx.wait(); // Get the network-specific explorer URL let explorerUrl = ''; switch (network) { case 'mainnet': explorerUrl = 'https://etherscan.io/tx/'; break; case 'goerli': explorerUrl = 'https://goerli.etherscan.io/tx/'; break; case 'sepolia': explorerUrl = 'https://sepolia.etherscan.io/tx/'; break; default: explorerUrl = 'https://etherscan.io/tx/'; } return { hash: tx.hash, explorer: `${explorerUrl}${tx.hash}` }; }