linea-mcp
Version:
A Model Context Protocol server for interacting with the Linea blockchain
959 lines (948 loc) • 49.9 kB
JavaScript
import { // Kept for post-confirmation logic
// parseEther, // Unused
formatEther, // Added for fee formatting
parseUnits, formatUnits, // Used in getTokenDecimals and getPoolInfo
// WalletClient, // Unused
// TransactionReceipt, // Unused
isAddress, zeroAddress, // viem's equivalent of AddressZero
// multicall is a client method, not a direct import
} from 'viem';
import KeyManagementService from '../../services/keyManagement.js';
import BlockchainService from '../../services/blockchain.js';
import config from '../../config/index.js'; // Now used for DEX addresses
import { erc20Abi, syncSwapMasterChefAbi } from './abis.js';
// --- ABIs (viem compatible) ---
// Note: These ABIs are simplified. Real DEX interactions might need more functions.
const DEX_ROUTER_ABI = [
{ name: 'swapExactTokensForTokens', type: 'function', stateMutability: 'nonpayable', inputs: [{ name: 'amountIn', type: 'uint256' }, { name: 'amountOutMin', type: 'uint256' }, { name: 'path', type: 'address[]' }, { name: 'to', type: 'address' }, { name: 'deadline', type: 'uint256' }], outputs: [{ name: 'amounts', type: 'uint256[]' }] },
{ name: 'swapExactETHForTokens', type: 'function', stateMutability: 'payable', inputs: [{ name: 'amountOutMin', type: 'uint256' }, { name: 'path', type: 'address[]' }, { name: 'to', type: 'address' }, { name: 'deadline', type: 'uint256' }], outputs: [{ name: 'amounts', type: 'uint256[]' }] },
{ name: 'swapExactTokensForETH', type: 'function', stateMutability: 'nonpayable', inputs: [{ name: 'amountIn', type: 'uint256' }, { name: 'amountOutMin', type: 'uint256' }, { name: 'path', type: 'address[]' }, { name: 'to', type: 'address' }, { name: 'deadline', type: 'uint256' }], outputs: [{ name: 'amounts', type: 'uint256[]' }] },
{ name: 'getAmountsOut', type: 'function', stateMutability: 'view', inputs: [{ name: 'amountIn', type: 'uint256' }, { name: 'path', type: 'address[]' }], outputs: [{ name: 'amounts', type: 'uint256[]' }] },
{ name: 'factory', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'address' }] },
{ name: 'WETH', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'address' }] }, // Some routers have a WETH() function
];
const FACTORY_ABI = [
{ name: 'getPair', type: 'function', stateMutability: 'view', inputs: [{ name: 'tokenA', type: 'address' }, { name: 'tokenB', type: 'address' }], outputs: [{ name: 'pair', type: 'address' }] },
{ name: 'allPairs', type: 'function', stateMutability: 'view', inputs: [{ name: '', type: 'uint256' }], outputs: [{ name: 'pair', type: 'address' }] },
{ name: 'allPairsLength', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'uint256' }] },
];
const PAIR_ABI = [
{ name: 'token0', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'address' }] },
{ name: 'token1', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'address' }] },
{ name: 'getReserves', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ name: 'reserve0', type: 'uint112' }, { name: 'reserve1', type: 'uint112' }, { name: 'blockTimestampLast', type: 'uint32' }] },
{ name: 'totalSupply', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'uint256' }] },
];
const ERC20_ABI_MINIMAL = [
{ name: 'decimals', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'uint8' }] },
{ name: 'symbol', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'string' }] },
{ name: 'name', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'string' }] },
{ name: 'approve', type: 'function', stateMutability: 'nonpayable', inputs: [{ name: 'spender', type: 'address' }, { name: 'amount', type: 'uint256' }], outputs: [{ name: '', type: 'bool' }] },
];
// -----------------------------
// Use actual config values, ensuring type safety
const DEX_ADDRESSES = {
ROUTER: (config.defi.routerAddress || '0x1111111111111111111111111111111111111111'), // Fallback placeholder
FACTORY: (config.defi.factoryAddress || '0x2222222222222222222222222222222222222222'), // Fallback placeholder
WETH: (config.defi.wethAddress || '0x3333333333333333333333333333333333333333'), // Fallback placeholder
};
const SYNC_SWAP_ADDRESSES = {
MASTER_CHEF: (config.defi.syncswap?.masterChef || '0x0000000000000000000000000000000000000000'), // Fallback needed
// Add ROUTER and FACTORY if needed by handlers, otherwise remove
// ROUTER: (config.defi.syncswap?.router || '0x0000000000000000000000000000000000000000') as Address,
// FACTORY: (config.defi.syncswap?.classicFactory || '0x0000000000000000000000000000000000000000') as Address,
};
/**
* 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';
}
}
/**
* Fetch token decimals - replace with a more robust solution if possible (Used in swapTokens)
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function getTokenDecimals(tokenAddress, publicClient) {
if (!isAddress(tokenAddress))
throw new Error("Invalid token address for fetching decimals.");
try {
const decimals = await publicClient.readContract({
address: tokenAddress,
abi: ERC20_ABI_MINIMAL,
functionName: 'decimals',
});
return decimals;
}
catch (e) {
console.error(`Failed to fetch decimals for ${tokenAddress}:`, e);
throw new Error(`Could not fetch decimals for token ${tokenAddress}. Is it a valid ERC20 token?`);
}
}
/**
* Helper function to get the LP token address for a given pool ID from SyncSwap MasterChef
*/
async function getLpTokenForPool(poolId, publicClient) {
try {
const poolInfo = await publicClient.readContract({
address: SYNC_SWAP_ADDRESSES.MASTER_CHEF,
abi: syncSwapMasterChefAbi,
functionName: 'poolInfo',
args: [BigInt(poolId)]
});
// poolInfo might be an array/tuple based on ABI: [lpToken, allocPoint, lastRewardBlock, accRewardPerShare]
// Assuming lpToken is the first element
if (poolInfo && typeof poolInfo === 'object' && 'lpToken' in poolInfo) {
const lpTokenAddress = poolInfo.lpToken;
if (!isAddress(lpTokenAddress) || lpTokenAddress === zeroAddress) {
throw new Error(`LP token address for pool ${poolId} is invalid or zero.`);
}
return lpTokenAddress;
}
else {
// Handle cases where the structure might be different (e.g., older ABI returning a tuple)
if (Array.isArray(poolInfo) && poolInfo.length > 0 && isAddress(poolInfo[0])) {
const lpTokenAddress = poolInfo[0];
if (lpTokenAddress === zeroAddress) {
throw new Error(`LP token address for pool ${poolId} is zero.`);
}
return lpTokenAddress;
}
console.error('Unexpected poolInfo structure:', poolInfo);
throw new Error(`Could not extract LP token address from poolInfo for pool ${poolId}. Unexpected structure.`);
}
}
catch (error) {
console.error(`Error fetching LP token for pool ${poolId}:`, error);
throw new Error(`Failed to get LP token address for pool ${poolId}: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Swap tokens on a DEX using viem, with fee estimation and confirmation
* @param params The parameters for swapping tokens
* @returns The transaction details or an abort message
*/
export async function swapTokens(params) {
try {
const { fromToken, toToken, amount, slippageTolerance = 0.5 } = params; // Default slippage 0.5%
// Validate addresses (allow 'ETH' string)
if (fromToken.toLowerCase() !== 'eth' && !isAddress(fromToken)) {
throw new Error('Invalid "fromToken" address.');
}
if (toToken.toLowerCase() !== 'eth' && !isAddress(toToken)) {
throw new Error('Invalid "toToken" address.');
}
// Initialize services
const blockchain = new BlockchainService('mainnet'); // Assuming mainnet
const publicClient = blockchain.client;
const keyService = new KeyManagementService();
const account = keyService.getDefaultAccount();
// Determine swap type and path
const isFromETH = fromToken.toLowerCase() === 'eth';
const isToETH = toToken.toLowerCase() === 'eth';
const path = [];
let fromTokenAddress;
let toTokenAddress;
let fromDecimals;
let fromSymbol = 'Token'; // Type inferred
if (isFromETH) {
path.push(DEX_ADDRESSES.WETH);
fromTokenAddress = DEX_ADDRESSES.WETH; // Use WETH internally
fromDecimals = 18; // ETH/WETH always has 18 decimals
fromSymbol = 'ETH';
}
else {
fromTokenAddress = fromToken;
path.push(fromTokenAddress);
// Fetch decimals and symbol together
try {
const results = await publicClient.multicall({ contracts: [
{ address: fromTokenAddress, abi: ERC20_ABI_MINIMAL, functionName: 'decimals' },
{ address: fromTokenAddress, abi: ERC20_ABI_MINIMAL, functionName: 'symbol' },
], allowFailure: false });
fromDecimals = results[0];
fromSymbol = results[1];
}
catch (e) {
console.error(`Failed to fetch details for fromToken ${fromTokenAddress}:`, e);
throw new Error(`Could not fetch details for token ${fromTokenAddress}. Is it a valid ERC20 token?`);
}
}
if (isToETH) {
path.push(DEX_ADDRESSES.WETH);
toTokenAddress = DEX_ADDRESSES.WETH;
}
else {
toTokenAddress = toToken;
path.push(toTokenAddress);
}
// Parse input amount
const parsedAmountIn = parseUnits(amount, fromDecimals);
// Get expected output amount using readContract
console.log(`Getting quote for swapping ${amount} ${fromSymbol} -> ${isToETH ? 'ETH' : toTokenAddress}...`);
const amountsOut = await publicClient.readContract({
address: DEX_ADDRESSES.ROUTER,
abi: DEX_ROUTER_ABI,
functionName: 'getAmountsOut',
args: [parsedAmountIn, path],
});
const expectedOutput = amountsOut[amountsOut.length - 1];
// Calculate minimum output with slippage tolerance (using bigint)
const slippageFactor = BigInt(Math.floor((1 - slippageTolerance / 100) * 10000)); // Use 10000 for precision
const minOutput = (expectedOutput * slippageFactor) / 10000n;
// Set deadline (e.g., 20 minutes from now)
const deadline = BigInt(Math.floor(Date.now() / 1000) + 1200);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let txHash; // Kept for post-confirmation logic
let gasEstimate;
let gasPrice;
let estimatedFeeEther;
let approveGasEstimate;
let approveGasPrice;
let approveEstimatedFeeEther;
// --- Approve Router if needed (Token -> ETH or Token -> Token) ---
if (!isFromETH) {
console.log(`Estimating gas for approving router ${DEX_ADDRESSES.ROUTER} to spend ${amount} ${fromSymbol}...`);
try {
approveGasEstimate = await publicClient.estimateContractGas({
address: fromTokenAddress,
abi: ERC20_ABI_MINIMAL,
functionName: 'approve',
args: [DEX_ADDRESSES.ROUTER, parsedAmountIn],
account,
});
approveGasPrice = await publicClient.getGasPrice();
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'}`);
}
// Ask for Approval Confirmation
throw new Error(`CONFIRMATION_REQUIRED: Step 1/2: Estimated fee to approve router for ${amount} ${fromSymbol} is ~${approveEstimatedFeeEther} ETH. Proceed? (Yes/No)`);
/*
// --- Code to run *after* user confirms Approval (Yes) ---
const walletClient = createWalletClient({ account, chain: blockchain.currentChain, transport: http(getRpcUrl('mainnet')) });
console.log(`Proceeding with token approval...`);
const approveHash = await walletClient.writeContract({
address: fromTokenAddress,
abi: ERC20_ABI_MINIMAL,
functionName: 'approve',
args: [DEX_ADDRESSES.ROUTER, parsedAmountIn],
gas: approveGasEstimate,
gasPrice: approveGasPrice,
});
console.log(`Approval 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('Approval successful.');
// --- End Post-Confirmation Code (Approval) ---
*/
}
// ----------------------------------------------------------------
// --- Estimate Gas for Swap ---
console.log(`Estimating gas for swap transaction...`);
try {
if (isFromETH) {
gasEstimate = await publicClient.estimateContractGas({
address: DEX_ADDRESSES.ROUTER,
abi: DEX_ROUTER_ABI,
functionName: 'swapExactETHForTokens',
args: [minOutput, path, account.address, deadline],
account,
value: parsedAmountIn,
});
}
else if (isToETH) {
gasEstimate = await publicClient.estimateContractGas({
address: DEX_ADDRESSES.ROUTER,
abi: DEX_ROUTER_ABI,
functionName: 'swapExactTokensForETH',
args: [parsedAmountIn, minOutput, path, account.address, deadline],
account,
// value: undefined (default)
});
}
else {
gasEstimate = await publicClient.estimateContractGas({
address: DEX_ADDRESSES.ROUTER,
abi: DEX_ROUTER_ABI,
functionName: 'swapExactTokensForTokens',
args: [parsedAmountIn, minOutput, path, account.address, deadline],
account,
// value: undefined (default)
});
}
gasPrice = await publicClient.getGasPrice(); // Re-fetch or use approveGasPrice if available and recent
estimatedFeeEther = formatEther(gasEstimate * gasPrice);
console.log(`Swap Estimated Fee: ~${estimatedFeeEther} ETH`);
}
catch (estimationError) {
console.error("Error estimating swap gas:", estimationError);
throw new Error(`Failed to estimate gas fee for swap: ${estimationError instanceof Error ? estimationError.message : 'Unknown error'}`);
}
// --- End Estimation (Swap) ---
// --- Ask for Confirmation (Swap) ---
let confirmationMsg;
if (approveEstimatedFeeEther) {
// Calculate total fee only if approval was needed
// Safely check if approveGasEstimate and approveGasPrice are defined before using them
const approvalFee = (approveGasEstimate && approveGasPrice) ? (approveGasEstimate * approveGasPrice) : 0n;
const totalFee = formatEther(approvalFee + (gasEstimate * gasPrice));
confirmationMsg = `Step 2/2: Estimated fee for the swap is ~${estimatedFeeEther} ETH (Total estimated: ~${totalFee} ETH). Proceed? (Yes/No)`;
}
else {
confirmationMsg = `Estimated fee for the swap is ~${estimatedFeeEther} ETH. Proceed? (Yes/No)`;
}
throw new Error(`CONFIRMATION_REQUIRED: ${confirmationMsg}`);
// --- End Confirmation (Swap) ---
/*
// --- Code to run *after* user confirms Swap (Yes) ---
// Ensure walletClient is defined
const walletClient = createWalletClient({ account, chain: blockchain.currentChain, transport: http(getRpcUrl('mainnet')) });
console.log(`Proceeding with swap...`);
// Execute the correct swap function based on the case
if (isFromETH) {
txHash = await walletClient.writeContract({
address: DEX_ADDRESSES.ROUTER,
abi: DEX_ROUTER_ABI,
functionName: 'swapExactETHForTokens',
args: [minOutput, path, account.address, deadline],
value: parsedAmountIn,
gas: gasEstimate,
gasPrice: gasPrice,
});
} else if (isToETH) {
txHash = await walletClient.writeContract({
address: DEX_ADDRESSES.ROUTER,
abi: DEX_ROUTER_ABI,
functionName: 'swapExactTokensForETH',
args: [parsedAmountIn, minOutput, path, account.address, deadline],
gas: gasEstimate,
gasPrice: gasPrice,
});
} else {
txHash = await walletClient.writeContract({
address: DEX_ADDRESSES.ROUTER,
abi: DEX_ROUTER_ABI,
functionName: 'swapExactTokensForTokens',
args: [parsedAmountIn, minOutput, path, account.address, deadline],
gas: gasEstimate,
gasPrice: gasPrice,
});
}
console.log(`Swap transaction submitted: ${txHash}. Waiting for confirmation...`);
// --- End Confirmation (Swap) ---
/*
// --- Code to run *after* user confirms Swap (Yes) ---
// Ensure walletClient is defined
const walletClient = createWalletClient({ account, chain: blockchain.currentChain, transport: http(getRpcUrl('mainnet')) });
console.log(`Proceeding with swap...`);
// Execute the correct swap function based on the case
if (isFromETH) {
txHash = await walletClient.writeContract({
address: DEX_ADDRESSES.ROUTER,
abi: DEX_ROUTER_ABI,
functionName: 'swapExactETHForTokens',
args: [minOutput, path, account.address, deadline],
value: parsedAmountIn,
gas: gasEstimate,
gasPrice: gasPrice,
});
} else if (isToETH) {
txHash = await walletClient.writeContract({
address: DEX_ADDRESSES.ROUTER,
abi: DEX_ROUTER_ABI,
functionName: 'swapExactTokensForETH',
args: [parsedAmountIn, minOutput, path, account.address, deadline],
gas: gasEstimate,
gasPrice: gasPrice,
});
} else {
txHash = await walletClient.writeContract({
address: DEX_ADDRESSES.ROUTER,
abi: DEX_ROUTER_ABI,
functionName: 'swapExactTokensForTokens',
args: [parsedAmountIn, minOutput, path, account.address, deadline],
gas: gasEstimate,
gasPrice: gasPrice,
});
}
console.log(`Swap transaction submitted: ${txHash}. Waiting for confirmation...`);
gasPrice: gasPrice,
});
console.log(`Swap transaction submitted: ${txHash}. Waiting for confirmation...`);
const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
console.log(`Swap transaction confirmed. Status: ${receipt.status}`);
if (receipt.status === 'reverted') {
throw new Error(`Swap transaction failed (reverted). Hash: ${txHash}`);
}
const toDecimals = isToETH ? 18 : await getTokenDecimals(toTokenAddress, publicClient);
return {
success: true,
transactionHash: txHash,
fromToken: isFromETH ? 'ETH' : fromTokenAddress,
toToken: isToETH ? 'ETH' : toTokenAddress,
amountIn: amount,
expectedAmountOut: formatUnits(expectedOutput, toDecimals),
minAmountOut: formatUnits(minOutput, toDecimals),
slippageTolerance,
from: account.address,
receipt: {
blockNumber: receipt.blockNumber.toString(),
gasUsed: receipt.gasUsed.toString(),
status: receipt.status,
},
estimatedFee: estimatedFeeEther, // Add swap estimate
approveEstimatedFee: approveEstimatedFeeEther // Add approval estimate if applicable
};
// --- End Post-Confirmation Code (Swap) ---
*/
}
catch (error) {
// Re-throw confirmation request errors
if (error instanceof Error && error.message.startsWith('CONFIRMATION_REQUIRED:')) {
throw error;
}
console.error('Error in swapTokens:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
if (errorMessage.includes('insufficient funds')) {
throw new Error(`Failed to swap tokens: Insufficient funds for transaction or approval.`);
}
else if (errorMessage.includes('reverted')) {
throw new Error(`Failed to swap tokens: Transaction reverted. Check slippage, approval, or path.`);
}
throw new Error(`Failed to swap tokens: ${errorMessage}`);
}
}
/**
* Get information about liquidity pools using viem
* @param params The parameters for getting liquidity pool information
* @returns The liquidity pool information
*/
export async function liquidityPools(params) {
try {
const { poolAddress, tokenA, tokenB } = params;
const blockchain = new BlockchainService('mainnet'); // Assuming mainnet
const publicClient = blockchain.client;
// If a specific pool is requested
if (poolAddress) {
if (!isAddress(poolAddress))
throw new Error("Invalid pool address provided.");
return await getPoolInfo(poolAddress, publicClient);
}
// If a token pair is specified
if (tokenA && tokenB) {
if (!isAddress(tokenA) || !isAddress(tokenB))
throw new Error("Invalid token addresses provided.");
console.log(`Fetching pair address for ${tokenA} / ${tokenB}...`);
const pairAddress = await publicClient.readContract({
address: DEX_ADDRESSES.FACTORY,
abi: FACTORY_ABI,
functionName: 'getPair',
args: [tokenA, tokenB],
});
if (pairAddress === zeroAddress) {
return {
success: true,
pools: [],
message: 'No liquidity pool found for the specified token pair.',
};
}
console.log(`Found pair address: ${pairAddress}`);
return await getPoolInfo(pairAddress, publicClient);
}
// If no specific pool or token pair is requested
// TODO: Implement fetching multiple pools (e.g., via allPairsLength/allPairs or indexer)
console.warn("Fetching all pools is not implemented. Provide a poolAddress or tokenA/tokenB.");
return {
success: true,
pools: [],
message: 'Listing all pools requires further implementation (e.g., using allPairs or an indexer). Please specify a pool address or token pair.',
};
}
catch (error) {
console.error('Error in liquidityPools:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
throw new Error(`Failed to get liquidity pool information: ${errorMessage}`);
}
}
/**
* Get information about a specific liquidity pool using viem and multicall
* @param poolAddress The address of the pool
* @param publicClient The viem PublicClient instance
* @returns The pool information
*/
async function getPoolInfo(poolAddress, publicClient) {
try {
console.log(`Fetching info for pool: ${poolAddress}`);
// Define contracts for multicall
const pairContract = { address: poolAddress, abi: PAIR_ABI };
// Initial reads for token addresses
const [token0Address, token1Address] = await publicClient.multicall({
contracts: [
{ ...pairContract, functionName: 'token0' },
{ ...pairContract, functionName: 'token1' },
],
allowFailure: false,
});
if (!isAddress(token0Address) || !isAddress(token1Address)) {
throw new Error(`Invalid token addresses returned by pool ${poolAddress}`);
}
// Define token contracts
const token0Contract = { address: token0Address, abi: ERC20_ABI_MINIMAL };
const token1Contract = { address: token1Address, abi: ERC20_ABI_MINIMAL };
// Multicall for all remaining details using the client method
const results = await publicClient.multicall({
contracts: [
{ ...token0Contract, functionName: 'symbol' }, // 0
{ ...token0Contract, functionName: 'name' }, // 1
{ ...token0Contract, functionName: 'decimals' }, // 2
{ ...token1Contract, functionName: 'symbol' }, // 3
{ ...token1Contract, functionName: 'name' }, // 4
{ ...token1Contract, functionName: 'decimals' }, // 5
{ ...pairContract, functionName: 'getReserves' }, // 6
{ ...pairContract, functionName: 'totalSupply' }, // 7
],
allowFailure: false, // Throw if any call fails
});
// Destructure results with type safety
const [token0Symbol, token0Name, token0Decimals, token1Symbol, token1Name, token1Decimals, reserves, totalSupply] = results;
// Format reserves
const reserve0 = formatUnits(reserves[0], token0Decimals);
const reserve1 = formatUnits(reserves[1], token1Decimals);
// Calculate prices (handle potential division by zero)
const reserve0Num = parseFloat(reserve0);
const reserve1Num = parseFloat(reserve1);
const price0In1 = reserve0Num !== 0 ? reserve1Num / reserve0Num : 0;
const price1In0 = reserve1Num !== 0 ? reserve0Num / reserve1Num : 0;
return {
success: true,
pool: {
address: poolAddress,
token0: {
address: token0Address,
symbol: token0Symbol,
name: token0Name,
decimals: token0Decimals,
reserve: reserve0,
},
token1: {
address: token1Address,
symbol: token1Symbol,
name: token1Name,
decimals: token1Decimals,
reserve: reserve1,
},
totalSupply: formatUnits(totalSupply, 18), // LP tokens usually have 18 decimals
prices: {
[`${token0Symbol}_in_${token1Symbol}`]: price0In1, // Use underscore for safer keys
[`${token1Symbol}_in_${token0Symbol}`]: price1In0,
},
lastUpdatedTimestamp: reserves[2],
},
};
}
catch (error) {
console.error(`Error in getPoolInfo for ${poolAddress}:`, error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
if (errorMessage.includes('call revert')) {
throw new Error(`Failed to get pool info: Contract call failed. Is ${poolAddress} a valid LP pair address?`);
}
throw new Error(`Failed to get pool information: ${errorMessage}`);
}
}
// --- DeFi Staking/Farming Handlers ---
/**
* Stake LP tokens into a SyncSwap MasterChef farm.
* Requires prior approval of the MasterChef contract by the user to spend the LP tokens.
*/
export async function stakeLpTokens(params) {
try {
const { poolId, amount } = params;
// Initialize services
const blockchain = new BlockchainService('mainnet'); // Assuming Linea mainnet
const publicClient = blockchain.client;
const keyService = new KeyManagementService();
const account = keyService.getDefaultAccount();
// 1. Get LP Token Address from Pool ID
console.log(`Fetching LP token address for pool ID: ${poolId}...`);
const lpTokenAddress = await getLpTokenForPool(poolId, publicClient);
console.log(`LP Token for Pool ${poolId}: ${lpTokenAddress}`);
// 2. Get LP Token Decimals
let lpDecimals;
let lpSymbol = 'LP Token';
try {
const results = await publicClient.multicall({
contracts: [
{ address: lpTokenAddress, abi: erc20Abi, functionName: 'decimals' },
{ address: lpTokenAddress, abi: erc20Abi, functionName: 'symbol' },
], allowFailure: false
});
lpDecimals = results[0];
lpSymbol = results[1];
}
catch (e) {
console.error(`Failed to fetch details for LP token ${lpTokenAddress}:`, e);
throw new Error(`Could not fetch details for LP token ${lpTokenAddress}. Is it a valid ERC20 token?`);
}
// 3. Parse Amount
const parsedAmount = parseUnits(amount, lpDecimals);
// 4. Check Allowance (Crucial Step!)
console.log(`Checking allowance for MasterChef (${SYNC_SWAP_ADDRESSES.MASTER_CHEF}) to spend ${amount} ${lpSymbol}...`);
const allowance = await publicClient.readContract({
address: lpTokenAddress,
abi: erc20Abi,
functionName: 'allowance',
args: [account.address, SYNC_SWAP_ADDRESSES.MASTER_CHEF]
});
if (allowance < parsedAmount) {
// Allowance is insufficient - Request approval first
console.log(`Insufficient allowance. Current: ${formatUnits(allowance, lpDecimals)}, Required: ${amount}`);
// Estimate gas for approval
let approveGasEstimate;
let approveGasPrice;
let approveEstimatedFeeEther;
try {
approveGasEstimate = await publicClient.estimateContractGas({
address: lpTokenAddress,
abi: erc20Abi,
functionName: 'approve',
args: [SYNC_SWAP_ADDRESSES.MASTER_CHEF, parsedAmount], // Approve the exact amount needed
account,
});
approveGasPrice = await publicClient.getGasPrice();
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'}`);
}
// Throw confirmation error for approval step
throw new Error(`CONFIRMATION_REQUIRED: Step 1/2: MasterChef requires approval to spend your ${lpSymbol}. Estimated approval fee: ~${approveEstimatedFeeEther} ETH. Proceed with approval? (Yes/No)`);
/*
// --- Code to run *after* user confirms Approval (Yes) ---
const walletClient = createWalletClient({ account, chain: blockchain.currentChain, transport: http(getRpcUrl('mainnet')) });
console.log(`Proceeding with token approval for MasterChef...`);
const approveHash = await walletClient.writeContract({
address: lpTokenAddress,
abi: erc20Abi,
functionName: 'approve',
args: [SYNC_SWAP_ADDRESSES.MASTER_CHEF, parsedAmount],
gas: approveGasEstimate,
gasPrice: approveGasPrice,
});
console.log(`Approval submitted: ${approveHash}. Waiting for confirmation...`);
const approveReceipt = await publicClient.waitForTransactionReceipt({ hash: approveHash });
if (approveReceipt.status === 'reverted') {
throw new Error(`Token approval for MasterChef failed (reverted). Hash: ${approveHash}`);
}
console.log('Approval successful.');
// --- End Post-Confirmation Code (Approval) ---
*/
}
console.log(`Sufficient allowance found (${formatUnits(allowance, lpDecimals)} ${lpSymbol}).`);
// 5. Estimate Gas for Staking (deposit)
console.log(`Estimating gas for staking ${amount} ${lpSymbol} in pool ${poolId}...`);
let stakeGasEstimate;
let stakeGasPrice;
let stakeEstimatedFeeEther;
try {
// Revert to using estimateContractGas directly
stakeGasEstimate = await publicClient.estimateContractGas({
address: SYNC_SWAP_ADDRESSES.MASTER_CHEF,
abi: syncSwapMasterChefAbi,
functionName: 'deposit',
args: [BigInt(poolId), parsedAmount],
account,
});
/* // Remove simulation block
const { request } = await publicClient.simulateContract({
address: SYNC_SWAP_ADDRESSES.MASTER_CHEF,
abi: syncSwapMasterChefAbi,
functionName: 'deposit',
args: [BigInt(poolId), parsedAmount],
account,
});
// Use estimateContractGas with the simulated request for better accuracy
stakeGasEstimate = await publicClient.estimateGas(request);
*/
stakeGasPrice = await publicClient.getGasPrice();
stakeEstimatedFeeEther = formatEther(stakeGasEstimate * stakeGasPrice);
console.log(`Staking Estimated Fee: ~${stakeEstimatedFeeEther} ETH`);
}
catch (estimationError) {
console.error("Error estimating staking gas:", estimationError);
// Try to provide a more specific error message if possible
if (estimationError instanceof Error && estimationError.message.includes('insufficient funds')) {
throw new Error('Estimation failed: Insufficient funds for gas.');
}
if (estimationError instanceof Error && estimationError.message.includes('allowance')) {
// This shouldn't happen if the allowance check passed, but check just in case
throw new Error('Estimation failed: Insufficient token allowance. Please try approving first.');
}
if (estimationError instanceof Error && estimationError.message.includes('execution reverted')) {
// Catch generic reverts which might indicate other issues like pool limits, etc.
// Look for specific revert reasons if the RPC provides them
const revertReason = estimationError.shortMessage || estimationError.message;
console.error("Revert reason (from estimation):", revertReason);
throw new Error(`Estimation failed: Transaction likely to revert. Reason: ${revertReason}`);
}
throw new Error(`Failed to estimate gas fee for staking: ${estimationError instanceof Error ? estimationError.message : 'Unknown estimation error'}`);
}
// 6. Ask for Staking Confirmation
const confirmationMessage = allowance < parsedAmount
? `CONFIRMATION_REQUIRED: Step 2/2: Proceed with staking ${amount} ${lpSymbol} (Pool ID: ${poolId})? Estimated staking fee: ~${stakeEstimatedFeeEther} ETH. (Approval fee was separate).`
: `CONFIRMATION_REQUIRED: Proceed with staking ${amount} ${lpSymbol} (Pool ID: ${poolId})? Estimated staking fee: ~${stakeEstimatedFeeEther} ETH.`;
throw new Error(confirmationMessage);
/*
// --- Code to run *after* user confirms Staking (Yes) ---
const walletClient = createWalletClient({ account, chain: blockchain.currentChain, transport: http(getRpcUrl('mainnet')) });
console.log(`Proceeding with staking...`);
const stakeHash = await walletClient.writeContract({
address: SYNC_SWAP_ADDRESSES.MASTER_CHEF,
abi: syncSwapMasterChefAbi,
functionName: 'deposit',
args: [BigInt(poolId), parsedAmount],
gas: stakeGasEstimate,
gasPrice: stakeGasPrice,
});
console.log(`Staking transaction submitted: ${stakeHash}. Waiting for confirmation...`);
const stakeReceipt = await publicClient.waitForTransactionReceipt({ hash: stakeHash });
if (stakeReceipt.status === 'reverted') {
throw new Error(`Staking transaction failed (reverted). Hash: ${stakeHash}`);
}
console.log('Staking successful.');
return {
status: 'success',
message: `Successfully staked ${amount} ${lpSymbol} in pool ${poolId}.`,
transactionHash: stakeHash,
explorerUrl: blockchain.getExplorerUrl(stakeHash),
};
// --- End Post-Confirmation Code (Staking) ---
*/
}
catch (error) {
console.error('[stakeLpTokens Error]:', error);
// Don't re-wrap confirmation errors
if (error instanceof Error && error.message.startsWith('CONFIRMATION_REQUIRED:')) {
return { status: 'confirmation_required', message: error.message };
}
return { status: 'error', message: `Staking failed: ${error instanceof Error ? error.message : 'Unknown error'}` };
}
}
/**
* Unstake LP tokens from a SyncSwap MasterChef farm.
*/
export async function unstakeLpTokens(params) {
try {
const { poolId, amount } = params;
// Initialize services
const blockchain = new BlockchainService('mainnet'); // Assuming Linea mainnet
const publicClient = blockchain.client;
const keyService = new KeyManagementService();
const account = keyService.getDefaultAccount();
// 1. Get LP Token Address and Decimals (needed for formatting messages)
let lpTokenAddress;
let lpDecimals;
let lpSymbol = 'LP Token';
try {
lpTokenAddress = await getLpTokenForPool(poolId, publicClient);
const results = await publicClient.multicall({
contracts: [
{ address: lpTokenAddress, abi: erc20Abi, functionName: 'decimals' },
{ address: lpTokenAddress, abi: erc20Abi, functionName: 'symbol' },
], allowFailure: false
});
lpDecimals = results[0];
lpSymbol = results[1];
}
catch (e) {
console.warn(`Could not fetch details for LP token in pool ${poolId}:`, e);
// Continue without symbol/decimals if lookup fails, but amounts will be less clear
lpDecimals = 18; // Assume 18 if lookup fails
}
// 2. Parse Amount
const parsedAmount = parseUnits(amount, lpDecimals);
// 3. Estimate Gas for Unstaking (withdraw)
console.log(`Estimating gas for unstaking ${amount} ${lpSymbol} from pool ${poolId}...`);
let unstakeGasEstimate;
let unstakeGasPrice;
let unstakeEstimatedFeeEther;
try {
// Revert to using estimateContractGas directly
unstakeGasEstimate = await publicClient.estimateContractGas({
address: SYNC_SWAP_ADDRESSES.MASTER_CHEF,
abi: syncSwapMasterChefAbi,
functionName: 'withdraw',
args: [BigInt(poolId), parsedAmount],
account,
});
/* // Remove simulation block
const { request } = await publicClient.simulateContract({
address: SYNC_SWAP_ADDRESSES.MASTER_CHEF,
abi: syncSwapMasterChefAbi,
functionName: 'withdraw',
args: [BigInt(poolId), parsedAmount],
account,
});
unstakeGasEstimate = await publicClient.estimateGas(request);
*/
unstakeGasPrice = await publicClient.getGasPrice();
unstakeEstimatedFeeEther = formatEther(unstakeGasEstimate * unstakeGasPrice);
console.log(`Unstaking Estimated Fee: ~${unstakeEstimatedFeeEther} ETH`);
}
catch (estimationError) {
console.error("Error estimating unstaking gas:", estimationError);
if (estimationError instanceof Error && (estimationError.message.includes('ERC20: transfer amount exceeds balance') || estimationError.message.includes('withdraw: not good'))) {
// MasterChef often reverts with specific messages for insufficient staked balance
throw new Error('Estimation failed: Insufficient staked balance or invalid amount.');
}
if (estimationError instanceof Error && estimationError.message.includes('insufficient funds')) {
throw new Error('Estimation failed: Insufficient funds for gas.');
}
if (estimationError instanceof Error && estimationError.message.includes('execution reverted')) {
const revertReason = estimationError.shortMessage || estimationError.message;
console.error("Revert reason (from estimation):", revertReason);
throw new Error(`Estimation failed: Transaction likely to revert. Reason: ${revertReason}`);
}
throw new Error(`Failed to estimate gas fee for unstaking: ${estimationError instanceof Error ? estimationError.message : 'Unknown estimation error'}`);
}
// 4. Ask for Unstaking Confirmation
throw new Error(`CONFIRMATION_REQUIRED: Proceed with unstaking ${amount} ${lpSymbol} (Pool ID: ${poolId})? Estimated unstaking fee: ~${unstakeEstimatedFeeEther} ETH.`);
/*
// --- Code to run *after* user confirms Unstaking (Yes) ---
const walletClient = createWalletClient({ account, chain: blockchain.currentChain, transport: http(getRpcUrl('mainnet')) });
console.log(`Proceeding with unstaking...`);
const unstakeHash = await walletClient.writeContract({
address: SYNC_SWAP_ADDRESSES.MASTER_CHEF,
abi: syncSwapMasterChefAbi,
functionName: 'withdraw',
args: [BigInt(poolId), parsedAmount],
gas: unstakeGasEstimate,
gasPrice: unstakeGasPrice,
});
console.log(`Unstaking transaction submitted: ${unstakeHash}. Waiting for confirmation...`);
const unstakeReceipt = await publicClient.waitForTransactionReceipt({ hash: unstakeHash });
if (unstakeReceipt.status === 'reverted') {
throw new Error(`Unstaking transaction failed (reverted). Hash: ${unstakeHash}`);
}
console.log('Unstaking successful.');
return {
status: 'success',
message: `Successfully unstaked ${amount} ${lpSymbol} from pool ${poolId}.`,
transactionHash: unstakeHash,
explorerUrl: blockchain.getExplorerUrl(unstakeHash),
};
// --- End Post-Confirmation Code (Unstaking) ---
*/
}
catch (error) {
console.error('[unstakeLpTokens Error]:', error);
// Don't re-wrap confirmation errors
if (error instanceof Error && error.message.startsWith('CONFIRMATION_REQUIRED:')) {
return { status: 'confirmation_required', message: error.message };
}
return { status: 'error', message: `Unstaking failed: ${error instanceof Error ? error.message : 'Unknown error'}` };
}
}
/**
* Get yield farming information (staked amount, pending rewards) for a user in a specific SyncSwap pool.
*/
export async function getYieldInfo(params) {
try {
const { userAddress, poolId } = params;
// Validate address
if (!isAddress(userAddress)) {
throw new Error('Invalid user address provided.');
}
const userAddr = userAddress;
// Initialize services
const blockchain = new BlockchainService('mainnet'); // Assuming Linea mainnet
const publicClient = blockchain.client;
// 1. Get LP Token details (address, symbol, decimals) for context
let lpTokenAddress;
let lpSymbol = 'LP Token';
let lpDecimals = 18; // Default to 18
try {
lpTokenAddress = await getLpTokenForPool(poolId, publicClient);
const results = await publicClient.multicall({
contracts: [
{ address: lpTokenAddress, abi: erc20Abi, functionName: 'decimals' },
{ address: lpTokenAddress, abi: erc20Abi, functionName: 'symbol' },
], allowFailure: true // Allow failure here, just provide less context
});
if (results[0].status === 'success')
lpDecimals = results[0].result;
if (results[1].status === 'success')
lpSymbol = results[1].result;
}
catch (e) {
console.warn(`Could not fetch details for LP token in pool ${poolId}:`, e);
// Continue without symbol/decimals if lookup fails
}
// 2. Use multicall to fetch userInfo and pendingReward simultaneously
console.log(`Fetching staking info for user ${userAddr} in pool ${poolId}...`);
const multicallResult = await publicClient.multicall({
contracts: [
{
address: SYNC_SWAP_ADDRESSES.MASTER_CHEF,
abi: syncSwapMasterChefAbi,
functionName: 'userInfo',
args: [BigInt(poolId), userAddr]
},
{
address: SYNC_SWAP_ADDRESSES.MASTER_CHEF,
abi: syncSwapMasterChefAbi,
functionName: 'pendingReward',
args: [BigInt(poolId), userAddr]
}
],
allowFailure: false // Fail if any call fails
});
// Process results - Direct access since allowFailure is false
// If any call failed, multicall would have thrown an error above.
/* // Remove incorrect status check
if (multicallResult[0].status !== 'success' || multicallResult[1].status !== 'success') {
const userInfoError = multicallResult[0].status === 'failure' ? multicallResult[0].error?.message : 'N/A';
const pendingRewardError = multicallResult[1].status === 'failure' ? multicallResult[1].error?.message : 'N/A';
throw new Error(`Failed to fetch staking data. UserInfo Error: ${userInfoError}, PendingReward Error: ${pendingRewardError}`);
}
*/
// Extract results directly
const userInfoResult = multicallResult[0]; // Explicit tuple type [amount, rewardDebt]
const pendingRewardResult = multicallResult[1];
const stakedAmountRaw = userInfoResult[0];
const pendingRewardsRaw = pendingRewardResult;
// Format results
const stakedAmountFormatted = formatUnits(stakedAmountRaw, lpDecimals);
// TODO: Need reward token decimals for accurate formatting!
// For now, assume reward token also has 18 decimals (common but not guaranteed)
const rewardTokenDecimals = 18;
const pendingRewardsFormatted = formatUnits(pendingRewardsRaw, rewardTokenDecimals);
return {
status: 'success',
poolId: poolId,
lpTokenAddress: lpTokenAddress ?? 'Unknown',
lpTokenSymbol: lpSymbol,
stakedAmount: stakedAmountFormatted,
stakedAmountRaw: stakedAmountRaw.toString(),
pendingRewards: pendingRewardsFormatted,
pendingRewardsRaw: pendingRewardsRaw.toString(),
// Note: APY/APR is usually calculated off-chain or via separate contracts/APIs
apy: 'Not available via MasterChef contract.'
};
}
catch (error) {
console.error('[getYieldInfo Error]:', error);
return { status: 'error', message: `Failed to get yield info: ${error instanceof Error ? error.message : 'Unknown error'}` };
}
}