Skip to main content
Glama
index.ts23.7 kB
import { type Address, encodeFunctionData, isAddress, parseUnits } from "viem"; import type { ToolRegistration } from "@/types/tools.js"; import { createTextResponse } from "@/types/tools.js"; import { getPublicClient } from "../../utils/evm/viem"; import { ERC20_TOKENS } from "../../utils/evm/supportedErc20Tokens"; import { quoteSchema, type QuoteSchema, swapSchema, type SwapSchema } from "./schema"; function isValidToken(token: string): boolean { // Check if it's FLOW (native token) if (token === "FLOW") return true; // Check if it matches any token's name, symbol, or contract address return Object.entries(ERC20_TOKENS).some( ([key, tokenInfo]) => key === token || tokenInfo.symbol === token || tokenInfo.name === token || tokenInfo.contractAddress === token, ); } function getTokenInfo(token: string) { // Special case for FLOW (native token) if (token === "FLOW") { return { symbol: "FLOW", name: "Flow", decimals: 18, }; } // Regular ERC20 token lookup for (const [key, info] of Object.entries(ERC20_TOKENS)) { if (key === token || info.symbol === token || info.name === token || info.contractAddress === token) { return info; } } return null; } // Punchswap V2 Factory ABI (minimal for getPair) const UniswapV2FactoryABI = [ { inputs: [ { name: "tokenA", type: "address" }, { name: "tokenB", type: "address" }, ], name: "getPair", outputs: [{ name: "pair", type: "address" }], stateMutability: "view", type: "function", }, ] as const; // Punchswap V2 Pair ABI (minimal for getReserves) const UniswapV2PairABI = [ { inputs: [], name: "getReserves", outputs: [ { name: "reserve0", type: "uint112" }, { name: "reserve1", type: "uint112" }, { name: "blockTimestampLast", type: "uint32" }, ], stateMutability: "view", type: "function", }, { inputs: [], name: "token0", outputs: [{ name: "", type: "address" }], stateMutability: "view", type: "function", }, { inputs: [], name: "token1", outputs: [{ name: "", type: "address" }], stateMutability: "view", type: "function", }, ] as const; // Punchswap V2 Factory address const PUNCHSWAP_V2_FACTORY_ADDRESS = "0x29372c22459a4e373851798bFd6808e71EA34A71" as Address; // ExecuteCall ABI for using flowEVMAccount to execute transactions const ExecuteCallABI = [ { inputs: [ { internalType: "address", name: "to", type: "address", }, { internalType: "uint256", name: "value", type: "uint256", }, { internalType: "bytes", name: "data", type: "bytes", }, ], name: "executeCall", outputs: [ { internalType: "bytes", name: "result", type: "bytes", }, ], stateMutability: "payable", type: "function", }, ]; // Add a helper function to ensure the value is properly formatted as a hex string function toHexString(value: bigint | number): `0x${string}` { return `0x${value.toString(16)}` as `0x${string}`; } /** * Formats a token amount according to its decimals * @param amount The raw amount as a bigint * @param decimals The number of decimals for the token * @returns A formatted string representation of the amount */ function formatTokenAmount(amount: bigint, decimals: number): string { if (amount === 0n) return "0"; const amountStr = amount.toString(); // If the amount is less than 10^decimals, we need to pad with leading zeros if (amountStr.length <= decimals) { const paddedAmount = amountStr.padStart(decimals + 1, "0"); const integerPart = paddedAmount.slice(0, -decimals) || "0"; const fractionalPart = paddedAmount.slice(-decimals).replace(/0+$/, ""); return fractionalPart ? `${integerPart}.${fractionalPart}` : integerPart; } else { const integerPart = amountStr.slice(0, amountStr.length - decimals); const fractionalPart = amountStr.slice(amountStr.length - decimals).replace(/0+$/, ""); return fractionalPart ? `${integerPart}.${fractionalPart}` : integerPart; } } /** * Calculate the amount out based on Punchswap V2 formula * @param amountIn The input amount * @param reserveIn The reserve of the input token * @param reserveOut The reserve of the output token * @returns The output amount */ function getAmountOut(amountIn: bigint, reserveIn: bigint, reserveOut: bigint): bigint { if (amountIn === 0n) return 0n; if (reserveIn === 0n || reserveOut === 0n) return 0n; // Punchswap V2 formula: amountOut = (amountIn * 997 * reserveOut) / (reserveIn * 1000 + amountIn * 997) const amountInWithFee = amountIn * 997n; const numerator = amountInWithFee * reserveOut; const denominator = reserveIn * 1000n + amountInWithFee; return numerator / denominator; } /** * Tool for getting price quotes from Punchswap V2 */ export const punchswapQuoteTool = { name: "punchswap_quote", description: ` Get a price quote from Punchswap V2 for swapping between two tokens. Supported tokens: Wrapped Flow(WFLOW), Trump(TRUMP), HotCocoa, Gwendolion, Pawderick, Catseye. You can specify tokens by name, symbol, or contract address. The tool will check if a liquidity pair exists and return the current exchange rate. NOTE: This is tool for flow EVM chain not flow blockchain. `, inputSchema: quoteSchema, handler: async (params: QuoteSchema) => { try { const { tokenIn, tokenOut, amountIn } = params; const publicClient = getPublicClient(); // Validate tokens using the new validation functions if (!isValidToken(tokenIn)) { return createTextResponse( `Invalid input token: ${tokenIn}. Supported tokens: FLOW, WFLOW, TRUMP, HotCocoa, Gwendolion, Pawderick, Catseye, USDF`, ); } if (!isValidToken(tokenOut)) { return createTextResponse( `Invalid output token: ${tokenOut}. Supported tokens: FLOW, WFLOW, TRUMP, HotCocoa, Gwendolion, Pawderick, Catseye, USDF`, ); } // Get token info using the new function const tokenInInfo = getTokenInfo(tokenIn); const tokenOutInfo = getTokenInfo(tokenOut); if (!tokenInInfo) { return createTextResponse(`Invalid input token: ${tokenIn}`); } if (!tokenOutInfo) { return createTextResponse(`Invalid output token: ${tokenOut}`); } // Convert amount to proper decimal representation using viem's parseUnits let amountInWei; try { amountInWei = parseUnits(amountIn, tokenInInfo.decimals); } catch (error) { return createTextResponse("Invalid amount"); } // Check if we're dealing with ETH (FLOW in this case) const isETHIn = tokenIn === "FLOW"; const isETHOut = tokenOut === "FLOW"; // For FLOW (native token), we need to use WFLOW for the pair lookup const WFLOW_ADDRESS = ERC20_TOKENS["Wrapped Flow"].contractAddress; // For pair lookup and path creation, we need to use the actual token addresses // For FLOW, we use WFLOW address const tokenInAddressForPair = isETHIn ? WFLOW_ADDRESS : ((tokenInInfo as any).contractAddress as Address); const tokenOutAddressForPair = isETHOut ? WFLOW_ADDRESS : ((tokenOutInfo as any).contractAddress as Address); // Get the pair address from the factory const pairAddress = (await publicClient.readContract({ address: PUNCHSWAP_V2_FACTORY_ADDRESS, abi: UniswapV2FactoryABI, functionName: "getPair", args: [tokenInAddressForPair, tokenOutAddressForPair], })) as Address; if (pairAddress === "0x0000000000000000000000000000000000000000") { return createTextResponse(`No liquidity pair found for ${tokenInInfo.symbol} and ${tokenOutInfo.symbol}`); } // Get token0 and token1 from the pair to determine the order const token0 = (await publicClient.readContract({ address: pairAddress, abi: UniswapV2PairABI, functionName: "token0", })) as Address; // Get reserves from the pair const [reserve0, reserve1] = (await publicClient.readContract({ address: pairAddress, abi: UniswapV2PairABI, functionName: "getReserves", })) as [bigint, bigint, number]; // Determine which reserve corresponds to which token const isToken0 = tokenInAddressForPair.toLowerCase() === token0.toLowerCase(); const reserveIn = isToken0 ? reserve0 : reserve1; const reserveOut = isToken0 ? reserve1 : reserve0; // Calculate the amount out using the Punchswap V2 formula const amountOut = getAmountOut(amountInWei, reserveIn, reserveOut); // Format the output amount according to decimals const formattedAmountOut = formatTokenAmount(amountOut, tokenOutInfo.decimals); // Calculate the exchange rate const exchangeRate = (Number(amountOut) / Number(amountInWei)) * 10 ** (tokenInInfo.decimals - tokenOutInfo.decimals); // Calculate price impact (simplified) const priceImpact = calculatePriceImpact(amountInWei, reserveIn); const result = { tokenIn: { symbol: tokenInInfo.symbol, name: tokenInInfo.name, address: (tokenInInfo as any).contractAddress || null, amount: amountIn, }, tokenOut: { symbol: tokenOutInfo.symbol, name: tokenOutInfo.name, address: (tokenOutInfo as any).contractAddress || null, amount: formattedAmountOut, }, exchangeRate: `1 ${tokenInInfo.symbol} = ${exchangeRate.toFixed(6)} ${tokenOutInfo.symbol}`, swapDetails: { pairAddress, reserveIn: reserveIn.toString(), reserveOut: reserveOut.toString(), priceImpact, }, }; return createTextResponse(JSON.stringify(result, null, 2)); } catch (error) { return createTextResponse( `Error getting Punchswap quote: ${error instanceof Error ? error.message : String(error)}`, ); } }, }; /** * Helper function to estimate price impact based on input amount and reserve * This is a simplified estimation for Punchswap V2 */ function calculatePriceImpact(amountIn: bigint, reserveIn: bigint): string { if (reserveIn === 0n) return "Unknown"; // Calculate price impact as a percentage of the reserve const impact = Number((amountIn * 10000n) / reserveIn) / 100; if (impact < 0.1) return "Very Low (<0.1%)"; if (impact < 0.5) return "Low (0.1-0.5%)"; if (impact < 1.0) return "Medium (0.5-1.0%)"; if (impact < 3.0) return "High (1.0-3.0%)"; return `Very High (${impact.toFixed(2)}%)`; } // Punchswap V2 Router ABI (minimal for swapExactTokensForTokens) const UniswapV2RouterABI = [ { inputs: [ { name: "amountIn", type: "uint256" }, { name: "amountOutMin", type: "uint256" }, { name: "path", type: "address[]" }, { name: "to", type: "address" }, { name: "deadline", type: "uint256" }, ], name: "swapExactTokensForTokens", outputs: [{ name: "amounts", type: "uint256[]" }], stateMutability: "nonpayable", type: "function", }, { inputs: [ { name: "amountIn", type: "uint256" }, { name: "amountOutMin", type: "uint256" }, { name: "path", type: "address[]" }, { name: "to", type: "address" }, { name: "deadline", type: "uint256" }, ], name: "swapExactTokensForETH", outputs: [{ name: "amounts", type: "uint256[]" }], stateMutability: "nonpayable", type: "function", }, { inputs: [ { name: "amountOutMin", type: "uint256" }, { name: "path", type: "address[]" }, { name: "to", type: "address" }, { name: "deadline", type: "uint256" }, ], name: "swapExactETHForTokens", outputs: [{ name: "amounts", type: "uint256[]" }], stateMutability: "payable", type: "function", }, ] as const; // ERC20 Approve ABI const ERC20ApproveABI = [ { inputs: [ { name: "spender", type: "address" }, { name: "amount", type: "uint256" }, ], name: "approve", outputs: [{ name: "", type: "bool" }], stateMutability: "nonpayable", type: "function", }, ] as const; // Punchswap V2 Router address const UNISWAP_V2_ROUTER_ADDRESS = "0xf45AFe28fd5519d5f8C1d4787a4D5f724C0eFa4d" as Address; // ERC20 Allowance ABI const ERC20AllowanceABI = [ { inputs: [ { name: "owner", type: "address" }, { name: "spender", type: "address" }, ], name: "allowance", outputs: [{ name: "", type: "uint256" }], stateMutability: "view", type: "function", }, ] as const; const MAX_ALLOWANCE = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffn; /** * Tool for creating a transaction to swap tokens on Punchswap V2 */ export const punchswapSwapTool: ToolRegistration<SwapSchema> = { name: "punchswap_swap", description: ` Create a transaction to swap tokens on Punchswap V2. Supported tokens: Wrapped Flow(WFLOW), Trump(TRUMP), HotCocoa, Gwendolion, Pawderick, Catseye. You can specify tokens by name, symbol, or contract address. The tool will check if a liquidity pair exists and create the appropriate transaction. If approval is needed, it will return the approval transaction first. NOTE: This is tool for flow EVM chain not flow blockchain. `, inputSchema: swapSchema, handler: async (params: SwapSchema) => { try { const { flowEVMAccount, tokenIn, tokenOut, amountIn } = params; const deadline = params.deadline || Math.floor(Date.now() / 1000) + 60 * 20; // 20 minutes from now const slippageTolerance = params.slippageTolerance || 10; // Default to 10% // Validate slippage tolerance if (slippageTolerance < 0.1 || slippageTolerance > 50) { return createTextResponse(`Invalid slippage tolerance: ${slippageTolerance}. Must be between 0.1% and 50%.`); } const publicClient = getPublicClient(); // Validate address if (!isAddress(flowEVMAccount)) { return createTextResponse( `Invalid Flow EVM account address: ${flowEVMAccount}. Must be a valid 0x format address.`, ); } // Validate tokens using the new validation functions if (!isValidToken(tokenIn)) { return createTextResponse( `Invalid input token: ${tokenIn}. Supported tokens: FLOW, WFLOW, TRUMP, HotCocoa, Gwendolion, Pawderick, Catseye, USDF`, ); } if (!isValidToken(tokenOut)) { return createTextResponse( `Invalid output token: ${tokenOut}. Supported tokens: FLOW, WFLOW, TRUMP, HotCocoa, Gwendolion, Pawderick, Catseye, USDF`, ); } // Get token info using the new function const tokenInInfo = getTokenInfo(tokenIn); const tokenOutInfo = getTokenInfo(tokenOut); if (!tokenInInfo) { return createTextResponse(`Invalid input token: ${tokenIn}`); } if (!tokenOutInfo) { return createTextResponse(`Invalid output token: ${tokenOut}`); } // Convert amountIn to wei using viem's parseUnits let amountInWei; try { amountInWei = parseUnits(amountIn, tokenInInfo.decimals); } catch (error) { return createTextResponse("Invalid amount"); } // Check if we're dealing with ETH (FLOW in this case) const isETHIn = tokenIn === "FLOW"; const isETHOut = tokenOut === "FLOW"; // For FLOW (native token), we need to use WFLOW for the pair lookup const WFLOW_ADDRESS = ERC20_TOKENS["Wrapped Flow"].contractAddress; // For pair lookup and path creation, we need to use the actual token addresses // For FLOW, we use WFLOW address const tokenInAddressForPair = isETHIn ? WFLOW_ADDRESS : ((tokenInInfo as any).contractAddress as Address); const tokenOutAddressForPair = isETHOut ? WFLOW_ADDRESS : ((tokenOutInfo as any).contractAddress as Address); // Get the pair address from the factory const pairAddress = (await publicClient.readContract({ address: PUNCHSWAP_V2_FACTORY_ADDRESS, abi: UniswapV2FactoryABI, functionName: "getPair", args: [tokenInAddressForPair, tokenOutAddressForPair], })) as Address; if (pairAddress === "0x0000000000000000000000000000000000000000") { return createTextResponse(`No liquidity pair found for ${tokenInInfo.symbol} and ${tokenOutInfo.symbol}`); } // Get token0 and token1 from the pair to determine the order const token0 = (await publicClient.readContract({ address: pairAddress, abi: UniswapV2PairABI, functionName: "token0", })) as Address; // Get reserves from the pair const [reserve0, reserve1] = (await publicClient.readContract({ address: pairAddress, abi: UniswapV2PairABI, functionName: "getReserves", })) as [bigint, bigint, number]; // Determine which reserve corresponds to which token const isToken0 = tokenInAddressForPair.toLowerCase() === token0.toLowerCase(); const reserveIn = isToken0 ? reserve0 : reserve1; const reserveOut = isToken0 ? reserve1 : reserve0; // Calculate the amount out using the Punchswap V2 formula const amountOut = getAmountOut(amountInWei, reserveIn, reserveOut); const formattedAmountOut = formatTokenAmount(amountOut, tokenOutInfo.decimals); // Apply slippage tolerance to get minimum amount out const slippageFactor = 1000n - BigInt(Math.floor(slippageTolerance * 10)); const amountOutMin = (amountOut * slippageFactor) / 1000n; // Create the path array for the swap // For FLOW, we use WFLOW in the path const path = [tokenInAddressForPair, tokenOutAddressForPair]; // If we're swapping ETH for tokens, we only need one transaction if (isETHIn) { // Swap ETH for Tokens const swapData = encodeFunctionData({ abi: UniswapV2RouterABI, functionName: "swapExactETHForTokens", args: [amountOutMin, path, flowEVMAccount as `0x${string}`, BigInt(deadline)], }); // Create executeCall data to execute the swap through flowEVMAccount // const executeCallData = encodeFunctionData({ // abi: ExecuteCallABI, // functionName: 'executeCall', // args: [UNISWAP_V2_ROUTER_ADDRESS, amountInWei.toString(), swapData] // }); // const transactionRequest = { // to: flowEVMAccount as Address, // data: executeCallData, // value: '0' // }; const transactionRequest = { to: UNISWAP_V2_ROUTER_ADDRESS, data: swapData, value: "0", }; const response = { transactionRequest: transactionRequest, tokenInInfos: { functionName: "swapExactETHForTokens", in: tokenInInfo, out: tokenOutInfo, amountIn: amountIn, amountOut: formattedAmountOut, amountInWei: amountInWei.toString(), amountOutMin: amountOutMin.toString(), path: path, deadline: deadline, isETHIn: isETHIn, isETHOut: isETHOut, }, type: "punchswap_swap", }; return createTextResponse(JSON.stringify(response)); } // For token to token or token to ETH swaps, we need to check allowance first try { const currentAllowance = (await publicClient.readContract({ address: tokenInAddressForPair, abi: ERC20AllowanceABI, functionName: "allowance", args: [flowEVMAccount as `0x${string}`, UNISWAP_V2_ROUTER_ADDRESS], })) as bigint; // If allowance is insufficient, return an approval transaction if (currentAllowance < amountInWei) { const approveData = encodeFunctionData({ abi: ERC20ApproveABI, functionName: "approve", args: [UNISWAP_V2_ROUTER_ADDRESS, MAX_ALLOWANCE], }); const approveTransaction = { to: tokenInAddressForPair as Address, data: approveData, value: "0", }; const response = { transactionRequest: approveTransaction, tokenInInfos: { functionName: "approve", in: tokenInInfo, out: tokenOutInfo, amountIn: amountIn, amountOut: formattedAmountOut, amountInWei: amountInWei.toString(), amountOutMin: amountOutMin.toString(), path: path, deadline: deadline, isETHIn: isETHIn, isETHOut: isETHOut, }, type: "punchswap_swap", }; return createTextResponse(JSON.stringify(response)); } } catch (error) { return createTextResponse( `Error checking token allowance: ${error instanceof Error ? error.message : String(error)}`, ); } // If we reach here, it means we have sufficient allowance // Create the swap transaction based on whether we're swapping to ETH or tokens if (isETHOut) { // Swap Tokens for ETH const swapData = encodeFunctionData({ abi: UniswapV2RouterABI, functionName: "swapExactTokensForETH", args: [amountInWei, amountOutMin, path, flowEVMAccount as `0x${string}`, BigInt(deadline)], }); const swapTransaction = { to: UNISWAP_V2_ROUTER_ADDRESS, data: swapData, value: "0", }; const response = { transactionRequest: swapTransaction, tokenInInfos: { functionName: "swapExactTokensForETH", in: tokenInInfo, out: tokenOutInfo, amountIn: amountIn, amountOut: formattedAmountOut, amountInWei: amountInWei.toString(), amountOutMin: amountOutMin.toString(), path: path, deadline: deadline, isETHIn: isETHIn, isETHOut: isETHOut, }, type: "punchswap_swap", }; return createTextResponse(JSON.stringify(response)); } else { // Swap Tokens for Tokens const swapData = encodeFunctionData({ abi: UniswapV2RouterABI, functionName: "swapExactTokensForTokens", args: [amountInWei, amountOutMin, path, flowEVMAccount as `0x${string}`, BigInt(deadline)], }); const swapTransaction = { to: UNISWAP_V2_ROUTER_ADDRESS as Address, data: swapData, value: "0", }; const response = { transactionRequest: swapTransaction, tokenInInfos: { functionName: "swapExactTokensForTokens", in: tokenInInfo, out: tokenOutInfo, amountIn: amountIn, amountOut: formattedAmountOut, amountInWei: amountInWei.toString(), amountOutMin: amountOutMin.toString(), path: path, deadline: deadline, isETHIn: isETHIn, isETHOut: isETHOut, }, type: "punchswap_swap", }; return createTextResponse(JSON.stringify(response)); } } catch (error) { return createTextResponse( `Error creating swap transaction: ${error instanceof Error ? error.message : String(error)}`, ); } }, };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Outblock/flow-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server