linea-mcp
Version:
A Model Context Protocol server for interacting with the Linea blockchain
898 lines • 59.5 kB
JavaScript
import { createWalletClient, http, parseEther,
// TransactionReceipt, // Unused
encodeFunctionData, // Needed for ETH bridging via sendTransaction
isAddress, parseUnits, // Add missing import
formatEther, // Added for fee formatting
formatUnits, // Added for allowance formatting
decodeEventLog, // Import decodeEventLog
} 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 L1_BRIDGE_MESSAGE_SERVICE_ABI = [
// Initiate L1 -> L2 (Used for ETH and ERC20, fee and value are adjusted)
{ name: 'sendMessage', type: 'function', stateMutability: 'payable', inputs: [{ name: 'to', type: 'address' }, { name: 'fee', type: 'uint256' }, { name: 'deadline', type: 'uint256' }, { name: 'calldata', type: 'bytes' }], outputs: [] },
// Complete L2 -> L1 (Manual Claim)
{ name: 'claimMessage', type: 'function', stateMutability: 'nonpayable', inputs: [{ name: 'from', type: 'address' }, { name: 'to', type: 'address' }, { name: 'fee', type: 'uint256' }, { name: 'value', type: 'uint256' }, { name: 'nonce', type: 'uint256' }, { name: 'message', type: 'bytes' }, { name: 'proof', type: 'bytes32[]' }], outputs: [] },
// Estimate fee for sending a message
{ name: 'estimateFee', type: 'function', stateMutability: 'view', inputs: [{ name: 'to', type: 'address' }, { name: 'deadline', type: 'uint256' }, { name: 'calldata', type: 'bytes' }], outputs: [{ name: '', type: 'uint256' }] },
// Events (for status tracking)
{ 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: 'fee', type: 'uint256' }, { indexed: false, name: 'value', type: 'uint256' }, { indexed: false, name: 'nonce', type: 'uint256' }, { indexed: false, name: 'calldata', type: 'bytes' }] },
{ name: 'MessageClaimed', type: 'event', anonymous: false, inputs: [{ indexed: true, name: 'messageHash', type: 'bytes32' }] },
// Check the status of a message hash (Claim status)
{ name: 'messageStatus', type: 'function', stateMutability: 'view', inputs: [{ name: 'messageHash', type: 'bytes32' }], outputs: [{ name: '', type: 'uint8' }] }, // Returns status enum (e.g., 0: NON_EXISTENT, 1: PENDING, 2: CLAIMABLE, 3: CLAIMED, 4: FAILED)
];
const L2_BRIDGE_MESSAGE_SERVICE_ABI = [
// Initiate L2 -> L1
{ name: 'sendMessage', type: 'function', stateMutability: 'payable', inputs: [{ name: 'to', type: 'address' }, { name: 'fee', type: 'uint256' }, { name: 'deadline', type: 'uint256' }, { name: 'calldata', type: 'bytes' }], outputs: [] },
// Estimate fee for sending a message
{ name: 'estimateFee', type: 'function', stateMutability: 'view', inputs: [{ name: 'to', type: 'address' }, { name: 'deadline', type: 'uint256' }, { name: 'calldata', type: 'bytes' }], outputs: [{ name: '', type: 'uint256' }] },
// Events
{ 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: 'fee', type: 'uint256' }, { indexed: false, name: 'value', type: 'uint256' }, { indexed: false, name: 'nonce', type: 'uint256' }, { indexed: false, name: 'calldata', type: 'bytes' }] },
];
const CCTP_TOKEN_MESSENGER_ABI = [
// Initiate on source chain
{ name: 'depositForBurn', type: 'function', stateMutability: 'payable', inputs: [{ name: 'amount', type: 'uint256' }, { name: 'destinationDomain', type: 'uint32' }, { name: 'mintRecipient', type: 'bytes32' }, { name: 'burnToken', type: 'address' }], outputs: [{ type: 'uint64', name: 'nonce' }] },
// Complete on destination chain (Called by Circle relay, event/status important for manual claim)
{ name: 'receiveMessage', type: 'function', stateMutability: 'nonpayable', inputs: [{ name: 'message', type: 'bytes' }, { name: 'attestation', type: 'bytes' }], outputs: [{ type: 'bool', name: 'success' }] },
// Events
{ name: 'DepositForBurn', type: 'event', anonymous: false, inputs: [{ indexed: true, name: 'nonce', type: 'uint64' }, { indexed: true, name: 'burnToken', type: 'address' }, { indexed: false, name: 'amount', type: 'uint256' }, { indexed: true, name: 'depositor', type: 'address' }, { indexed: false, name: 'mintRecipient', type: 'bytes32' }, { indexed: false, name: 'destinationDomain', type: 'uint32' }, { indexed: false, name: 'destinationTokenMessenger', type: 'bytes32' }, { indexed: false, name: 'destinationCaller', type: 'bytes32' }] },
{ name: 'MessageReceived', type: 'event', anonymous: false, inputs: [{ indexed: true, name: 'caller', type: 'address' }, { indexed: false, name: 'sourceDomain', type: 'uint32' }, { indexed: true, name: 'nonce', type: 'uint64' }, { indexed: false, name: 'sender', type: 'bytes32' }, { indexed: false, name: 'messageBody', type: 'bytes' }] },
];
const ERC20_ABI = [
{
name: 'approve',
type: 'function',
stateMutability: 'nonpayable',
inputs: [
{ name: 'spender', type: 'address' },
{ name: 'amount', type: 'uint256' },
],
outputs: [{ name: '', type: 'bool' }],
},
{ name: 'decimals', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'uint8' }] },
{ name: 'balanceOf', type: 'function', stateMutability: 'view', inputs: [{ name: 'account', type: 'address' }], outputs: [{ name: '', type: 'uint256' }] },
{ name: 'allowance', type: 'function', stateMutability: 'view', inputs: [{ name: 'owner', type: 'address' }, { name: 'spender', type: 'address' }], outputs: [{ name: '', type: 'uint256' }] },
];
// --- Canonical Token Bridge ABI (Simplified) ---
const _TOKEN_BRIDGE_ABI = [
// Example function on L1/L2 Token Bridge to handle deposits received via message
{ name: 'depositERC20', type: 'function', stateMutability: 'nonpayable', inputs: [{ name: 'token', type: 'address' }, { name: 'recipient', type: 'address' }, { name: 'amount', type: 'uint256' }], outputs: [] },
// Add other relevant functions like finalizeDeposit, withdraw, etc. as needed
];
/**
* Get the layer type (l1 or l2) based on the network name.
*/
function getLayerType(network) {
return network === 'ethereum' ? 'l1' : 'l2';
}
/**
* Get the network environment (mainnet or testnet) for config lookup.
*/
function getNetworkEnv(network) {
// Assuming 'ethereum' maps to mainnet L1, 'mainnet' to mainnet L2, 'testnet' to testnet L2
// Adjust if 'ethereum' needs to distinguish between mainnet/testnet L1
return (network === 'ethereum' || network === 'mainnet') ? 'mainnet' : 'testnet';
}
/**
* Get the appropriate Message Service contract address and ABI based on the source network.
* Also returns the corresponding Token Bridge address for the destination network.
*/
function getBridgeConfig(sourceNetwork, destinationNetwork) {
const sourceLayer = getLayerType(sourceNetwork);
const _destinationLayer = getLayerType(destinationNetwork);
const networkEnv = getNetworkEnv(sourceNetwork);
const destinationNetworkEnv = getNetworkEnv(destinationNetwork);
if (sourceLayer === 'l1') {
const address = config.nativeBridge[networkEnv].l1;
if (!isAddress(address))
throw new Error(`L1 Message Service address for ${networkEnv} is not configured or invalid.`);
const destTokenBridge = config.tokenBridge[destinationNetworkEnv].l2;
if (!isAddress(destTokenBridge))
throw new Error(`L2 Token Bridge address for ${destinationNetworkEnv} is not configured or invalid.`);
return {
messageServiceAddress: address,
messageServiceAbi: L1_BRIDGE_MESSAGE_SERVICE_ABI,
destinationTokenBridgeAddress: destTokenBridge
};
}
else { // sourceLayer === 'l2'
const address = config.nativeBridge[networkEnv].l2;
if (!isAddress(address))
throw new Error(`L2 Message Service address for ${networkEnv} is not configured or invalid.`);
const destTokenBridge = config.tokenBridge[destinationNetworkEnv].l1;
if (!isAddress(destTokenBridge))
throw new Error(`L1 Token Bridge address for ${destinationNetworkEnv} is not configured or invalid.`);
return {
messageServiceAddress: address,
messageServiceAbi: L2_BRIDGE_MESSAGE_SERVICE_ABI,
destinationTokenBridgeAddress: destTokenBridge
};
}
}
/**
* Fetch ERC20 token decimals.
*/
async function getTokenDecimals(publicClient, tokenAddress) {
try {
const decimals = await publicClient.readContract({
address: tokenAddress,
abi: ERC20_ABI,
functionName: 'decimals',
});
return Number(decimals);
}
catch (error) {
console.error(`Error fetching decimals for token ${tokenAddress}:`, error);
throw new Error(`Could not fetch decimals for token ${tokenAddress}. Ensure it's a valid ERC20 contract.`);
}
}
/**
* 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';
}
}
/**
* 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) {
// İmplementasyon burada...
// Eski bridgeNativeAssets implementasyonu
try {
const { sourceChain, destinationChain, assetType, tokenAddress, amount } = params;
// Specific handling for USDC
if (assetType === 'ERC20' && tokenAddress &&
// USDC token address condition - this should be added to config
// This is a placeholder. Real implementation should check if the token is USDC
tokenAddress.toLowerCase() === '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'.toLowerCase()) {
return bridgeUsdc(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();
// Create Wallet Client for sending transactions
const walletClient = createWalletClient({
account,
chain: blockchain.currentChain,
transport: http(getRpcUrl(sourceNetwork))
});
// --- Determine bridge direction and get Message Service config ---
const sourceLayer = getLayerType(sourceNetwork);
const destinationLayer = getLayerType(destinationChain);
const { messageServiceAddress, messageServiceAbi, destinationTokenBridgeAddress } = getBridgeConfig(sourceNetwork, destinationChain);
console.log(`Bridging from ${sourceNetwork} (${sourceLayer}) to ${destinationChain} (${destinationLayer}) via ${messageServiceAddress}`);
// ------------------------------------------------------------------
const _minGasLimit = 100000n; // Use bigint
let txHash; // Transaction hash after sending
let gasEstimate;
let gasPrice;
let _estimatedFeeEther;
// Common sendMessage parameters
const recipient = account.address; // Default to sender, parameterize if needed
const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600); // 1 hour from now
let messagingFee;
let transactionValue = 0n; // Value to send with sendMessage call (ETH amount + fees)
let messageCalldata = '0x'; // Calldata for the message
if (assetType === 'ETH') {
// TODO: Confirm if calldata should be non-empty for ETH transfers
messageCalldata = '0x'; // Assuming empty calldata for direct ETH reception
// Estimate messaging fee first
try {
console.log('Estimating messaging fee...');
messagingFee = await publicClient.readContract({
address: messageServiceAddress,
abi: messageServiceAbi,
functionName: 'estimateFee',
args: [destinationTokenBridgeAddress, deadline, messageCalldata] // Target is Token Bridge for consistency?
});
console.log(`Estimated Messaging Fee: ${formatEther(messagingFee)} ETH`);
}
catch (feeError) {
console.error("Error estimating messaging fee:", feeError);
throw new Error(`Failed to estimate messaging fee: ${feeError instanceof Error ? feeError.message : 'Unknown error'}`);
}
const parsedValue = parseEther(amount);
// For ETH L1->L2: value is sent with the message
// For ETH L2->L1: value is sent, and anti-DDOS fee is added
transactionValue = parsedValue + messagingFee; // Base value + message fee
if (sourceLayer === 'l2') {
const antiDdosFee = parseEther('0.001'); // Add Anti-DDOS fee for L2->L1 ETH bridge
transactionValue += antiDdosFee;
console.log(`Adding 0.001 ETH Anti-DDOS fee for L2->L1 ETH bridge.`);
}
const sendMessageData = encodeFunctionData({
abi: messageServiceAbi, // Use the correct ABI
functionName: 'sendMessage',
args: [destinationTokenBridgeAddress, messagingFee, deadline, messageCalldata] // Send message TO the destination Token Bridge
});
// --- Estimate Gas Fee (ETH Bridge) ---
console.log(`Estimating gas for bridging ${amount} ETH to ${destinationChain}...`);
try {
gasEstimate = await publicClient.estimateGas({
account,
to: messageServiceAddress, // Use message service address
value: transactionValue, // Send ETH amount + messaging fee (+ anti-DDOS if L2->L1)
data: sendMessageData,
});
gasPrice = await publicClient.getGasPrice();
_estimatedFeeEther = formatEther(gasEstimate * gasPrice);
}
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 - MCP Style) ---
// Confirmation should be handled before calling this function
// Proceed with sending...
// --- End Confirmation ---
// Send transaction
console.log(`Sending transaction...`);
txHash = await walletClient.writeContract({
chain: blockchain.currentChain,
account: account,
address: messageServiceAddress,
abi: messageServiceAbi,
functionName: 'sendMessage',
args: [destinationTokenBridgeAddress, messagingFee, deadline, messageCalldata],
value: transactionValue,
gas: gasEstimate,
gasPrice: gasPrice,
});
// Wait for transaction confirmation and return result
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}`);
return {
success: true,
transactionHash: txHash,
sourceChain,
destinationChain,
recipient,
assetType,
tokenAddress: null, // ETH has no token address
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.`,
};
}
else if (assetType === 'ERC20' && tokenAddress) {
if (!isAddress(tokenAddress)) {
throw new Error('Invalid token address provided for ERC20 bridge.');
}
const tokenAddressHex = tokenAddress;
// Fetch token decimals dynamically
console.log(`Fetching decimals for token ${tokenAddressHex}...`);
const decimals = await getTokenDecimals(publicClient, tokenAddressHex);
console.log(`Token decimals: ${decimals}`);
const parsedAmount = parseUnits(amount, decimals);
// --- Estimate Gas Fee (Approval) ---
// Approval is needed for the Message Service contract to handle the token
console.log(`Checking allowance and estimating gas for approving Message Service contract ${messageServiceAddress} to spend ${amount} ${tokenAddressHex}...`);
let approveGasEstimate;
let approveGasPrice;
let approveEstimatedFeeEther;
// Check current allowance first
const currentAllowance = await publicClient.readContract({
address: tokenAddressHex,
abi: ERC20_ABI,
functionName: 'allowance',
args: [account.address, messageServiceAddress]
});
let _approvalNeeded = false;
if (currentAllowance < parsedAmount) {
_approvalNeeded = true;
console.log(`Approval needed. Current allowance: ${formatUnits(currentAllowance, decimals)}, Required: ${amount}`);
try {
approveGasEstimate = await publicClient.estimateContractGas({
address: tokenAddressHex,
abi: ERC20_ABI, // Use correct ABI
functionName: 'approve',
args: [messageServiceAddress, parsedAmount], // Approve the Message Service contract
account,
});
approveGasPrice = await publicClient.getGasPrice(); // Assign value inside try
approveEstimatedFeeEther = formatEther(approveGasEstimate * approveGasPrice); // Assign value inside try
console.log(`Approval Estimated Fee: ~${approveEstimatedFeeEther} ETH`);
// Confirmation for approval should be handled before calling this function
// Proceed with sending...
}
catch (error) {
// Re-throw confirmation request if it was the cause
if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'APPROVAL_CONFIRMATION_REQUIRED') {
throw error;
}
// Otherwise, throw a generic estimation error
console.error("Error estimating approval gas:", error);
throw new Error(`Failed to estimate gas fee for approval: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
else {
console.log(`Sufficient allowance already granted: ${formatUnits(currentAllowance, decimals)}`);
}
// --- End Estimation (Approval) ---
// --- Ask for Confirmation (Approval) ---
if (_approvalNeeded) {
console.log(`Proceeding with token approval...`);
const approveTxHash = await walletClient.writeContract({
chain: blockchain.currentChain,
account: account,
address: tokenAddressHex,
abi: ERC20_ABI,
functionName: 'approve',
args: [messageServiceAddress, parsedAmount],
gas: approveGasEstimate, // Use estimated gas
gasPrice: approveGasPrice, // Use estimated gas price
});
console.log(`Approval transaction submitted: ${approveTxHash}. Waiting for confirmation...`);
try {
const approveReceipt = await publicClient.waitForTransactionReceipt({ hash: approveTxHash });
if (approveReceipt.status === 'reverted') {
console.error('Approval transaction failed:', approveReceipt);
throw new Error(`Token approval failed (reverted). Hash: ${approveTxHash}`);
}
console.log('Token approval successful.');
}
catch (waitError) {
console.error('Error waiting for approval confirmation:', waitError);
throw new Error(`Failed to confirm approval transaction: ${waitError instanceof Error ? waitError.message : 'Unknown error'}`);
}
}
// --- End Confirmation (Approval) ---
// --- Prepare sendMessage for ERC20 ---
// Calldata for the destination Token Bridge's depositERC20 function (already prepared)
try {
// Encode the calldata for depositERC20 on the destination token bridge
messageCalldata = encodeFunctionData({
abi: _TOKEN_BRIDGE_ABI,
functionName: 'depositERC20',
args: [tokenAddressHex, account.address, parsedAmount]
});
console.log(`Prepared ERC20 bridge calldata for depositERC20 function`);
}
catch (encodeError) {
console.error("Error encoding ERC20 bridge calldata:", encodeError);
throw new Error(`Failed to prepare bridge data: ${encodeError instanceof Error ? encodeError.message : 'Unknown error'}`);
}
// Estimate messaging fee based on the ERC20 calldata
try {
console.log('Estimating messaging fee for ERC20 bridge...');
messagingFee = await publicClient.readContract({
address: messageServiceAddress,
abi: messageServiceAbi,
functionName: 'estimateFee',
args: [destinationTokenBridgeAddress, deadline, messageCalldata] // Target is Token Bridge
});
console.log(`Estimated Messaging Fee: ${formatEther(messagingFee)} ETH`);
}
catch (feeError) {
console.error("Error estimating messaging fee:", feeError);
throw new Error(`Failed to estimate messaging fee: ${feeError instanceof Error ? feeError.message : 'Unknown error'}`);
}
// For ERC20, the token amount is NOT sent in msg.value
// It's encoded in the calldata for the target contract to handle.
transactionValue = messagingFee; // Only pay messaging fee
if (sourceLayer === 'l2') {
const antiDdosFee = parseEther('0.001'); // Add Anti-DDOS fee for L2->L1 ERC20 bridge
transactionValue += antiDdosFee;
console.log(`Adding 0.001 ETH Anti-DDOS fee for L2->L1 ERC20 bridge.`);
}
// --- Estimate Gas Fee (Bridge ERC20) ---
console.log(`Estimating gas for bridging ${amount} of token ${tokenAddress}...`);
let bridgeGasEstimate;
let bridgeGasPrice;
let bridgeEstimatedFeeEther;
try {
bridgeGasEstimate = await publicClient.estimateContractGas({
address: messageServiceAddress,
abi: messageServiceAbi,
functionName: 'sendMessage',
args: [destinationTokenBridgeAddress, messagingFee, deadline, messageCalldata],
value: transactionValue, // Only messaging fee (+ anti-DDOS if L2->L1)
account,
});
bridgeGasPrice = await publicClient.getGasPrice(); // Re-fetch or reuse approveGasPrice
bridgeEstimatedFeeEther = formatEther(bridgeGasEstimate * bridgeGasPrice); // Use correct variables
console.log(`Bridge Estimated Fee: ~${bridgeEstimatedFeeEther} 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) ---
// Confirmation should be handled before calling this function
// Proceed with sending...
// --- End Confirmation (Bridge ERC20) ---
// Send transaction
console.log(`Sending transaction...`);
txHash = await walletClient.writeContract({
chain: blockchain.currentChain,
account: account,
address: messageServiceAddress,
abi: messageServiceAbi,
functionName: 'sendMessage',
args: [destinationTokenBridgeAddress, messagingFee, deadline, messageCalldata],
value: transactionValue,
gas: bridgeGasEstimate, // Use correct estimated gas
gasPrice: bridgeGasPrice, // Use correct estimated gas price
});
// Wait for transaction confirmation and return result
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}`);
return {
success: true,
transactionHash: txHash,
sourceChain,
destinationChain,
recipient,
assetType,
tokenAddress: tokenAddressHex,
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.`,
};
}
else {
throw new Error('Invalid asset type or missing token address for ERC20.');
}
}
catch (error) {
// Handle MCP style confirmation errors if they bubble up (though they shouldn't reach here now)
if (typeof error === 'object' && error !== null && 'code' in error && (error.code === 'APPROVAL_CONFIRMATION_REQUIRED' || error.code === 'BRIDGE_CONFIRMATION_REQUIRED')) {
console.warn('Confirmation error reached main catch block:', error);
throw error; // Re-throw to be handled by the caller
}
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}`);
}
}
/**
* Bridge USDC using CCTP protocol
* @param params The parameters for bridging USDC
* @returns The transaction details or an abort message
*/
export async function bridgeUsdc(params) {
try {
const { sourceChain, destinationChain, tokenAddress, amount } = params;
if (!tokenAddress) {
throw new Error('Token address is required for USDC bridging');
}
// Validate chains
if (sourceChain === destinationChain) {
throw new Error('Source and destination chains must be different.');
}
// 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();
console.log(`Preparing to bridge USDC from ${sourceChain} to ${destinationChain} using CCTP protocol...`);
// Create Wallet Client for sending transactions
const walletClient = createWalletClient({
account,
chain: blockchain.currentChain,
transport: http(getRpcUrl(sourceNetwork))
});
// Fetch token decimals dynamically
console.log(`Fetching decimals for USDC ${tokenAddress}...`);
const decimals = await getTokenDecimals(publicClient, tokenAddress);
console.log(`USDC decimals: ${decimals}`);
const parsedAmount = parseUnits(amount, decimals);
// Get CCTP token messenger address from config
const networkEnv = getNetworkEnv(sourceNetwork);
const cctpMessengerAddress = sourceNetwork === 'ethereum'
? config.cctp[networkEnv].ethereum
: config.cctp[networkEnv].linea;
if (!isAddress(cctpMessengerAddress)) {
throw new Error(`CCTP Token Messenger address is invalid or not configured for ${sourceNetwork}.`);
}
// Determine destination domain based on destination chain
// These domain IDs are specific to CCTP and need to be configured
const destinationDomain = destinationChain === 'ethereum' ? 0 : 1; // Example values, adjust based on actual CCTP domains
// Convert recipient address to bytes32 format as required by CCTP
const mintRecipient = account.address.padStart(66, '0x000000000000000000000000');
// Check and approve token allowance for CCTP messenger if needed
console.log(`Checking USDC allowance for CCTP Token Messenger...`);
const currentAllowance = await publicClient.readContract({
address: tokenAddress,
abi: ERC20_ABI,
functionName: 'allowance',
args: [account.address, cctpMessengerAddress]
});
let _approvalNeeded = false;
if (currentAllowance < parsedAmount) {
_approvalNeeded = true;
console.log(`Approval needed for USDC. Current: ${formatUnits(currentAllowance, decimals)}, Required: ${amount}`);
// Estimate gas for approval
const approveGasEstimate = await publicClient.estimateContractGas({
address: tokenAddress,
abi: ERC20_ABI,
functionName: 'approve',
args: [cctpMessengerAddress, parsedAmount],
account,
});
const approveGasPrice = await publicClient.getGasPrice();
const approveEstimatedFeeEther = formatEther(approveGasEstimate * approveGasPrice);
console.log(`USDC Approval Estimated Fee: ~${approveEstimatedFeeEther} ETH`);
// Submit approval transaction
console.log(`Approving USDC for CCTP...`);
const approveTxHash = await walletClient.writeContract({
chain: blockchain.currentChain,
account,
address: tokenAddress,
abi: ERC20_ABI,
functionName: 'approve',
args: [cctpMessengerAddress, parsedAmount],
gas: approveGasEstimate,
gasPrice: approveGasPrice,
});
console.log(`USDC approval transaction submitted: ${approveTxHash}. Waiting for confirmation...`);
const approveReceipt = await publicClient.waitForTransactionReceipt({ hash: approveTxHash });
if (approveReceipt.status === 'reverted') {
throw new Error(`USDC approval failed (reverted). Hash: ${approveTxHash}`);
}
console.log('USDC approval successful.');
}
else {
console.log(`Sufficient USDC allowance already granted: ${formatUnits(currentAllowance, decimals)}`);
}
// Estimate gas for depositForBurn transaction
console.log(`Estimating gas for CCTP bridge transaction...`);
const bridgeGasEstimate = await publicClient.estimateContractGas({
address: cctpMessengerAddress,
abi: CCTP_TOKEN_MESSENGER_ABI,
functionName: 'depositForBurn',
args: [parsedAmount, destinationDomain, mintRecipient, tokenAddress],
account,
});
const bridgeGasPrice = await publicClient.getGasPrice();
const bridgeEstimatedFeeEther = formatEther(bridgeGasEstimate * bridgeGasPrice);
console.log(`CCTP Bridge Estimated Fee: ~${bridgeEstimatedFeeEther} ETH`);
// Submit CCTP bridge transaction
console.log(`Submitting CCTP bridge transaction...`);
const txHash = await walletClient.writeContract({
chain: blockchain.currentChain,
account,
address: cctpMessengerAddress,
abi: CCTP_TOKEN_MESSENGER_ABI,
functionName: 'depositForBurn',
args: [parsedAmount, destinationDomain, mintRecipient, tokenAddress],
gas: bridgeGasEstimate,
gasPrice: bridgeGasPrice,
});
console.log(`CCTP bridge transaction submitted: ${txHash}. Waiting for confirmation...`);
const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
if (receipt.status === 'reverted') {
throw new Error(`CCTP bridge transaction failed (reverted). Hash: ${txHash}`);
}
// Extract nonce from receipt logs if available
let nonce = null;
for (const log of receipt.logs) {
try {
if (log.address.toLowerCase() === cctpMessengerAddress.toLowerCase()) {
const decodedLog = decodeEventLog({
abi: CCTP_TOKEN_MESSENGER_ABI,
data: log.data,
topics: log.topics
});
if (decodedLog.eventName === 'DepositForBurn' && decodedLog.args.nonce) {
nonce = decodedLog.args.nonce.toString();
break;
}
}
}
catch (e) { /* Ignore decoding errors */ }
}
console.log(`CCTP bridge transaction confirmed. Nonce: ${nonce || 'Unknown'}`);
return {
success: true,
transactionHash: txHash,
sourceChain,
destinationChain,
assetType: 'USDC',
tokenAddress: tokenAddress,
amount,
from: account.address,
recipient: account.address,
status: 'initiated_on_source',
// CCTP typically requires manual claiming on both L1->L2 and L2->L1 directions
claimType: 'manual',
cctpNonce: nonce,
receipt: {
blockNumber: receipt.blockNumber.toString(),
gasUsed: receipt.gasUsed.toString(),
status: receipt.status,
},
message: `USDC bridge initiated via CCTP. Monitor destination chain for completion. Typically completes in ~20 minutes.`,
};
}
catch (error) {
console.error('Error in bridgeUsdc:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
throw new Error(`Failed to bridge USDC: ${errorMessage}`);
}
}
/**
* Automatic claim helper for bridged funds
* This attempts to send a claim transaction with a higher gas price to incentivize quick processing
* @param params The parameters for automatic claiming
* @returns The result of the automatic claim transaction
*/
export async function autoClaimFunds(params) {
try {
// Use a higher gas price multiplier for auto-claiming
const GAS_PRICE_MULTIPLIER = 1.2; // 20% higher than current gas price
// We'll use most of the same logic as manual claiming
const { messageHash, sourceChain, messageDetails, proof } = params;
if (!messageHash || !sourceChain || !messageDetails) {
throw new Error('Missing required parameters for auto-claiming funds');
}
const sourceNetwork = sourceChain;
const sourceLayer = getLayerType(sourceNetwork);
// Claim always happens on L1
if (sourceLayer !== 'l2') {
throw new Error('Auto-claim operation is only applicable for L2->L1 transfers');
}
// Get L1 information (destination for an L2->L1 transfer)
const destinationNetwork = 'ethereum';
const networkEnv = getNetworkEnv(sourceNetwork);
const l1MessageServiceAddress = config.nativeBridge[networkEnv].l1;
if (!isAddress(l1MessageServiceAddress)) {
throw new Error(`L1 Message Service address for ${networkEnv} is not configured or invalid.`);
}
console.log(`Auto-claim: Preparing to claim message ${messageHash} on Ethereum L1...`);
// Initialize services for the destination (L1) chain
const l1Blockchain = new BlockchainService(destinationNetwork);
const l1PublicClient = l1Blockchain.client;
const keyService = new KeyManagementService();
const account = keyService.getDefaultAccount();
// Create Wallet Client for sending the claim transaction
const walletClient = createWalletClient({
account,
chain: l1Blockchain.currentChain,
transport: http(getRpcUrl(destinationNetwork))
});
// Extract message details
const { from, to, fee, value, nonce, calldata } = messageDetails;
// Check if message is actually claimable
console.log('Auto-claim: Verifying claim status before proceeding...');
const messageStatusCode = await l1PublicClient.readContract({
address: l1MessageServiceAddress,
abi: L1_BRIDGE_MESSAGE_SERVICE_ABI,
functionName: 'messageStatus',
args: [messageHash]
});
if (messageStatusCode !== 2) {
const statusMap = {
0: 'NON_EXISTENT',
1: 'PENDING',
2: 'CLAIMABLE',
3: 'CLAIMED',
4: 'FAILED'
};
const statusText = statusMap[messageStatusCode] || 'UNKNOWN';
throw new Error(`Message is not in CLAIMABLE state for auto-claim. Current status: ${statusText} (${messageStatusCode})`);
}
console.log('Auto-claim: Message is confirmed as CLAIMABLE. Proceeding with auto-claim transaction...');
// Calculate postman fee based on gas estimates
// Formula from docs: target layer gas price * (gas estimated + gas limit surplus) * margin
const gasEstimated = 100000n;
const gasLimitSurplus = 6000n;
const margin = 2n;
const currentGasPrice = await l1PublicClient.getGasPrice();
const boostedGasPrice = BigInt(Math.floor(Number(currentGasPrice) * GAS_PRICE_MULTIPLIER));
const postmanFee = boostedGasPrice * (gasEstimated + gasLimitSurplus) * margin;
console.log(`Auto-claim: Calculated postman fee: ${formatEther(postmanFee)} ETH`);
// Estimate gas for the claim transaction with boosted gas price
console.log('Auto-claim: Estimating gas for auto-claim transaction...');
const gasEstimate = await l1PublicClient.estimateContractGas({
address: l1MessageServiceAddress,
abi: L1_BRIDGE_MESSAGE_SERVICE_ABI,
functionName: 'claimMessage',
args: [from, to, BigInt(fee), BigInt(value), BigInt(nonce), calldata, proof],
account,
});
const estimatedFeeEther = formatEther(gasEstimate * boostedGasPrice);
console.log(`Auto-claim: Estimated Fee (with boost): ~${estimatedFeeEther} ETH`);
// Send the auto-claim transaction with boosted gas price
console.log('Auto-claim: Sending auto-claim transaction...');
const txHash = await walletClient.writeContract({
chain: l1Blockchain.currentChain,
account,
address: l1MessageServiceAddress,
abi: L1_BRIDGE_MESSAGE_SERVICE_ABI,
functionName: 'claimMessage',
args: [from, to, BigInt(fee), BigInt(value), BigInt(nonce), calldata, proof],
gas: gasEstimate,
gasPrice: boostedGasPrice,
});
// Wait for transaction confirmation
console.log(`Auto-claim: Transaction submitted: ${txHash}. Waiting for confirmation...`);
const receipt = await l1PublicClient.waitForTransactionReceipt({ hash: txHash });
console.log(`Auto-claim: Transaction confirmed on ${destinationNetwork}. Status: ${receipt.status}`);
// Look for MessageClaimed event in logs
let messageClaimedEvent = null;
for (const log of receipt.logs) {
try {
if (log.address.toLowerCase() === l1MessageServiceAddress.toLowerCase()) {
const decodedLog = decodeEventLog({
abi: L1_BRIDGE_MESSAGE_SERVICE_ABI,
data: log.data,
topics: log.topics
});
if (decodedLog.eventName === 'MessageClaimed' &&
decodedLog.args.messageHash?.toLowerCase() === messageHash.toLowerCase()) {
messageClaimedEvent = decodedLog;
break;
}
}
}
catch (e) { /* Ignore decoding errors */ }
}
// Return auto-claim result
return {
success: receipt.status === 'success',
transactionHash: txHash,
messageHash,
sourceChain,
destinationChain: destinationNetwork,
claimEvent: messageClaimedEvent,
claimType: 'automatic',
postmanFee: formatEther(postmanFee),
status: receipt.status === 'success' ? 'auto_claimed' : 'auto_claim_failed',
message: receipt.status === 'success'
? 'Funds successfully auto-claimed on destination chain'
: 'Auto-claim transaction failed on destination chain',
receipt: {
blockNumber: receipt.blockNumber.toString(),
gasUsed: receipt.gasUsed.toString(),
status: receipt.status,
}
};
}
catch (error) {
console.error('Error in autoClaimFunds:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
throw new Error(`Failed to auto-claim funds: ${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;
// Validate source chain
if (!['ethereum', 'mainnet', 'testnet'].includes(sourceChain)) {
throw new Error('Invalid source chain specified.');
}
const sourceNetwork = sourceChain;
const sourceLayer = getLayerType(sourceNetwork);
// Determine destination chain (where the message needs to be claimed/status checked)
// Status checking and claiming always happens on L1
const destinationNetwork = (sourceLayer === 'l2') ? 'ethereum' : 'mainnet'; // Adjust if testnets need specific mapping
const _destinationChainId = (getNetworkEnv(destinationNetwork) === 'mainnet') ? 1 : 5; // Example: 1 for Ethereum Mainnet, 5 for Goerli/Sepolia (adjust based on actual testnet)
// 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_source' : 'not_found_source';
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. Hash: ${transactionHash}`,
receipt: receipt, // Include full receipt for debugging
};
}
// Transaction succeeded on source chain. Find the MessageSent event.
console.log('Transaction confirmed on source. Parsing logs for MessageSent event...');
const { messageServiceAddress: sourceMessageServiceAddr, messageServiceAbi: sourceMessageServiceAbi } = getBridgeConfig(sourceNetwork, destinationNetwork);
let messageSentLog = null;
for (const log of receipt.logs) {
try {
// Check if the log address matches the source message service address
if (log.address.toLowerCase() === sourceMessageServiceAddr.toLowerCase()) {
const decodedLog = decodeEventLog({ abi: sourceMessageServiceAbi, data: log.data, topics: log.topics });
if (decodedLog.eventName === 'MessageSent') {
messageSentLog = decodedLog;
break;
}
}
}
catch (e) { /* Ignore decoding errors for other events */ }
}
if (!messageSentLog) {
console.error('MessageSent event not found in transaction logs:', receipt.logs);
throw new Error('Could not find MessageSent event for the bridge transaction.');
}
const messageHash = messageSentLog.args.messageHash;
console.log(`Found MessageSent event with hash: ${messageHash}`);
// Now check the status on the destination (L1) chain
const destNetworkEnv = getNetworkEnv(destinationNetwork);
const l1MessageServiceAddress = config.nativeBridge[destNetworkEnv].l1;
const l1Blockchain = new BlockchainService(destinationNetwork); // Connect to destination L1
const l1PublicClient = l1Blockchain.client;
console.log(`Checking message status on ${destinationNetwork} (L1) contract ${l1MessageServiceAddress}...`);
let messageStatusCode;
try {
messageStatusCode = await l1PublicClient.readContract({
address: l1MessageServiceAddress,
abi: L1_BRIDGE_MESSAGE_SERVICE_ABI,
functionName: 'messageStatus',
args: [messageHash]
});
}
catch (statusError) {
console.error('Error fetching message status on L1:', statusError);
throw new Error(`Failed to fetch message status on destination chain: ${statusError instanceof Error ? statusError.message : 'Unknown error'}`);
}
// Interpret the status code (assuming 0: NON_EXISTENT, 1: PENDING, 2: CLAIMABLE, 3: CLAIMED, 4: FAILED - VERIFY THESE!)
let finalStatus;
let finalMessage;
let claimData = null;
switch (messageStatus