UNPKG

linea-mcp

Version:

A Model Context Protocol server for interacting with the Linea blockchain

376 lines (373 loc) 17.6 kB
import { // createWalletClient, // Unused (in current state) // http, // Unused (in current state) parseEther, // Needed for estimation & post-confirmation logic // WalletClient, // Unused // TransactionReceipt, // Unused encodeFunctionData, // Needed for ETH bridging via sendTransaction isAddress, parseUnits, // Add missing import formatEther, // Added for fee formatting } from 'viem'; import KeyManagementService /*, { SupportedAccount } */ from '../../services/keyManagement.js'; // Removed unused SupportedAccount import BlockchainService from '../../services/blockchain.js'; import config from '../../config/index.js'; // --- ABIs (viem compatible) --- const LINEA_BRIDGE_ABI = [ { name: 'bridgeETH', type: 'function', stateMutability: 'payable', // Important: indicates value can be sent inputs: [{ name: 'minGasLimit', type: 'uint256' }], outputs: [], }, { name: 'bridgeERC20', type: 'function', stateMutability: 'nonpayable', inputs: [ { name: 'token', type: 'address' }, { name: 'amount', type: 'uint256' }, { name: 'minGasLimit', type: 'uint256' }, ], outputs: [], }, { name: 'MessageSent', type: 'event', anonymous: false, inputs: [ { indexed: true, name: 'messageHash', type: 'bytes32' }, { indexed: true, name: 'from', type: 'address' }, { indexed: true, name: 'to', type: 'address' }, { indexed: false, name: 'value', type: 'uint256' }, { indexed: false, name: 'fee', type: 'uint256' }, ], }, // Add other events if needed for status checking later ]; const ERC20_APPROVE_ABI = [ { name: 'approve', type: 'function', stateMutability: 'nonpayable', inputs: [ { name: 'spender', type: 'address' }, { name: 'amount', type: 'uint256' }, ], outputs: [{ name: '', type: 'bool' }], }, // Add symbol/name/decimals if needed, but prefer reading from token handler if possible ]; // ----------------------------- /** * Get RPC URL based on network name - Helper function (Currently unused but kept for post-confirmation logic) */ // eslint-disable-next-line @typescript-eslint/no-unused-vars function getRpcUrl(network) { switch (network) { case 'ethereum': return config.rpc.ethereum; case 'testnet': return config.rpc.testnet; case 'mainnet': default: return config.rpc.mainnet || 'https://rpc.linea.build'; } } /** * Get Bridge Contract Address based on network name */ function getBridgeAddress(network) { switch (network) { case 'ethereum': return config.bridge.ethereumBridgeAddress; case 'testnet': // Assuming testnet bridge is same as mainnet for now case 'mainnet': default: return config.bridge.lineaBridgeAddress; } } /** * Bridge assets between Ethereum and Linea using viem, with fee estimation and confirmation * @param params The parameters for bridging assets * @returns The transaction details or an abort message */ export async function bridgeAssets(params) { try { const { sourceChain, destinationChain, assetType, tokenAddress, amount } = params; // Validate chains if (sourceChain === destinationChain) { throw new Error('Source and destination chains must be different.'); } if (!['ethereum', 'mainnet', 'testnet'].includes(sourceChain) || !['ethereum', 'mainnet', 'testnet'].includes(destinationChain)) { throw new Error('Invalid source or destination chain specified.'); } // Initialize services for the source chain const sourceNetwork = sourceChain; const blockchain = new BlockchainService(sourceNetwork); const publicClient = blockchain.client; const keyService = new KeyManagementService(); const account = keyService.getDefaultAccount(); // Get bridge contract address for the source chain const bridgeAddress = getBridgeAddress(sourceNetwork); if (!isAddress(bridgeAddress)) { throw new Error(`Bridge address for ${sourceNetwork} is not configured or invalid.`); } const minGasLimit = 100000n; // Use bigint // eslint-disable-next-line @typescript-eslint/no-unused-vars let txHash; // Kept for post-confirmation logic let gasEstimate; let gasPrice; let estimatedFeeEther; if (assetType === 'ETH') { const parsedValue = parseEther(amount); const bridgeData = encodeFunctionData({ abi: LINEA_BRIDGE_ABI, functionName: 'bridgeETH', args: [minGasLimit], }); // --- Estimate Gas Fee (ETH Bridge) --- console.log(`Estimating gas for bridging ${amount} ETH to ${destinationChain}...`); try { gasEstimate = await publicClient.estimateGas({ account, to: bridgeAddress, value: parsedValue, data: bridgeData, }); gasPrice = await publicClient.getGasPrice(); estimatedFeeEther = formatEther(gasEstimate * gasPrice); console.log(`Estimated Fee: ~${estimatedFeeEther} ETH`); } catch (estimationError) { console.error("Error estimating ETH bridge gas:", estimationError); throw new Error(`Failed to estimate gas fee: ${estimationError instanceof Error ? estimationError.message : 'Unknown error'}`); } // --- End Estimation --- // --- Ask for Confirmation (ETH Bridge) --- throw new Error(`CONFIRMATION_REQUIRED: Estimated fee to bridge ${amount} ETH from ${sourceChain} to ${destinationChain} is ~${estimatedFeeEther} ETH. Proceed? (Yes/No)`); // --- End Confirmation --- /* // --- Code to run *after* user confirms (Yes) --- const walletClient = createWalletClient({ account, chain: blockchain.currentChain, transport: http(getRpcUrl(sourceNetwork)) }); console.log(`Proceeding with ETH bridge...`); txHash = await walletClient.sendTransaction({ account, // Ensure account is passed if not implicit in client to: bridgeAddress, value: parsedValue, data: bridgeData, gas: gasEstimate, gasPrice: gasPrice, }); // ... rest of the post-confirmation logic ... */ } else if (assetType === 'ERC20' && tokenAddress) { if (!isAddress(tokenAddress)) { throw new Error('Invalid token address provided for ERC20 bridge.'); } const tokenAddressHex = tokenAddress; // TODO: Fetch actual decimals const decimals = 18; console.warn(`Assuming token ${tokenAddress} has ${decimals} decimals. Fetch actual decimals in production.`); const parsedAmount = parseUnits(amount, decimals); // --- Estimate Gas Fee (Approval) --- console.log(`Estimating gas for approving bridge contract ${bridgeAddress} to spend ${amount} tokens...`); let approveGasEstimate; let approveGasPrice; let approveEstimatedFeeEther; try { approveGasEstimate = await publicClient.estimateContractGas({ address: tokenAddressHex, abi: ERC20_APPROVE_ABI, functionName: 'approve', args: [bridgeAddress, parsedAmount], account, }); approveGasPrice = await publicClient.getGasPrice(); // Can potentially reuse gasPrice if fetched recently approveEstimatedFeeEther = formatEther(approveGasEstimate * approveGasPrice); console.log(`Approval Estimated Fee: ~${approveEstimatedFeeEther} ETH`); } catch (estimationError) { console.error("Error estimating approval gas:", estimationError); throw new Error(`Failed to estimate gas fee for approval: ${estimationError instanceof Error ? estimationError.message : 'Unknown error'}`); } // --- End Estimation (Approval) --- // --- Ask for Confirmation (Approval) --- throw new Error(`CONFIRMATION_REQUIRED: Step 1/2: Estimated fee to approve bridge for ${amount} tokens is ~${approveEstimatedFeeEther} ETH. Proceed? (Yes/No)`); // --- End Confirmation (Approval) --- /* // --- Code to run *after* user confirms Approval (Yes) --- const walletClient = createWalletClient({ account, chain: blockchain.currentChain, transport: http(getRpcUrl(sourceNetwork)) }); console.log(`Proceeding with token approval...`); const approveHash = await walletClient.writeContract({ address: tokenAddressHex, abi: ERC20_APPROVE_ABI, functionName: 'approve', args: [bridgeAddress, parsedAmount], gas: approveGasEstimate, gasPrice: approveGasPrice, }); console.log(`Approval transaction submitted: ${approveHash}. Waiting...`); const approveReceipt = await publicClient.waitForTransactionReceipt({ hash: approveHash }); if (approveReceipt.status === 'reverted') { throw new Error(`Token approval failed (reverted). Hash: ${approveHash}`); } console.log('Token approval successful.'); // --- End Post-Confirmation Code (Approval) --- */ // --- Estimate Gas Fee (Bridge ERC20) --- console.log(`Estimating gas for bridging ${amount} of token ${tokenAddress}...`); try { gasEstimate = await publicClient.estimateContractGas({ address: bridgeAddress, abi: LINEA_BRIDGE_ABI, functionName: 'bridgeERC20', args: [tokenAddressHex, parsedAmount, minGasLimit], account, }); gasPrice = await publicClient.getGasPrice(); // Re-fetch or reuse approveGasPrice estimatedFeeEther = formatEther(gasEstimate * gasPrice); console.log(`Bridge Estimated Fee: ~${estimatedFeeEther} ETH`); } catch (estimationError) { console.error("Error estimating bridgeERC20 gas:", estimationError); // Cast to any to bypass persistent TS error, then access message const errorMsg = estimationError?.message || 'Unknown error'; throw new Error(`Failed to estimate gas fee for bridge transaction: ${errorMsg}`); } // --- End Estimation (Bridge ERC20) --- // --- Ask for Confirmation (Bridge ERC20) --- throw new Error(`CONFIRMATION_REQUIRED: Step 2/2: Estimated fee for the bridge transaction itself is ~${estimatedFeeEther} ETH. Proceed? (Yes/No)`); // --- End Confirmation (Bridge ERC20) --- /* // --- Code to run *after* user confirms Bridge (Yes) --- // Ensure walletClient is defined (it should be from approval step) console.log('Proceeding with bridgeERC20 transaction...'); txHash = await walletClient.writeContract({ address: bridgeAddress, abi: LINEA_BRIDGE_ABI, functionName: 'bridgeERC20', args: [tokenAddressHex, parsedAmount, minGasLimit], gas: gasEstimate, gasPrice: gasPrice, }); // ... rest of the post-confirmation logic ... */ } else { throw new Error('Invalid asset type or missing token address for ERC20.'); } // This part is now unreachable due to the confirmation throws /* console.log(`Bridge transaction submitted: ${txHash}. Waiting for confirmation...`); const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); console.log(`Bridge transaction confirmed on ${sourceChain}. Status: ${receipt.status}`); if (receipt.status === 'reverted') { throw new Error(`Bridge transaction failed (reverted). Hash: ${txHash}`); } return { success: true, transactionHash: txHash, sourceChain, destinationChain, assetType, tokenAddress: assetType === 'ERC20' ? tokenAddress : null, amount, from: account.address, status: 'initiated_on_source', receipt: { blockNumber: receipt.blockNumber.toString(), gasUsed: receipt.gasUsed.toString(), status: receipt.status, }, message: `Bridge transaction initiated on ${sourceChain}. Monitor destination chain for completion.`, estimatedFee: estimatedFeeEther // Include estimate if available }; */ } catch (error) { // Re-throw confirmation request errors if (error instanceof Error && error.message.startsWith('CONFIRMATION_REQUIRED:')) { throw error; } console.error('Error in bridgeAssets:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; if (errorMessage.includes('insufficient funds')) { throw new Error(`Failed to bridge assets: Insufficient funds for transaction.`); } else if (errorMessage.includes('reverted')) { throw new Error(`Failed to bridge assets: Transaction reverted. Check parameters, approval, or funds.`); } throw new Error(`Failed to bridge assets: ${errorMessage}`); } } /** * Check the status of a bridge transaction using viem * @param params The parameters for checking bridge status * @returns The status of the bridge transaction */ export async function bridgeStatus(params) { try { const { transactionHash, sourceChain } = params; if (!['ethereum', 'mainnet', 'testnet'].includes(sourceChain)) { throw new Error('Invalid source chain specified.'); } const sourceNetwork = sourceChain; // Initialize services for the source chain const blockchain = new BlockchainService(sourceNetwork); const publicClient = blockchain.client; console.log(`Checking status for transaction ${transactionHash} on ${sourceChain}...`); // Get transaction receipt on the source chain const receipt = await publicClient.getTransactionReceipt({ hash: transactionHash }); if (!receipt) { // Check if transaction is just pending const tx = await publicClient.getTransaction({ hash: transactionHash }); const status = tx ? 'pending' : 'not_found'; const message = tx ? 'Transaction is pending confirmation on source chain.' : 'Transaction not found on source chain.'; return { success: true, transactionHash, sourceChain, status: status, message: message, }; } // Check if transaction failed on the source chain if (receipt.status === 'reverted') { return { success: true, transactionHash, sourceChain, status: 'failed_on_source', message: 'Bridge transaction failed (reverted) on source chain.', receipt: { blockNumber: receipt.blockNumber.toString(), gasUsed: receipt.gasUsed.toString(), status: receipt.status, }, }; } // Transaction succeeded on source chain. // A real implementation would now: // 1. Parse logs from the receipt to find the message hash or identifier. // 2. Query a bridge relay service or the destination chain's bridge contract // using the identifier to check if the message has been processed. // For this example, we'll just indicate it's in progress. const destinationChain = sourceChain === 'ethereum' ? 'linea' : 'ethereum'; // Simplified logic return { success: true, transactionHash, sourceChain, destinationChain, // Indicate expected destination status: 'completed_on_source', // More accurate status message: `Bridge transaction confirmed on ${sourceChain}. Processing on ${destinationChain} may take time. Further status check requires cross-chain monitoring (not implemented in this example).`, receipt: { blockNumber: receipt.blockNumber.toString(), gasUsed: receipt.gasUsed.toString(), status: receipt.status, }, }; } catch (error) { console.error('Error in bridgeStatus:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; throw new Error(`Failed to check bridge status: ${errorMessage}`); } }