linea-mcp
Version:
A Model Context Protocol server for interacting with the Linea blockchain
221 lines (212 loc) • 9.41 kB
JavaScript
import { isAddress, parseEther,
// Hex, // Unused
// TransactionReceipt, // Unused
// estimateGas, // Use client.estimateGas
// getGasPrice, // Use client.getGasPrice
formatEther, // Added for fee formatting
} from 'viem';
import BlockchainService from '../../services/blockchain.js';
import KeyManagementService from '../../services/keyManagement.js';
import config from '../../config/index.js'; // Import config for RPC URL
/**
* 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 the default wallet address using viem
* @returns The wallet address
*/
export async function getAddress(_params) {
try {
const keyService = new KeyManagementService();
// Use the updated method returning a viem account
const account = keyService.getDefaultAccount();
return {
success: true,
address: account.address,
};
}
catch (error) {
console.error('Error in getAddress:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
throw new Error(`Failed to get address: ${errorMessage}`);
}
}
/**
* List balances for a wallet using viem
* @param params The parameters for listing balances
* @returns The wallet balances
*/
export async function listBalances(params) {
try {
// Assuming mainnet for now, could be parameterized later
const blockchain = new BlockchainService('mainnet');
let ownerAddress;
// If no address is provided, use the default account's address
if (!params.address) {
const keyService = new KeyManagementService();
const account = keyService.getDefaultAccount();
ownerAddress = account.address;
console.warn(`No address provided, using default account address: ${ownerAddress}`);
}
else if (isAddress(params.address)) {
ownerAddress = params.address;
}
else {
throw new Error('Invalid owner address provided.');
}
// Get ETH balance using the viem-based service
const ethBalance = await blockchain.getBalance(ownerAddress);
// TODO: Implement fetching balances for common ERC20 tokens on Linea
// This would involve:
// 1. Defining a list of common token addresses for Linea.
// 2. Using publicClient.multicall to fetch balances efficiently.
// 3. Formatting the results.
return {
success: true,
address: ownerAddress,
balances: {
ETH: ethBalance,
// Example: USDC: await getTokenBalance(ownerAddress, USDC_ADDRESS, publicClient),
},
// Add a note about token balances being incomplete
note: "ERC20 token balances are not fully implemented in this version.",
};
}
catch (error) {
console.error('Error in listBalances:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
if (errorMessage.includes('Invalid owner address')) {
throw new Error(`Failed to list balances: Invalid address format.`);
}
throw new Error(`Failed to list balances: ${errorMessage}`);
}
}
/**
* Transfer ETH funds from one wallet to another using viem, with fee estimation and confirmation
* @param params The parameters for transferring funds
* @returns The transaction details or an abort message
*/
export async function transferFunds(params) {
try {
const { destination, amount, assetId = 'ETH' } = params; // Default to ETH
// Validate destination address
if (!isAddress(destination)) {
throw new Error('Invalid destination address');
}
// Only ETH transfers are supported currently
if (assetId !== 'ETH') {
throw new Error(`Only ETH transfers are supported by this tool currently. Use the 'tokens_erc20Transfer' tool for tokens.`);
}
// Parse amount
let parsedAmount;
try {
parsedAmount = parseEther(amount);
}
catch (error) {
throw new Error('Invalid amount format. Amount must be a string representing Ether (e.g., "0.1").');
}
// Initialize services
const blockchain = new BlockchainService('mainnet'); // Assuming mainnet
const publicClient = blockchain.client;
const keyService = new KeyManagementService();
// Get the default account (sender)
const account = keyService.getDefaultAccount();
// --- Estimate Gas Fee ---
console.log(`Estimating gas for transferring ${amount} ETH to ${destination}...`);
let gasEstimate;
let gasPrice;
let estimatedFeeEther;
try {
gasEstimate = await publicClient.estimateGas({
account,
to: destination,
value: parsedAmount,
});
gasPrice = await publicClient.getGasPrice();
const estimatedFee = gasEstimate * gasPrice;
estimatedFeeEther = formatEther(estimatedFee);
console.log(`Estimated Gas: ${gasEstimate}, Gas Price: ${gasPrice}, Estimated Fee: ${estimatedFeeEther} ETH`);
}
catch (estimationError) {
console.error("Error estimating gas:", estimationError);
throw new Error(`Failed to estimate gas fee: ${estimationError instanceof Error ? estimationError.message : 'Unknown error'}`);
}
// --- End Estimation ---
// --- Ask for Confirmation ---
// This requires integration with the MCP client to ask the user
// We'll simulate this by throwing a specific error that the client can catch
// In a real MCP server, you'd use the SDK's requestUserConfirmation or similar
throw new Error(`CONFIRMATION_REQUIRED: Estimated fee to transfer ${amount} ETH to ${destination} is ~${estimatedFeeEther} ETH. Proceed? (Yes/No)`);
// --- End Confirmation ---
/*
// --- Code to run *after* user confirms (Yes) ---
// This part would be executed in a subsequent call or callback in a real MCP server
// Create a WalletClient instance to send transactions
const walletClient = createWalletClient({
account,
chain: blockchain.currentChain,
transport: http(getRpcUrl('mainnet')),
});
console.log(`Proceeding with transfer of ${amount} ETH from ${account.address} to ${destination}...`);
// Send ETH transaction
const hash = await walletClient.sendTransaction({
to: destination,
value: parsedAmount,
gas: gasEstimate, // Use estimated gas
gasPrice: gasPrice, // Use fetched gas price (or maxFeePerGas/maxPriorityFeePerGas for EIP-1559)
});
console.log(`Transaction submitted with hash: ${hash}. Waiting for confirmation...`);
// Wait for the transaction to be confirmed
const receipt = await publicClient.waitForTransactionReceipt({ hash });
console.log(`Transaction confirmed in block ${receipt.blockNumber}. Status: ${receipt.status}`);
if (receipt.status === 'reverted') {
throw new Error(`ETH transfer transaction failed (reverted). Hash: ${hash}`);
}
return {
success: true,
transactionHash: hash,
receipt: { // Include receipt details
blockNumber: receipt.blockNumber.toString(),
gasUsed: receipt.gasUsed.toString(),
status: receipt.status,
},
from: account.address,
to: destination,
amount, // Return original amount string
assetId,
estimatedFee: estimatedFeeEther, // Include estimate
};
// --- End Post-Confirmation Code ---
*/
}
catch (error) {
// Re-throw confirmation request errors
if (error instanceof Error && error.message.startsWith('CONFIRMATION_REQUIRED:')) {
throw error;
}
console.error('Error in transferFunds:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
if (errorMessage.includes('Invalid destination address')) {
throw new Error(`Failed to transfer funds: Invalid destination address format.`);
}
else if (errorMessage.includes('Invalid amount format')) {
throw new Error(`Failed to transfer funds: ${errorMessage}`);
}
else if (errorMessage.includes('insufficient funds')) {
throw new Error(`Failed to transfer funds: Insufficient ETH balance for the transaction.`);
}
else if (errorMessage.includes('reverted')) {
throw new Error(`Failed to transfer funds: Transaction reverted.`);
}
throw new Error(`Failed to transfer funds: ${errorMessage}`);
}
}