EDUCHAIN Agent Kit

  • src
import { ethers } from 'ethers'; import * as blockchain from './blockchain.js'; import * as subgraph from './subgraph.js'; import * as routes from './routes.js'; // import { Percent } from '@uniswap/sdk-core'; // SailFish V3 contract addresses const CONTRACTS = { SwapRouter: '0x1a1e967e523435CeF20642e3D7811F7d0da9a704', Quoter: '0x14b4D9238550dc75Cf164FDa471Aa1d8A6A2b0c6', QuoterV2: '0x83EE12582E3448Ab69E664A2ba69b6AedE112205', UniswapV3Factory: '0x963A7f4eB46967A9fd3dFbabD354fC294FA2BF5C', WETH9: '0xd02E8c38a8E3db71f8b2ae30B8186d7874934e12', // Wrapped EDU address }; // Common token addresses (can be expanded) const TOKENS = { WETH: '0xd02E8c38a8E3db71f8b2ae30B8186d7874934e12', // Wrapped EDU }; // ABIs const SWAP_ROUTER_ABI = [ { type: "constructor", inputs: [ { name: "_factory", type: "address", internalType: "address" }, { name: "_WETH9", type: "address", internalType: "address" }, { name: "initCodeHash", type: "bytes32", internalType: "bytes32" }, ], stateMutability: "nonpayable", }, { type: "receive", stateMutability: "payable" }, { type: "function", name: "WETH9", inputs: [], outputs: [{ name: "", type: "address", internalType: "address" }], stateMutability: "view", }, { type: "function", name: "exactInput", inputs: [ { name: "params", type: "tuple", internalType: "struct ISwapRouter.ExactInputParams", components: [ { name: "path", type: "bytes", internalType: "bytes" }, { name: "recipient", type: "address", internalType: "address", }, { name: "deadline", type: "uint256", internalType: "uint256", }, { name: "amountIn", type: "uint256", internalType: "uint256", }, { name: "amountOutMinimum", type: "uint256", internalType: "uint256", }, ], }, ], outputs: [{ name: "amountOut", type: "uint256", internalType: "uint256" }], stateMutability: "payable", }, { type: "function", name: "exactInputSingle", inputs: [ { name: "params", type: "tuple", internalType: "struct ISwapRouter.ExactInputSingleParams", components: [ { name: "tokenIn", type: "address", internalType: "address" }, { name: "tokenOut", type: "address", internalType: "address", }, { name: "fee", type: "uint24", internalType: "uint24" }, { name: "recipient", type: "address", internalType: "address", }, { name: "deadline", type: "uint256", internalType: "uint256", }, { name: "amountIn", type: "uint256", internalType: "uint256", }, { name: "amountOutMinimum", type: "uint256", internalType: "uint256", }, { name: "sqrtPriceLimitX96", type: "uint160", internalType: "uint160", }, ], }, ], outputs: [{ name: "amountOut", type: "uint256", internalType: "uint256" }], stateMutability: "payable", }, { type: "function", name: "exactOutput", inputs: [ { name: "params", type: "tuple", internalType: "struct ISwapRouter.ExactOutputParams", components: [ { name: "path", type: "bytes", internalType: "bytes" }, { name: "recipient", type: "address", internalType: "address", }, { name: "deadline", type: "uint256", internalType: "uint256", }, { name: "amountOut", type: "uint256", internalType: "uint256", }, { name: "amountInMaximum", type: "uint256", internalType: "uint256", }, ], }, ], outputs: [{ name: "amountIn", type: "uint256", internalType: "uint256" }], stateMutability: "payable", }, { type: "function", name: "exactOutputSingle", inputs: [ { name: "params", type: "tuple", internalType: "struct ISwapRouter.ExactOutputSingleParams", components: [ { name: "tokenIn", type: "address", internalType: "address" }, { name: "tokenOut", type: "address", internalType: "address", }, { name: "fee", type: "uint24", internalType: "uint24" }, { name: "recipient", type: "address", internalType: "address", }, { name: "deadline", type: "uint256", internalType: "uint256", }, { name: "amountOut", type: "uint256", internalType: "uint256", }, { name: "amountInMaximum", type: "uint256", internalType: "uint256", }, { name: "sqrtPriceLimitX96", type: "uint160", internalType: "uint160", }, ], }, ], outputs: [{ name: "amountIn", type: "uint256", internalType: "uint256" }], stateMutability: "payable", }, { type: "function", name: "factory", inputs: [], outputs: [{ name: "", type: "address", internalType: "address" }], stateMutability: "view", }, { type: "function", name: "multicall", inputs: [{ name: "data", type: "bytes[]", internalType: "bytes[]" }], outputs: [{ name: "results", type: "bytes[]", internalType: "bytes[]" }], stateMutability: "payable", }, { type: "function", name: "refundETH", inputs: [], outputs: [], stateMutability: "payable", }, { type: "function", name: "selfPermit", inputs: [ { name: "token", type: "address", internalType: "address" }, { name: "value", type: "uint256", internalType: "uint256" }, { name: "deadline", type: "uint256", internalType: "uint256" }, { name: "v", type: "uint8", internalType: "uint8" }, { name: "r", type: "bytes32", internalType: "bytes32" }, { name: "s", type: "bytes32", internalType: "bytes32" }, ], outputs: [], stateMutability: "payable", }, { type: "function", name: "selfPermitAllowed", inputs: [ { name: "token", type: "address", internalType: "address" }, { name: "nonce", type: "uint256", internalType: "uint256" }, { name: "expiry", type: "uint256", internalType: "uint256" }, { name: "v", type: "uint8", internalType: "uint8" }, { name: "r", type: "bytes32", internalType: "bytes32" }, { name: "s", type: "bytes32", internalType: "bytes32" }, ], outputs: [], stateMutability: "payable", }, { type: "function", name: "selfPermitAllowedIfNecessary", inputs: [ { name: "token", type: "address", internalType: "address" }, { name: "nonce", type: "uint256", internalType: "uint256" }, { name: "expiry", type: "uint256", internalType: "uint256" }, { name: "v", type: "uint8", internalType: "uint8" }, { name: "r", type: "bytes32", internalType: "bytes32" }, { name: "s", type: "bytes32", internalType: "bytes32" }, ], outputs: [], stateMutability: "payable", }, { type: "function", name: "selfPermitIfNecessary", inputs: [ { name: "token", type: "address", internalType: "address" }, { name: "value", type: "uint256", internalType: "uint256" }, { name: "deadline", type: "uint256", internalType: "uint256" }, { name: "v", type: "uint8", internalType: "uint8" }, { name: "r", type: "bytes32", internalType: "bytes32" }, { name: "s", type: "bytes32", internalType: "bytes32" }, ], outputs: [], stateMutability: "payable", }, { type: "function", name: "sweepToken", inputs: [ { name: "token", type: "address", internalType: "address" }, { name: "amountMinimum", type: "uint256", internalType: "uint256", }, { name: "recipient", type: "address", internalType: "address" }, ], outputs: [], stateMutability: "payable", }, { type: "function", name: "sweepTokenWithFee", inputs: [ { name: "token", type: "address", internalType: "address" }, { name: "amountMinimum", type: "uint256", internalType: "uint256", }, { name: "recipient", type: "address", internalType: "address" }, { name: "feeBips", type: "uint256", internalType: "uint256" }, { name: "feeRecipient", type: "address", internalType: "address" }, ], outputs: [], stateMutability: "payable", }, { type: "function", name: "uniswapV3SwapCallback", inputs: [ { name: "amount0Delta", type: "int256", internalType: "int256" }, { name: "amount1Delta", type: "int256", internalType: "int256" }, { name: "_data", type: "bytes", internalType: "bytes" }, ], outputs: [], stateMutability: "nonpayable", }, { type: "function", name: "unwrapWETH9", inputs: [ { name: "amountMinimum", type: "uint256", internalType: "uint256", }, { name: "recipient", type: "address", internalType: "address" }, ], outputs: [], stateMutability: "payable", }, { type: "function", name: "unwrapWETH9WithFee", inputs: [ { name: "amountMinimum", type: "uint256", internalType: "uint256", }, { name: "recipient", type: "address", internalType: "address" }, { name: "feeBips", type: "uint256", internalType: "uint256" }, { name: "feeRecipient", type: "address", internalType: "address" }, ], outputs: [], stateMutability: "payable", }, ]; const ERC20_ABI = [ 'function approve(address spender, uint256 amount) external returns (bool)', 'function allowance(address owner, address spender) external view returns (uint256)', 'function balanceOf(address owner) view returns (uint256)', 'function decimals() view returns (uint8)', 'function symbol() view returns (string)', 'function name() view returns (string)', ]; const QUOTER_ABI = [ { type: "constructor", inputs: [ { name: "_factory", type: "address", internalType: "address" }, { name: "_WETH9", type: "address", internalType: "address" }, { name: "initCodeHash", type: "bytes32", internalType: "bytes32" }, ], stateMutability: "nonpayable", }, { type: "function", name: "WETH9", inputs: [], outputs: [{ name: "", type: "address", internalType: "address" }], stateMutability: "view", }, { type: "function", name: "factory", inputs: [], outputs: [{ name: "", type: "address", internalType: "address" }], stateMutability: "view", }, { type: "function", name: "quoteExactInput", inputs: [ { name: "path", type: "bytes", internalType: "bytes" }, { name: "amountIn", type: "uint256", internalType: "uint256" }, ], outputs: [ { name: "amountOut", type: "uint256", internalType: "uint256" }, { name: "sqrtPriceX96AfterList", type: "uint160[]", internalType: "uint160[]", }, { name: "initializedTicksCrossedList", type: "uint32[]", internalType: "uint32[]", }, { name: "gasEstimate", type: "uint256", internalType: "uint256" }, ], stateMutability: "nonpayable", }, { type: "function", name: "quoteExactInputSingle", inputs: [ { name: "params", type: "tuple", internalType: "struct IQuoterV2.QuoteExactInputSingleParams", components: [ { name: "tokenIn", type: "address", internalType: "address" }, { name: "tokenOut", type: "address", internalType: "address", }, { name: "amountIn", type: "uint256", internalType: "uint256", }, { name: "fee", type: "uint24", internalType: "uint24" }, { name: "sqrtPriceLimitX96", type: "uint160", internalType: "uint160", }, ], }, ], outputs: [ { name: "amountOut", type: "uint256", internalType: "uint256" }, { name: "sqrtPriceX96After", type: "uint160", internalType: "uint160", }, { name: "initializedTicksCrossed", type: "uint32", internalType: "uint32", }, { name: "gasEstimate", type: "uint256", internalType: "uint256" }, ], stateMutability: "nonpayable", }, { type: "function", name: "quoteExactOutput", inputs: [ { name: "path", type: "bytes", internalType: "bytes" }, { name: "amountOut", type: "uint256", internalType: "uint256" }, ], outputs: [ { name: "amountIn", type: "uint256", internalType: "uint256" }, { name: "sqrtPriceX96AfterList", type: "uint160[]", internalType: "uint160[]", }, { name: "initializedTicksCrossedList", type: "uint32[]", internalType: "uint32[]", }, { name: "gasEstimate", type: "uint256", internalType: "uint256" }, ], stateMutability: "nonpayable", }, { type: "function", name: "quoteExactOutputSingle", inputs: [ { name: "params", type: "tuple", internalType: "struct IQuoterV2.QuoteExactOutputSingleParams", components: [ { name: "tokenIn", type: "address", internalType: "address" }, { name: "tokenOut", type: "address", internalType: "address", }, { name: "amount", type: "uint256", internalType: "uint256" }, { name: "fee", type: "uint24", internalType: "uint24" }, { name: "sqrtPriceLimitX96", type: "uint160", internalType: "uint160", }, ], }, ], outputs: [ { name: "amountIn", type: "uint256", internalType: "uint256" }, { name: "sqrtPriceX96After", type: "uint160", internalType: "uint160", }, { name: "initializedTicksCrossed", type: "uint32", internalType: "uint32", }, { name: "gasEstimate", type: "uint256", internalType: "uint256" }, ], stateMutability: "nonpayable", }, { type: "function", name: "uniswapV3SwapCallback", inputs: [ { name: "amount0Delta", type: "int256", internalType: "int256" }, { name: "amount1Delta", type: "int256", internalType: "int256" }, { name: "path", type: "bytes", internalType: "bytes" }, ], outputs: [], stateMutability: "view", }, ]; // Fee tiers const FEE_TIERS = { LOWEST: 100, // 0.01% LOW: 500, // 0.05% MEDIUM: 3000, // 0.3% HIGH: 10000 // 1% }; // Helper function to encode path for multi-hop swaps function encodePath(path: string[], fees: number[]): string { if (path.length !== fees.length + 1) { throw new Error('Path and fees length mismatch'); } let encoded = '0x'; for (let i = 0; i < fees.length; i++) { encoded += path[i].slice(2); encoded += fees[i].toString(16).padStart(6, '0'); } encoded += path[path.length - 1].slice(2); return encoded; } // Find the best route for a token pair export async function findBestRoute( tokenA: string, tokenB: string ): Promise<routes.RouteInfo> { try { // Get the best route from the routes module const bestRoute = await routes.getBestRoute(tokenA, tokenB); if (!bestRoute) { throw new Error(`No route found for token pair ${tokenA}/${tokenB}`); } // Remove console.log to prevent interference with JSON parsing // console.log(`Found ${bestRoute.type} route for ${tokenA}/${tokenB} with fee: ${bestRoute.totalFee * 100}%`); return bestRoute; } catch (error) { console.error('Error finding best route:', error); throw error; } } // Calculate price impact percentage export function calculatePriceImpact( amountIn: string, amountOut: string, midPrice: string, tokenInDecimals: number, tokenOutDecimals: number ): number { const amountInDecimal = parseFloat(amountIn); const amountOutDecimal = parseFloat(amountOut); const midPriceDecimal = parseFloat(midPrice); // Calculate expected output based on mid price const expectedOutput = amountInDecimal * midPriceDecimal; // Calculate price impact const impact = (expectedOutput - amountOutDecimal) / expectedOutput * 100; return Math.max(0, impact); // Ensure we don't return negative values } // Apply slippage to an amount export function applySlippage( amount: string, slippagePercentage: number, increase: boolean = false ): string { const amountBigInt = ethers.getBigInt(amount); const slippageFactor = 1000 - (slippagePercentage * 10); // Convert percentage to basis points if (increase) { // Increase amount by slippage (for maximum input) return ((amountBigInt * BigInt(1000)) / BigInt(slippageFactor)).toString(); } else { // Decrease amount by slippage (for minimum output) return ((amountBigInt * BigInt(slippageFactor)) / BigInt(1000)).toString(); } } // Get quote for token swap export async function getSwapQuote( tokenIn: string, tokenOut: string, amountIn: string, slippagePercentage: number = 0.5 ): Promise<{ amountOut: string; formattedAmountOut: string; minimumAmountOut: string; formattedMinimumAmountOut: string; tokenInSymbol: string; tokenOutSymbol: string; tokenInDecimals: number; tokenOutDecimals: number; route: routes.RouteInfo; priceImpact: number; midPrice: string; }> { try { // Find the best route for the token pair const route = await findBestRoute(tokenIn, tokenOut); const provider = blockchain.getProvider(); // Get token details const tokenInContract = new ethers.Contract(tokenIn, ERC20_ABI, provider); const tokenOutContract = new ethers.Contract(tokenOut, ERC20_ABI, provider); const [tokenInDecimals, tokenOutDecimals, tokenInSymbol, tokenOutSymbol] = await Promise.all([ tokenInContract.decimals(), tokenOutContract.decimals(), tokenInContract.symbol(), tokenOutContract.symbol(), ]); // Convert amount to token units const amountInWei = ethers.parseUnits(amountIn, tokenInDecimals); // Get quote based on route type let amountOutWei: bigint; let midPrice: string; if (route.type === 'direct') { // Direct route (single hop) const pool = route.path[0]; const fee = parseInt(pool.feeTier); // Get quote using the QuoterV2 contract const quoterContract = new ethers.Contract(CONTRACTS.QuoterV2, QUOTER_ABI, provider); const quoteParams = { tokenIn, tokenOut, amountIn: amountInWei, fee, sqrtPriceLimitX96: 0 // No price limit }; // Use a static call to get the quote without sending a transaction const quoterInterface = new ethers.Interface(QUOTER_ABI); const calldata = quoterInterface.encodeFunctionData('quoteExactInputSingle', [quoteParams]); const result = await provider.call({ to: CONTRACTS.QuoterV2, data: calldata, }); const decodedResult = quoterInterface.decodeFunctionResult('quoteExactInputSingle', result); amountOutWei = decodedResult[0]; // Get the first return value (amountOut) // Get mid price from the pool if (pool.token0.address.toLowerCase() === tokenIn.toLowerCase()) { midPrice = pool.token1Price || '0'; } else { midPrice = pool.token0Price || '0'; } } else { // Indirect route (multi-hop) // For simplicity, we'll use the direct route approach for each hop and multiply the results // In a production environment, you would use the exactInput function with a path // First hop const pool1 = route.path[0]; const fee1 = parseInt(pool1.feeTier); const intermediaryToken = route.intermediaryToken!; // Second hop const pool2 = route.path[1]; const fee2 = parseInt(pool2.feeTier); // Get quote for first hop const quoterContract = new ethers.Contract(CONTRACTS.QuoterV2, QUOTER_ABI, provider); // First hop quote const quoteParams1 = { tokenIn, tokenOut: intermediaryToken.address, amountIn: amountInWei, fee: fee1, sqrtPriceLimitX96: 0 }; const quoterInterface = new ethers.Interface(QUOTER_ABI); const calldata1 = quoterInterface.encodeFunctionData('quoteExactInputSingle', [quoteParams1]); const result1 = await provider.call({ to: CONTRACTS.QuoterV2, data: calldata1, }); const decodedResult1 = quoterInterface.decodeFunctionResult('quoteExactInputSingle', result1); const intermediateAmountWei = decodedResult1[0]; // Second hop quote const quoteParams2 = { tokenIn: intermediaryToken.address, tokenOut, amountIn: intermediateAmountWei, fee: fee2, sqrtPriceLimitX96: 0 }; const calldata2 = quoterInterface.encodeFunctionData('quoteExactInputSingle', [quoteParams2]); const result2 = await provider.call({ to: CONTRACTS.QuoterV2, data: calldata2, }); const decodedResult2 = quoterInterface.decodeFunctionResult('quoteExactInputSingle', result2); amountOutWei = decodedResult2[0]; // Calculate mid price for multi-hop (approximate) const midPrice1 = pool1.token0.address.toLowerCase() === tokenIn.toLowerCase() ? pool1.token1Price || '0' : pool1.token0Price || '0'; const midPrice2 = pool2.token0.address.toLowerCase() === intermediaryToken.address.toLowerCase() ? pool2.token1Price || '0' : pool2.token0Price || '0'; midPrice = (parseFloat(midPrice1) * parseFloat(midPrice2)).toString(); } // Format amount out const formattedAmountOut = ethers.formatUnits(amountOutWei, tokenOutDecimals); // Calculate minimum amount out with slippage const minimumAmountOut = applySlippage(amountOutWei.toString(), slippagePercentage); const formattedMinimumAmountOut = ethers.formatUnits(minimumAmountOut, tokenOutDecimals); // Calculate price impact const priceImpact = calculatePriceImpact( amountIn, formattedAmountOut, midPrice, tokenInDecimals, tokenOutDecimals ); return { amountOut: amountOutWei.toString(), formattedAmountOut, minimumAmountOut, formattedMinimumAmountOut, tokenInSymbol, tokenOutSymbol, tokenInDecimals, tokenOutDecimals, route, priceImpact, midPrice }; } catch (error) { console.error('Error getting swap quote:', error); throw error; } } // Swap token to token export async function swapExactTokensForTokens( privateKey: string, tokenIn: string, tokenOut: string, amountIn: string, slippagePercentage: number = 0.5 // Default 0.5% slippage ): Promise<{ hash: string; from: string; amountIn: string; amountOut: string; tokenIn: string; tokenOut: string; tokenInSymbol: string; tokenOutSymbol: string; route: routes.RouteInfo; }> { try { const provider = blockchain.getProvider(); const wallet = new ethers.Wallet(privateKey, provider); const fromAddress = wallet.address; // Get quote and route information const quote = await getSwapQuote(tokenIn, tokenOut, amountIn, slippagePercentage); const route = quote.route; // Convert amount to token units const amountInWei = ethers.parseUnits(amountIn, quote.tokenInDecimals); const minAmountOut = ethers.getBigInt(quote.minimumAmountOut); // Approve router to spend tokens const tokenContract = new ethers.Contract(tokenIn, ERC20_ABI, wallet); const approveTx = await tokenContract.approve(CONTRACTS.SwapRouter, amountInWei); await approveTx.wait(); // Create swap router contract const swapRouter = new ethers.Contract(CONTRACTS.SwapRouter, SWAP_ROUTER_ABI, wallet); // Execute swap based on route type const deadline = Math.floor(Date.now() / 1000) + 60 * 20; // 20 minutes from now let tx; if (route.type === 'direct') { // Direct route (single hop) const fee = parseInt(route.path[0].feeTier); const swapParams = { tokenIn, tokenOut, fee, recipient: fromAddress, deadline, amountIn: amountInWei, amountOutMinimum: minAmountOut, sqrtPriceLimitX96: 0 // No price limit }; tx = await swapRouter.exactInputSingle(swapParams); } else { // Indirect route (multi-hop) const intermediaryToken = route.intermediaryToken!; // Encode the path for multi-hop swap const path = ethers.solidityPacked( ['address', 'uint24', 'address', 'uint24', 'address'], [ tokenIn, parseInt(route.path[0].feeTier), intermediaryToken.address, parseInt(route.path[1].feeTier), tokenOut ] ); const swapParams = { path, recipient: fromAddress, deadline, amountIn: amountInWei, amountOutMinimum: minAmountOut }; tx = await swapRouter.exactInput(swapParams); } const receipt = await tx.wait(); if (!receipt) { throw new Error('Transaction failed'); } return { hash: tx.hash, from: fromAddress, amountIn, amountOut: quote.formattedMinimumAmountOut, tokenIn, tokenOut, tokenInSymbol: quote.tokenInSymbol, tokenOutSymbol: quote.tokenOutSymbol, route }; } catch (error) { console.error('Error swapping tokens:', error); throw error; } } // Swap EDU to token export async function swapExactEDUForTokens( privateKey: string, tokenOut: string, amountIn: string, slippagePercentage: number = 0.5 // Default 0.5% slippage ): Promise<{ hash: string; from: string; amountIn: string; amountOut: string; tokenOut: string; tokenOutSymbol: string; route: routes.RouteInfo; }> { try { const provider = blockchain.getProvider(); const wallet = new ethers.Wallet(privateKey, provider); const fromAddress = wallet.address; // Get quote and route information const quote = await getSwapQuote(CONTRACTS.WETH9, tokenOut, amountIn, slippagePercentage); const route = quote.route; // Convert EDU amount to wei const amountInWei = ethers.parseEther(amountIn); const minAmountOut = ethers.getBigInt(quote.minimumAmountOut); // Create swap router contract const swapRouter = new ethers.Contract(CONTRACTS.SwapRouter, SWAP_ROUTER_ABI, wallet); // Execute swap based on route type const deadline = Math.floor(Date.now() / 1000) + 60 * 20; // 20 minutes from now let tx; try { // Transaction details are logged to server console only, not mixed with JSON response if (route.type === 'direct') { // Direct route (single hop) const fee = parseInt(route.path[0].feeTier); const swapParams = { tokenIn: CONTRACTS.WETH9, tokenOut, fee, recipient: fromAddress, deadline, amountIn: amountInWei, amountOutMinimum: minAmountOut, sqrtPriceLimitX96: 0 // No price limit }; // Check if the value is reasonable (not excessively large) if (amountInWei > ethers.parseEther('1000000')) { throw new Error(`Amount too large: ${ethers.formatEther(amountInWei)} EDU`); } // Use a gas limit to prevent excessive gas usage tx = await swapRouter.exactInputSingle(swapParams, { value: amountInWei, gasLimit: 500000 // Set a reasonable gas limit }); } else { // Indirect route (multi-hop) const intermediaryToken = route.intermediaryToken!; // Encode the path for multi-hop swap const path = ethers.solidityPacked( ['address', 'uint24', 'address', 'uint24', 'address'], [ CONTRACTS.WETH9, parseInt(route.path[0].feeTier), intermediaryToken.address, parseInt(route.path[1].feeTier), tokenOut ] ); const swapParams = { path, recipient: fromAddress, deadline, amountIn: amountInWei, amountOutMinimum: minAmountOut }; // Check if the value is reasonable (not excessively large) if (amountInWei > ethers.parseEther('1000000')) { throw new Error(`Amount too large: ${ethers.formatEther(amountInWei)} EDU`); } // Use a gas limit to prevent excessive gas usage tx = await swapRouter.exactInput(swapParams, { value: amountInWei, gasLimit: 500000 // Set a reasonable gas limit }); } } catch (error: any) { console.error('Error executing swap transaction:', error); // Provide more detailed error information if (error.message && error.message.includes('insufficient funds')) { throw new Error(`Insufficient funds for transaction. Make sure you have enough EDU for the swap amount (${amountIn} EDU) plus gas fees.`); } throw error; } const receipt = await tx.wait(); if (!receipt) { throw new Error('Transaction failed'); } return { hash: tx.hash, from: fromAddress, amountIn, amountOut: quote.formattedMinimumAmountOut, tokenOut, tokenOutSymbol: quote.tokenOutSymbol, route }; } catch (error) { console.error('Error swapping EDU for tokens:', error); throw error; } } // Wrap EDU to WEDU export async function wrapEDU( privateKey: string, amount: string ): Promise<{ hash: string; from: string; amount: string; success: boolean; }> { try { const provider = blockchain.getProvider(); const wallet = new ethers.Wallet(privateKey, provider); const fromAddress = wallet.address; // Convert EDU amount to wei const amountInWei = ethers.parseEther(amount); // Create WETH9 contract instance const wethAbi = [ 'function deposit() external payable', 'function withdraw(uint256 amount) external', 'function balanceOf(address owner) view returns (uint256)', 'function approve(address spender, uint256 amount) external returns (bool)' ]; const wethContract = new ethers.Contract(CONTRACTS.WETH9, wethAbi, wallet); // Deposit EDU to get WEDU const tx = await wethContract.deposit({ value: amountInWei }); const receipt = await tx.wait(); if (!receipt) { throw new Error('Transaction failed'); } return { hash: tx.hash, from: fromAddress, amount, success: true }; } catch (error) { console.error('Error wrapping EDU to WEDU:', error); throw error; } } // Unwrap WEDU to EDU export async function unwrapWEDU( privateKey: string, amount: string ): Promise<{ hash: string; from: string; amount: string; success: boolean; }> { try { const provider = blockchain.getProvider(); const wallet = new ethers.Wallet(privateKey, provider); const fromAddress = wallet.address; // Convert WEDU amount to wei const amountInWei = ethers.parseEther(amount); // Create WETH9 contract instance const wethAbi = [ 'function deposit() external payable', 'function withdraw(uint256 amount) external', 'function balanceOf(address owner) view returns (uint256)', 'function approve(address spender, uint256 amount) external returns (bool)' ]; const wethContract = new ethers.Contract(CONTRACTS.WETH9, wethAbi, wallet); // Check balance const balance = await wethContract.balanceOf(fromAddress); if (balance < amountInWei) { throw new Error(`Insufficient WEDU balance. You have ${ethers.formatEther(balance)} WEDU, but tried to unwrap ${amount} WEDU.`); } // Withdraw WEDU to get EDU const tx = await wethContract.withdraw(amountInWei); const receipt = await tx.wait(); if (!receipt) { throw new Error('Transaction failed'); } return { hash: tx.hash, from: fromAddress, amount, success: true }; } catch (error) { console.error('Error unwrapping WEDU to EDU:', error); throw error; } } // Swap token to EDU export async function swapExactTokensForEDU( privateKey: string, tokenIn: string, amountIn: string, slippagePercentage: number = 0.5, // Default 0.5% slippage fee?: number ): Promise<{ hash: string; from: string; amountIn: string; amountOut: string; tokenIn: string; tokenInSymbol: string; }> { try { const provider = blockchain.getProvider(); const wallet = new ethers.Wallet(privateKey, provider); const fromAddress = wallet.address; // If fee is not provided, find the best route const route = await findBestRoute(tokenIn, CONTRACTS.WETH9); const fee = parseInt(route.path[0].feeTier); // Get token details const tokenInContract = new ethers.Contract(tokenIn, ERC20_ABI, provider); const tokenInDecimals = await tokenInContract.decimals(); const tokenInSymbol = await tokenInContract.symbol(); // Convert amount to token units const amountInWei = ethers.parseUnits(amountIn, tokenInDecimals); // Approve router to spend tokens const approveTx = await tokenInContract.approve(CONTRACTS.SwapRouter, amountInWei); await approveTx.wait(); // Get quote for token to WETH (since we'll be using WETH internally) const quoterContract = new ethers.Contract(CONTRACTS.QuoterV2, QUOTER_ABI, provider); const quoteParams = { tokenIn, tokenOut: CONTRACTS.WETH9, amountIn: amountInWei, fee, sqrtPriceLimitX96: 0 // No price limit }; // Use a static call to get the quote without sending a transaction const quoterInterface = new ethers.Interface(QUOTER_ABI); const calldata = quoterInterface.encodeFunctionData('quoteExactInputSingle', [quoteParams]); const result = await provider.call({ to: CONTRACTS.QuoterV2, data: calldata, }); const decodedResult = quoterInterface.decodeFunctionResult('quoteExactInputSingle', result); const amountOutWei = decodedResult[0]; // Get the first return value (amountOut) // Calculate minimum amount out with slippage const slippageFactor = 1000 - (slippagePercentage * 10); // Convert percentage to basis points const minAmountOut = (amountOutWei * BigInt(slippageFactor)) / BigInt(1000); // Create swap router contract const swapRouter = new ethers.Contract(CONTRACTS.SwapRouter, SWAP_ROUTER_ABI, wallet); // Execute swap const deadline = Math.floor(Date.now() / 1000) + 60 * 20; // 20 minutes from now // We need to swap to WETH and then unwrap it to ETH const swapParams = { tokenIn, tokenOut: CONTRACTS.WETH9, fee, recipient: CONTRACTS.SwapRouter, // Send to router for unwrapping deadline, amountIn: amountInWei, amountOutMinimum: minAmountOut, sqrtPriceLimitX96: 0 // No price limit }; const tx = await swapRouter.exactInputSingle(swapParams); // Unwrap WETH to ETH and send to user await swapRouter.unwrapWETH9(minAmountOut, fromAddress); const receipt = await tx.wait(); if (!receipt) { throw new Error('Transaction failed'); } return { hash: tx.hash, from: fromAddress, amountIn, amountOut: ethers.formatEther(minAmountOut), tokenIn, tokenInSymbol }; } catch (error) { console.error('Error swapping tokens for EDU:', error); throw error; } }