Skip to main content
Glama
dragonswap.ts47.4 kB
import { type Address, formatUnits, parseUnits, encodePacked, getContract } from 'viem'; import { getPublicClient, getWalletClient } from './clients.js'; import { readContract, writeContract } from './contracts.js'; import * as services from './index.js'; import { getPrivateKeyAsHex } from '../config.js'; import { DEFAULT_NETWORK } from '../chains.js'; import { dragonSwapQuoterV2Abi } from './abi/dragonSwapQuoterV2.js'; import { dragonSwapPoolAbi } from './abi/dragonSwapPool.js'; import { dragonSwapRouterAbi } from './abi/dragonSwapRouter.js'; import { dragonSwapPositionManagerAbi } from './abi/dragonSwapPositionManager.js'; import { dragonSwapFactoryAbi } from './abi/dragonSwapFactory.js'; import { erc20Abi } from './abi/erc20.js'; import { COMMON_TOKENS, DRAGONSWAP_ADDRESSES } from './utils.js'; // Common fee tiers for DragonSwap export const FEE_TIERS = { LOWEST: 100, // 0.01% LOW: 500, // 0.05% MEDIUM: 3000, // 0.3% HIGH: 10000 // 1% } as const; /** * Check if an address is a valid contract */ async function isContract(address: Address, network = DEFAULT_NETWORK): Promise<boolean> { try { const client = getPublicClient(network); const bytecode = await client.getBytecode({ address }); return bytecode !== undefined && bytecode !== '0x'; } catch { return false; } } /** * Create a path for V3 swaps (includes fee tier) */ function encodePath(tokenA: Address, tokenB: Address, fee: number): `0x${string}` { return encodePacked( ['address', 'uint24', 'address'], [tokenA, fee, tokenB] ); } /** * Convert sqrtPriceX96 to human-readable price */ function sqrtPriceX96ToPrice(sqrtPriceX96: bigint, decimals0: number, decimals1: number): string { const price = (Number(sqrtPriceX96) / (2 ** 96)) ** 2; const adjustedPrice = price * (10 ** decimals0) / (10 ** decimals1); return adjustedPrice.toString(); } /** * Get token price from a DragonSwap V3 pool */ export async function getTokenPrice( poolAddress: string, tokenAddress: string, network = DEFAULT_NETWORK ): Promise<{ price: string; sqrtPriceX96: string; tick: number; liquidity: string; tokens: { token0: Address; token1: Address; }; fee: number; }> { try { const validatedPoolAddress = services.helpers.validateAddress(poolAddress); const validatedTokenAddress = services.helpers.validateAddress(tokenAddress); console.log(`Fetching token price for ${tokenAddress} from pool ${poolAddress} on network ${network}`); // Check if the pool address is a valid contract const isValidContract = await isContract(validatedPoolAddress, network); if (!isValidContract) { throw new Error(`Address ${poolAddress} is not a valid contract on network ${network}`); } // Get pool information const [slot0, token0, token1, fee, liquidity] = await Promise.all([ readContract({ address: validatedPoolAddress, abi: dragonSwapPoolAbi, functionName: 'slot0' }, network), readContract({ address: validatedPoolAddress, abi: dragonSwapPoolAbi, functionName: 'token0' }, network), readContract({ address: validatedPoolAddress, abi: dragonSwapPoolAbi, functionName: 'token1' }, network), readContract({ address: validatedPoolAddress, abi: dragonSwapPoolAbi, functionName: 'fee' }, network), readContract({ address: validatedPoolAddress, abi: dragonSwapPoolAbi, functionName: 'liquidity' }, network) ]); const [sqrtPriceX96, tick] = slot0 as [bigint, number, number, number, number, number, boolean]; const token0Address = token0 as Address; const token1Address = token1 as Address; const feeAmount = fee as number; const liquidityAmount = liquidity as bigint; // Validate that the requested token is actually in this pool const isToken0 = validatedTokenAddress.toLowerCase() === token0Address.toLowerCase(); const isToken1 = validatedTokenAddress.toLowerCase() === token1Address.toLowerCase(); if (!isToken0 && !isToken1) { throw new Error(`Token ${tokenAddress} is not part of the pool ${poolAddress}. Pool contains: ${token0Address} and ${token1Address}`); } // Check for liquidity if (liquidityAmount === 0n) { throw new Error(`Pool ${poolAddress} has no liquidity`); } // Get actual token decimals const token0Contract = getContract({ address: token0Address, abi: erc20Abi, client: getPublicClient(network), }); const decimalsToken0 = await token0Contract.read.decimals(); const token1Contract = getContract({ address: token1Address, abi: erc20Abi, client: getPublicClient(network), }); const decimalsToken1 = await token1Contract.read.decimals(); // Calculate price (assuming 18 decimals for both tokens for simplicity) const price = sqrtPriceX96ToPrice(sqrtPriceX96, decimalsToken0, decimalsToken1); // If requesting token0 price in terms of token1, use price as is // If requesting token1 price in terms of token0, invert the price const finalPrice = isToken0 ? price : (1 / parseFloat(price)).toString(); return { price: finalPrice, sqrtPriceX96: sqrtPriceX96.toString(), tick, liquidity: liquidityAmount.toString(), tokens: { token0: token0Address, token1: token1Address }, fee: feeAmount }; } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get token price: ${error.message}`); } throw new Error(`Failed to get token price: ${String(error)}`); } } /** * Get swap quote from DragonSwap V3 QuoterV2 */ export async function getSwapQuote( amountIn: string, tokenIn: string, tokenOut: string, fee: number = FEE_TIERS.MEDIUM, network = DEFAULT_NETWORK ): Promise<{ amountOut: string; sqrtPriceX96After: string; initializedTicksCrossed: number; gasEstimate: string; path: string; }> { try { const validatedTokenIn = services.helpers.validateAddress(tokenIn); const validatedTokenOut = services.helpers.validateAddress(tokenOut); console.log(`Getting swap quote from ${tokenIn} to ${tokenOut} with fee ${fee}`); // Validate amount input const amountInNumber = parseFloat(amountIn); if (isNaN(amountInNumber) || amountInNumber <= 0) { throw new Error(`Invalid amount: ${amountIn}`); } // Get actual input token decimals const tokenInContract = getContract({ address: validatedTokenIn, abi: erc20Abi, client: getPublicClient(network), }); const decimalsIn = await tokenInContract.read.decimals(); const amountInWei = parseUnits(amountIn, decimalsIn); // Get actual output token decimals const tokenOutContract = getContract({ address: validatedTokenOut, abi: erc20Abi, client: getPublicClient(network), }); const decimalsOut = await tokenOutContract.read.decimals(); // const amountInWei = parseUnits(amountIn, 18); // Assuming 18 decimals // Use quoteExactInputSingle for single-hop swaps const quoteParams = { tokenIn: validatedTokenIn, tokenOut: validatedTokenOut, amountIn: amountInWei, fee: fee, sqrtPriceLimitX96: 0n // No price limit }; const [amountOut, sqrtPriceX96After, initializedTicksCrossed, gasEstimate] = await readContract({ address: DRAGONSWAP_ADDRESSES.quoter as Address, abi: dragonSwapQuoterV2Abi, functionName: 'quoteExactInputSingle', args: [quoteParams] }, network) as [bigint, bigint, number, bigint]; const path = encodePath(validatedTokenIn, validatedTokenOut, fee); return { amountOut: formatUnits(amountOut, decimalsOut), sqrtPriceX96After: sqrtPriceX96After.toString(), initializedTicksCrossed, gasEstimate: gasEstimate.toString(), path }; } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get swap quote: ${error.message}`); } throw new Error(`Failed to get swap quote: ${String(error)}`); } } /** * Get multi-hop swap quote using encoded path */ export async function getMultiHopSwapQuote( amountIn: string, path: `0x${string}`, network = DEFAULT_NETWORK ): Promise<{ amountOut: string; sqrtPriceX96AfterList: string[]; initializedTicksCrossedList: number[]; gasEstimate: string; }> { try { const amountInWei = parseUnits(amountIn, 18); const [amountOut, sqrtPriceX96AfterList, initializedTicksCrossedList, gasEstimate] = await readContract({ address: DRAGONSWAP_ADDRESSES.quoter as Address, abi: dragonSwapQuoterV2Abi, functionName: 'quoteExactInput', args: [path, amountInWei] }, network) as [bigint, bigint[], number[], bigint]; return { amountOut: formatUnits(amountOut, 18), sqrtPriceX96AfterList: sqrtPriceX96AfterList.map(x => x.toString()), initializedTicksCrossedList, gasEstimate: gasEstimate.toString() }; } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get multi-hop swap quote: ${error.message}`); } throw new Error(`Failed to get multi-hop swap quote: ${String(error)}`); } } export async function wrapSei( amount: string, network = DEFAULT_NETWORK ): Promise<`0x${string}`> { try { const privateKey = getPrivateKeyAsHex(); if (!privateKey) { throw new Error('Private key not available. Set the PRIVATE_KEY environment variable and restart the MCP server.'); } const amountWei = parseUnits(amount, 18); console.log(`Wrapping ${amount} SEI to WSEI`); const walletClient = getWalletClient(privateKey, network); const hash = await walletClient.writeContract({ address: DRAGONSWAP_ADDRESSES.swapRouter as Address, abi: dragonSwapRouterAbi, functionName: 'wrapSEI', args: [amountWei], value: amountWei, account: walletClient.account!, chain: walletClient.chain }); return hash; } catch (error) { if (error instanceof Error) { throw new Error(`Failed to wrap SEI: ${error.message}`); } throw new Error(`Failed to wrap SEI: ${String(error)}`); } } export async function unwrapSei( amount: string, network = DEFAULT_NETWORK ): Promise<`0x${string}`> { try { const privateKey = getPrivateKeyAsHex(); if (!privateKey) { throw new Error('Private key not available. Set the PRIVATE_KEY environment variable and restart the MCP server.'); } const amountWei = parseUnits(amount, 18); console.log(`Unwrapping ${amount} WSEI to SEI`); const walletClient = getWalletClient(privateKey, network); const hash = await walletClient.writeContract({ address: DRAGONSWAP_ADDRESSES.swapRouter as Address, abi: dragonSwapRouterAbi, functionName: 'unwrapWSEI', args: [amountWei], account: walletClient.account!, chain: walletClient.chain }); return hash; } catch (error) { if (error instanceof Error) { throw new Error(`Failed to unwrap WSEI: ${error.message}`); } throw new Error(`Failed to unwrap WSEI: ${String(error)}`); } } /** * Execute a token swap on DragonSwap */ export async function executeSwap( tokenIn: string, tokenOut: string, amountIn: string, amountOutMinimum: string, recipient: string, fee: number = FEE_TIERS.MEDIUM, deadline?: number, network = DEFAULT_NETWORK ): Promise<`0x${string}`> { try { const validatedTokenIn = services.helpers.validateAddress(tokenIn); const validatedTokenOut = services.helpers.validateAddress(tokenOut); const validatedRecipient = services.helpers.validateAddress(recipient); // Get private key from environment const privateKey = getPrivateKeyAsHex(); if (!privateKey) { throw new Error('Private key not available. Set the PRIVATE_KEY environment variable and restart the MCP server.'); } const amountInWei = parseUnits(amountIn, 18); const amountOutMinWei = parseUnits(amountOutMinimum, 18); const swapDeadline = deadline || Math.floor(Date.now() / 1000) + 300; // 5 minutes from now console.log(`Executing swap: ${amountIn} ${tokenIn} -> ${tokenOut} (min: ${amountOutMinimum})`); // Create wallet client for sending the transaction const walletClient = getWalletClient(privateKey, network); // Create swap parameters const swapParams = { tokenIn: validatedTokenIn, tokenOut: validatedTokenOut, fee: fee, recipient: validatedRecipient, deadline: BigInt(swapDeadline), amountIn: amountInWei, amountOutMinimum: amountOutMinWei, sqrtPriceLimitX96: 0n // No price limit }; // Execute the swap and return the transaction hash const hash = await walletClient.writeContract({ address: DRAGONSWAP_ADDRESSES.swapRouter as Address, abi: dragonSwapRouterAbi, functionName: 'exactInputSingle', args: [swapParams], account: walletClient.account!, chain: walletClient.chain }); return hash; } catch (error) { if (error instanceof Error) { throw new Error(`Failed to execute swap: ${error.message}`); } throw new Error(`Failed to execute swap: ${String(error)}`); } } /** * Execute a multi-hop swap using encoded path */ export async function executeMultiHopSwap( path: `0x${string}`, amountIn: string, amountOutMinimum: string, recipient: string, deadline?: number, network = DEFAULT_NETWORK ): Promise<`0x${string}`> { try { const validatedRecipient = services.helpers.validateAddress(recipient); // Get private key from environment const privateKey = getPrivateKeyAsHex(); if (!privateKey) { throw new Error('Private key not available. Set the PRIVATE_KEY environment variable and restart the MCP server.'); } const amountInWei = parseUnits(amountIn, 18); const amountOutMinWei = parseUnits(amountOutMinimum, 18); const swapDeadline = deadline || Math.floor(Date.now() / 1000) + 300; console.log(`Executing multi-hop swap: ${amountIn} tokens through path ${path} (min: ${amountOutMinimum})`); const swapParams = { path: path, recipient: validatedRecipient, deadline: BigInt(swapDeadline), amountIn: amountInWei, amountOutMinimum: amountOutMinWei }; // Create wallet client for sending the transaction const walletClient = getWalletClient(privateKey, network); // Execute the swap with wallet client const hash = await walletClient.writeContract({ address: DRAGONSWAP_ADDRESSES.swapRouter as Address, abi: dragonSwapRouterAbi, functionName: 'exactInput', args: [swapParams], account: walletClient.account!, chain: walletClient.chain }); return hash; } catch (error) { if (error instanceof Error) { throw new Error(`Failed to execute multi-hop swap: ${error.message}`); } throw new Error(`Failed to execute multi-hop swap: ${String(error)}`); } } /** * Add liquidity to a DragonSwap V3 pool */ export async function addLiquidity( token0: string, token1: string, fee: number, tickLower: number, tickUpper: number, amount0Desired: string, amount1Desired: string, amount0Min: string, amount1Min: string, recipient: string, deadline?: number, network = DEFAULT_NETWORK ): Promise<{ hash: string; amount0: string; amount1: string; }> { try { const validatedToken0 = services.helpers.validateAddress(token0); const validatedToken1 = services.helpers.validateAddress(token1); const validatedRecipient = services.helpers.validateAddress(recipient); // Get private key from environment const privateKey = getPrivateKeyAsHex(); if (!privateKey) { throw new Error('Private key not available. Set the PRIVATE_KEY environment variable and restart the MCP server.'); } const amount0DesiredWei = parseUnits(amount0Desired, 18); const amount1DesiredWei = parseUnits(amount1Desired, 18); const amount0MinWei = parseUnits(amount0Min, 18); const amount1MinWei = parseUnits(amount1Min, 18); const liquidityDeadline = deadline || Math.floor(Date.now() / 1000) + 300; console.log(`Adding liquidity: ${amount0Desired} ${token0} + ${amount1Desired} ${token1}`); const mintParams = { token0: validatedToken0, token1: validatedToken1, fee: fee, tickLower: tickLower, tickUpper: tickUpper, amount0Desired: amount0DesiredWei, amount1Desired: amount1DesiredWei, amount0Min: amount0MinWei, amount1Min: amount1MinWei, recipient: validatedRecipient, deadline: BigInt(liquidityDeadline) }; // Create wallet client for sending the transaction const walletClient = getWalletClient(privateKey, network); // Execute the transaction with wallet client const hash = await walletClient.writeContract({ address: DRAGONSWAP_ADDRESSES.positionManager as Address, abi: dragonSwapPositionManagerAbi, functionName: 'mint', args: [mintParams], account: walletClient.account!, chain: walletClient.chain }); // Parse the result from logs or return values return { hash: hash, amount0: formatUnits(amount0DesiredWei, 18), amount1: formatUnits(amount1DesiredWei, 18) }; } catch (error) { if (error instanceof Error) { throw new Error(`Failed to add liquidity: ${error.message}`); } throw new Error(`Failed to add liquidity: ${String(error)}`); } } /** * Remove liquidity from a DragonSwap V3 position */ export async function removeLiquidity( tokenId: string, liquidity: string, amount0Min: string, amount1Min: string, deadline?: number, network = DEFAULT_NETWORK ): Promise<`0x${string}`> { try { // Get private key from environment const privateKey = getPrivateKeyAsHex(); if (!privateKey) { throw new Error('Private key not available. Set the PRIVATE_KEY environment variable and restart the MCP server.'); } const liquidityAmount = parseUnits(liquidity, 0); // Liquidity is not scaled const amount0MinWei = parseUnits(amount0Min, 18); const amount1MinWei = parseUnits(amount1Min, 18); const liquidityDeadline = deadline || Math.floor(Date.now() / 1000) + 300; console.log(`Removing liquidity: ${liquidity} from position ${tokenId}`); const decreaseParams = { tokenId: BigInt(tokenId), liquidity: liquidityAmount, amount0Min: amount0MinWei, amount1Min: amount1MinWei, deadline: BigInt(liquidityDeadline) }; // Create wallet client for sending the transaction const walletClient = getWalletClient(privateKey, network); // Execute the transaction with wallet client const hash = await walletClient.writeContract({ address: DRAGONSWAP_ADDRESSES.positionManager as Address, abi: dragonSwapPositionManagerAbi, functionName: 'decreaseLiquidity', args: [decreaseParams], account: walletClient.account!, chain: walletClient.chain }); return hash; } catch (error) { if (error instanceof Error) { throw new Error(`Failed to remove liquidity: ${error.message}`); } throw new Error(`Failed to remove liquidity: ${String(error)}`); } } /** * Collect fees from a DragonSwap V3 position */ export async function collectFees( tokenId: string, recipient: string, amount0Max?: string, amount1Max?: string, network = DEFAULT_NETWORK ): Promise<`0x${string}`> { try { const validatedRecipient = services.helpers.validateAddress(recipient); // Get private key from environment const privateKey = getPrivateKeyAsHex(); if (!privateKey) { throw new Error('Private key not available. Set the PRIVATE_KEY environment variable and restart the MCP server.'); } const amount0MaxWei = amount0Max ? parseUnits(amount0Max, 18) : parseUnits('999999999', 18); const amount1MaxWei = amount1Max ? parseUnits(amount1Max, 18) : parseUnits('999999999', 18); console.log(`Collecting fees from position ${tokenId} to ${recipient}`); const collectParams = { tokenId: BigInt(tokenId), recipient: validatedRecipient, amount0Max: amount0MaxWei, amount1Max: amount1MaxWei }; // Create wallet client for sending the transaction const walletClient = getWalletClient(privateKey, network); // Execute the transaction with wallet client const hash = await walletClient.writeContract({ address: DRAGONSWAP_ADDRESSES.positionManager as Address, abi: dragonSwapPositionManagerAbi, functionName: 'collect', args: [collectParams], account: walletClient.account!, chain: walletClient.chain }); return hash; } catch (error) { if (error instanceof Error) { throw new Error(`Failed to collect fees: ${error.message}`); } throw new Error(`Failed to collect fees: ${String(error)}`); } } /** * Get position information */ export async function getPositionInfo( tokenId: string, network = DEFAULT_NETWORK ): Promise<{ nonce: string; operator: Address; token0: Address; token1: Address; fee: number; tickLower: number; tickUpper: number; liquidity: string; feeGrowthInside0LastX128: string; feeGrowthInside1LastX128: string; tokensOwed0: string; tokensOwed1: string; }> { try { const position = await readContract({ address: DRAGONSWAP_ADDRESSES.positionManager as Address, abi: dragonSwapPositionManagerAbi, functionName: 'positions', args: [BigInt(tokenId)] }, network); const [ nonce, operator, token0, token1, fee, tickLower, tickUpper, liquidity, feeGrowthInside0LastX128, feeGrowthInside1LastX128, tokensOwed0, tokensOwed1 ] = position as [bigint, Address, Address, Address, number, number, number, bigint, bigint, bigint, bigint, bigint]; return { nonce: nonce.toString(), operator, token0, token1, fee, tickLower, tickUpper, liquidity: liquidity.toString(), feeGrowthInside0LastX128: feeGrowthInside0LastX128.toString(), feeGrowthInside1LastX128: feeGrowthInside1LastX128.toString(), tokensOwed0: tokensOwed0.toString(), tokensOwed1: tokensOwed1.toString() }; } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get position info: ${error.message}`); } throw new Error(`Failed to get position info: ${String(error)}`); } } /** * Get or create a pool */ export async function getOrCreatePool( tokenA: string, tokenB: string, fee: number, network = DEFAULT_NETWORK ): Promise<{ poolAddress: Address; isNewPool: boolean; }> { try { const validatedTokenA = services.helpers.validateAddress(tokenA); const validatedTokenB = services.helpers.validateAddress(tokenB); // Try to get existing pool const existingPool = await readContract({ address: DRAGONSWAP_ADDRESSES.factory as Address, abi: dragonSwapFactoryAbi, functionName: 'getPool', args: [validatedTokenA, validatedTokenB, fee] }, network) as Address; if (existingPool && existingPool !== '0x0000000000000000000000000000000000000000') { return { poolAddress: existingPool, isNewPool: false }; } // Create new pool const result = await writeContract({ address: DRAGONSWAP_ADDRESSES.factory as Address, abi: dragonSwapFactoryAbi, functionName: 'createPool', args: [validatedTokenA, validatedTokenB, fee] }, network); return { poolAddress: result as Address, isNewPool: true }; } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get or create pool: ${error.message}`); } throw new Error(`Failed to get or create pool: ${String(error)}`); } } /** * Get pool address for token pair and fee tier */ export async function getPoolAddress( tokenA: string, tokenB: string, fee: number, network = DEFAULT_NETWORK ): Promise<Address> { const validatedTokenA = services.helpers.validateAddress(tokenA); const validatedTokenB = services.helpers.validateAddress(tokenB); const poolAddress = await readContract({ address: DRAGONSWAP_ADDRESSES.factory as Address, abi: dragonSwapFactoryAbi, functionName: 'getPool', args: [validatedTokenA, validatedTokenB, fee] }, network) as Address; if (!poolAddress || poolAddress === '0x0000000000000000000000000000000000000000') { throw new Error('Pool does not exist'); } return poolAddress; } /** * Create new pool for token pair and fee tier */ export async function createPool( tokenA: string, tokenB: string, fee: number, network = DEFAULT_NETWORK ): Promise<Address> { try { const validatedTokenA = services.helpers.validateAddress(tokenA); const validatedTokenB = services.helpers.validateAddress(tokenB); // Get private key from environment const privateKey = getPrivateKeyAsHex(); if (!privateKey) { throw new Error('Private key not available. Set the PRIVATE_KEY environment variable and restart the MCP server.'); } console.log(`Creating pool for ${tokenA}/${tokenB} with fee tier ${fee}`); // Create wallet client for sending the transaction const walletClient = getWalletClient(privateKey, network); // Execute the transaction with wallet client const hash = await walletClient.writeContract({ address: DRAGONSWAP_ADDRESSES.factory as Address, abi: dragonSwapFactoryAbi, functionName: 'createPool', args: [validatedTokenA, validatedTokenB, fee], account: walletClient.account!, chain: walletClient.chain }); // Note: The function returns a transaction hash, not the pool address // To get the pool address, you'd need to parse the transaction receipt or call getPool() return hash as Address; } catch (error) { if (error instanceof Error) { throw new Error(`Failed to create pool: ${error.message}`); } throw new Error(`Failed to create pool: ${String(error)}`); } } /** * Get comprehensive pool information */ export async function getPoolInfo( poolAddress: string, network = DEFAULT_NETWORK ): Promise<{ address: Address; token0: Address; token1: Address; fee: number; tickSpacing: number; liquidity: string; sqrtPriceX96: string; tick: number; observationIndex: number; observationCardinality: number; observationCardinalityNext: number; feeProtocol: number; unlocked: boolean; token0Info?: { symbol: string; decimals: number; balance: string; }; token1Info?: { symbol: string; decimals: number; balance: string; }; }> { try { const validatedPoolAddress = services.helpers.validateAddress(poolAddress); // Check if pool exists const isValidContract = await isContract(validatedPoolAddress, network); if (!isValidContract) { throw new Error(`Pool address ${poolAddress} is not a valid contract`); } // Get all pool data const [slot0, token0, token1, fee, liquidity] = await Promise.all([ readContract({ address: validatedPoolAddress, abi: dragonSwapPoolAbi, functionName: 'slot0' }, network), readContract({ address: validatedPoolAddress, abi: dragonSwapPoolAbi, functionName: 'token0' }, network), readContract({ address: validatedPoolAddress, abi: dragonSwapPoolAbi, functionName: 'token1' }, network), readContract({ address: validatedPoolAddress, abi: dragonSwapPoolAbi, functionName: 'fee' }, network), readContract({ address: validatedPoolAddress, abi: dragonSwapPoolAbi, functionName: 'liquidity' }, network) ]); const [ sqrtPriceX96, tick, observationIndex, observationCardinality, observationCardinalityNext, feeProtocol, unlocked ] = slot0 as [bigint, number, number, number, number, number, boolean]; const token0Address = token0 as Address; const token1Address = token1 as Address; const feeAmount = fee as number; const liquidityAmount = liquidity as bigint; // Get tick spacing based on fee tier const tickSpacing = getTickSpacing(feeAmount); const poolInfo = { address: validatedPoolAddress, token0: token0Address, token1: token1Address, fee: feeAmount, tickSpacing, liquidity: liquidityAmount.toString(), sqrtPriceX96: sqrtPriceX96.toString(), tick, observationIndex, observationCardinality, observationCardinalityNext, feeProtocol, unlocked }; return poolInfo; } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get pool info: ${error.message}`); } throw new Error(`Failed to get pool info: ${String(error)}`); } } /** * Check token allowance */ export async function checkTokenAllowance( tokenAddress: string, owner: string, spender: string, network = DEFAULT_NETWORK ): Promise<{ allowance: string; hasEnoughAllowance: boolean; tokenInfo: { address: Address; decimals: number; }; }> { try { const validatedToken = services.helpers.validateAddress(tokenAddress); const validatedOwner = services.helpers.validateAddress(owner); const validatedSpender = services.helpers.validateAddress(spender); const [allowance, decimals] = await Promise.all([ readContract({ address: validatedToken, abi: erc20Abi, functionName: 'allowance', args: [validatedOwner, validatedSpender] }, network), readContract({ address: validatedToken, abi: erc20Abi, functionName: 'decimals' }, network) ]); const allowanceAmount = allowance as bigint; const tokenDecimals = decimals as number; return { allowance: formatUnits(allowanceAmount, tokenDecimals), hasEnoughAllowance: allowanceAmount > 0n, tokenInfo: { address: validatedToken, decimals: tokenDecimals } }; } catch (error) { if (error instanceof Error) { throw new Error(`Failed to check token allowance: ${error.message}`); } throw new Error(`Failed to check token allowance: ${String(error)}`); } } /** * Get token balance */ export async function getTokenBalance( tokenAddress: string, account: string, network = DEFAULT_NETWORK ): Promise<{ balance: string; balanceWei: string; tokenInfo: { address: Address; decimals: number; }; }> { try { const validatedToken = services.helpers.validateAddress(tokenAddress); const validatedAccount = services.helpers.validateAddress(account); const [balance, decimals] = await Promise.all([ readContract({ address: validatedToken, abi: erc20Abi, functionName: 'balanceOf', args: [validatedAccount] }, network), readContract({ address: validatedToken, abi: erc20Abi, functionName: 'decimals' }, network) ]); const balanceAmount = balance as bigint; const tokenDecimals = decimals as number; return { balance: formatUnits(balanceAmount, tokenDecimals), balanceWei: balanceAmount.toString(), tokenInfo: { address: validatedToken, decimals: tokenDecimals } }; } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get token balance: ${error.message}`); } throw new Error(`Failed to get token balance: ${String(error)}`); } } /** * Calculate optimal tick range for liquidity provision */ export function calculateTickRange( currentTick: number, fee: number, rangeWidth: number = 20 ): { tickLower: number; tickUpper: number; tickSpacing: number; } { const tickSpacing = getTickSpacing(fee); const tickRadius = Math.floor(rangeWidth / 2); // Calculate ticks and ensure they're aligned to tick spacing const tickLower = Math.floor((currentTick - tickRadius) / tickSpacing) * tickSpacing; const tickUpper = Math.ceil((currentTick + tickRadius) / tickSpacing) * tickSpacing; return { tickLower, tickUpper, tickSpacing }; } /** * Calculate minimum amounts with slippage protection */ export function calculateSlippage( amount: string, slippageTolerance: number = 0.5 // 0.5% default ): { originalAmount: string; minAmount: string; maxAmount: string; slippagePercent: number; } { const originalAmount = parseFloat(amount); const slippageMultiplier = slippageTolerance / 100; const minAmount = originalAmount * (1 - slippageMultiplier); const maxAmount = originalAmount * (1 + slippageMultiplier); return { originalAmount: amount, minAmount: minAmount.toString(), maxAmount: maxAmount.toString(), slippagePercent: slippageTolerance }; } /** * Create encoded path for multi-hop swaps */ export function createMultiHopPath( tokens: Address[], fees: number[] ): `0x${string}` { if (tokens.length < 2) { throw new Error('At least 2 tokens required for a path'); } if (tokens.length - 1 !== fees.length) { throw new Error('Number of fees must be one less than number of tokens'); } let path = tokens[0]; for (let i = 0; i < fees.length; i++) { // Encode fee as 3-byte hex const feeHex = fees[i].toString(16).padStart(6, '0'); path += feeHex + tokens[i + 1].slice(2); // Remove '0x' prefix from subsequent tokens } return path as `0x${string}`; } /** * Convert tick to price */ export function tickToPrice( tick: number, decimals0: number = 18, decimals1: number = 18 ): string { const price = Math.pow(1.0001, tick); const adjustedPrice = price * Math.pow(10, decimals0 - decimals1); return adjustedPrice.toString(); } /** * Convert price to tick */ export function priceToTick( price: string, decimals0: number = 18, decimals1: number = 18 ): number { const priceNum = parseFloat(price); const adjustedPrice = priceNum * Math.pow(10, decimals1 - decimals0); const tick = Math.log(adjustedPrice) / Math.log(1.0001); return Math.round(tick); } /** * Sort tokens for V3 compatibility (token0 < token1) */ export function sortTokens( tokenA: Address, tokenB: Address ): { token0: Address; token1: Address; isReversed: boolean; } { const isReversed = tokenA.toLowerCase() > tokenB.toLowerCase(); return { token0: isReversed ? tokenB : tokenA, token1: isReversed ? tokenA : tokenB, isReversed }; } /** * Get tick spacing for fee tiers */ export function getTickSpacing(fee: number): number { switch (fee) { case FEE_TIERS.LOWEST: // 0.01% return 1; case FEE_TIERS.LOW: // 0.05% return 10; case FEE_TIERS.MEDIUM: // 0.3% return 60; case FEE_TIERS.HIGH: // 1% return 200; default: throw new Error(`Unknown fee tier: ${fee}`); } } /** * Batch multiple operations in one transaction using multicall */ export async function batchOperations( operations: { functionName: string; args: any[]; }[], deadline?: number, network = DEFAULT_NETWORK ): Promise<{ hash: string; results: string[]; }> { try { const batchDeadline = deadline || Math.floor(Date.now() / 1000) + 300; // Encode each operation const encodedCalls: `0x${string}`[] = operations.map(op => { // This is a simplified encoding - in practice, you'd use proper ABI encoding return encodePacked(['string'], [JSON.stringify(op)]); }); const result = await writeContract({ address: DRAGONSWAP_ADDRESSES.swapRouter as Address, abi: dragonSwapRouterAbi, functionName: 'multicall', args: [BigInt(batchDeadline), encodedCalls] }, network); return { hash: result as string, results: [] // Results would be decoded from transaction receipt }; } catch (error) { if (error instanceof Error) { throw new Error(`Failed to batch operations: ${error.message}`); } throw new Error(`Failed to batch operations: ${String(error)}`); } } /** * Get optimal swap route for better pricing */ export async function getOptimalSwapRoute( tokenIn: string, tokenOut: string, amountIn: string, network = DEFAULT_NETWORK ): Promise<{ bestRoute: { type: 'direct' | 'multi-hop'; amountOut: string; path: `0x${string}`; gasEstimate: string; fees: number[]; route: string[]; }; allRoutes: any[]; }> { try { const validatedTokenIn = services.helpers.validateAddress(tokenIn); const validatedTokenOut = services.helpers.validateAddress(tokenOut); const allFeeTiers = [FEE_TIERS.LOWEST, FEE_TIERS.LOW, FEE_TIERS.MEDIUM, FEE_TIERS.HIGH]; let bestRoute: any = { amountOut: '0' }; const allRoutesConsidered = []; // 1. Find best direct route const directRoutesPromises = allFeeTiers.map(fee => getSwapQuote(amountIn, tokenIn, tokenOut, fee, network) .then(result => ({ ...result, fee })) .catch(() => null) ); const directRoutesResults = await Promise.all(directRoutesPromises); directRoutesResults.forEach(result => { if (result && parseFloat(result.amountOut) > parseFloat(bestRoute.amountOut)) { bestRoute = { type: 'direct', amountOut: result.amountOut, path: result.path, gasEstimate: result.gasEstimate, fees: [result.fee], route: [tokenIn, tokenOut] }; } if(result) allRoutesConsidered.push({type: 'direct', ...result}); }); // 2. Find best multi-hop routes via common tokens (WSEI, USDC) const intermediateTokens = [COMMON_TOKENS.WSEI, COMMON_TOKENS.USDC]; for (const intermediateToken of intermediateTokens) { if (validatedTokenIn.toLowerCase() === intermediateToken.toLowerCase() || validatedTokenOut.toLowerCase() === intermediateToken.toLowerCase()) { continue; } // Find best fee for first hop: tokenIn -> intermediateToken const firstHopPromises = allFeeTiers.map(fee => getSwapQuote(amountIn, tokenIn, intermediateToken, fee, network) .then(result => ({ ...result, fee })) .catch(() => null) ); const firstHopResults = await Promise.all(firstHopPromises); const bestFirstHop = firstHopResults.reduce((best, current) => (current && parseFloat(current.amountOut) > parseFloat(best?.amountOut ?? '0')) ? current : best, null ); if (!bestFirstHop) continue; // No path for first hop // Find best fee for second hop: intermediateToken -> tokenOut const secondHopPromises = allFeeTiers.map(fee => getSwapQuote(bestFirstHop.amountOut, intermediateToken, tokenOut, fee, network) .then(result => ({ ...result, fee })) .catch(() => null) ); const secondHopResults = await Promise.all(secondHopPromises); const bestSecondHop = secondHopResults.reduce((best, current) => (current && parseFloat(current.amountOut) > parseFloat(best?.amountOut ?? '0')) ? current : best, null ); if (!bestSecondHop) continue; // No path for second hop const multiHopPath = createMultiHopPath( [validatedTokenIn, services.helpers.validateAddress(intermediateToken), validatedTokenOut], [bestFirstHop.fee, bestSecondHop.fee] ); const multiHopQuote = await getMultiHopSwapQuote(amountIn, multiHopPath, network); allRoutesConsidered.push({ type: 'multi-hop', ...multiHopQuote, route: [tokenIn, intermediateToken, tokenOut], fees: [bestFirstHop.fee, bestSecondHop.fee] }); if (parseFloat(multiHopQuote.amountOut) > parseFloat(bestRoute.amountOut)) { bestRoute = { type: 'multi-hop', amountOut: multiHopQuote.amountOut, path: multiHopPath, gasEstimate: multiHopQuote.gasEstimate, fees: [bestFirstHop.fee, bestSecondHop.fee], route: [tokenIn, intermediateToken, tokenOut] }; } } if (bestRoute.amountOut === '0') { throw new Error('No valid swap route found for the given tokens.'); } return { bestRoute, allRoutes: allRoutesConsidered }; } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get optimal swap route: ${error.message}`); } throw new Error(`Failed to get optimal swap route: ${String(error)}`); } } /** * Calculate liquidity amounts for a given price range */ export function calculateLiquidityAmounts( amount0: string, amount1: string, currentPrice: string, tickLower: number, tickUpper: number, decimals0: number = 18, decimals1: number = 18 ): { liquidity: string; amount0Used: string; amount1Used: string; ratio: string; } { const amount0Wei = parseUnits(amount0, decimals0); const amount1Wei = parseUnits(amount1, decimals1); const priceLower = parseFloat(tickToPrice(tickLower, decimals0, decimals1)); const priceUpper = parseFloat(tickToPrice(tickUpper, decimals0, decimals1)); const currentPriceNum = parseFloat(currentPrice); // Simplified liquidity calculation // In practice, you'd use the full V3 math let liquidity0 = 0; let liquidity1 = 0; if (currentPriceNum <= priceLower) { // All amount0 liquidity0 = Number(formatUnits(amount0Wei, decimals0)); } else if (currentPriceNum >= priceUpper) { // All amount1 liquidity1 = Number(formatUnits(amount1Wei, decimals1)); } else { // Mixed amounts const ratio = (currentPriceNum - priceLower) / (priceUpper - priceLower); liquidity0 = Number(formatUnits(amount0Wei, decimals0)) * (1 - ratio); liquidity1 = Number(formatUnits(amount1Wei, decimals1)) * ratio; } return { liquidity: Math.max(liquidity0, liquidity1).toString(), amount0Used: liquidity0.toString(), amount1Used: liquidity1.toString(), ratio: (liquidity1 / (liquidity0 + liquidity1)).toString() }; } /** * Monitor pool events and price changes */ export async function monitorPool( poolAddress: string, callback: (event: { type: 'swap' | 'mint' | 'burn'; data: any; timestamp: number; }) => void, network = DEFAULT_NETWORK ): Promise<() => void> { try { const validatedPool = services.helpers.validateAddress(poolAddress); const client = getPublicClient(network); // Set up event listeners const unwatch = client.watchContractEvent({ address: validatedPool, abi: dragonSwapPoolAbi, eventName: 'Swap', onLogs: (logs) => { logs.forEach(log => { callback({ type: 'swap', data: log, timestamp: Date.now() }); }); } }); return unwatch; } catch (error) { if (error instanceof Error) { throw new Error(`Failed to monitor pool: ${error.message}`); } throw new Error(`Failed to monitor pool: ${String(error)}`); } } /** * Get historical pool data */ export async function getPoolHistoricalData( poolAddress: string, fromBlock: number, toBlock: number | 'latest' = 'latest', network = DEFAULT_NETWORK ): Promise<{ swaps: any[]; mints: any[]; burns: any[]; priceHistory: { timestamp: number; price: string; sqrtPriceX96: string; }[]; }> { try { const validatedPool = services.helpers.validateAddress(poolAddress); const client = getPublicClient(network); // Define event signatures for Uniswap V3 style pools const SWAP_EVENT_SIGNATURE = '0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67'; // Swap event const MINT_EVENT_SIGNATURE = '0x7a53080ba414158be7ec69b987b5fb7d07dee101fe85488f0853ae16239d0bde'; // Mint event const BURN_EVENT_SIGNATURE = '0x0c396cd989a39f4459b5fa1aed6a9a8dcdbc45908acfd67e028cd568da98982c'; // Burn event // Get all logs first, then filter by event signature const allLogs = await client.getLogs({ address: validatedPool, fromBlock: BigInt(fromBlock), toBlock: toBlock === 'latest' ? 'latest' : BigInt(toBlock) }); // Filter logs by event signature const swapLogs = allLogs.filter(log => log.topics[0] === SWAP_EVENT_SIGNATURE); const mintLogs = allLogs.filter(log => log.topics[0] === MINT_EVENT_SIGNATURE); const burnLogs = allLogs.filter(log => log.topics[0] === BURN_EVENT_SIGNATURE); // Convert BigInt values to strings for serialization const processLogs = (logs: any[]) => { return logs.map(log => ({ ...log, blockNumber: log.blockNumber.toString(), transactionIndex: log.transactionIndex.toString(), logIndex: log.logIndex.toString(), // Convert any other BigInt fields as needed })); }; const processedSwapLogs = processLogs(swapLogs); const processedMintLogs = processLogs(mintLogs); const processedBurnLogs = processLogs(burnLogs); // Extract price history from swap events (simplified) const priceHistory = processedSwapLogs.map((log, index) => ({ timestamp: Date.now() - (processedSwapLogs.length - index) * 60000, // Mock timestamps price: "0", // You'll need to decode the actual price from log data sqrtPriceX96: "0" // You'll need to decode this from log data })); return { swaps: processedSwapLogs, mints: processedMintLogs, burns: processedBurnLogs, priceHistory: priceHistory }; } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get pool historical data: ${error.message}`); } throw new Error(`Failed to get pool historical data: ${String(error)}`); } } /** * Calculate impermanent loss for a liquidity position */ export function calculateImpermanentLoss( initialPrice: string, currentPrice: string, initialAmount0: string, initialAmount1: string ): { impermanentLoss: string; currentValue: string; hodlValue: string; lossPercentage: string; } { const initialPriceNum = parseFloat(initialPrice); const currentPriceNum = parseFloat(currentPrice); const priceRatio = currentPriceNum / initialPriceNum; const initialAmount0Num = parseFloat(initialAmount0); const initialAmount1Num = parseFloat(initialAmount1); // Calculate current LP position value const currentAmount0 = initialAmount0Num / Math.sqrt(priceRatio); const currentAmount1 = initialAmount1Num * Math.sqrt(priceRatio); const currentValue = currentAmount0 * currentPriceNum + currentAmount1; // Calculate HODL value const hodlValue = initialAmount0Num * currentPriceNum + initialAmount1Num; // Calculate impermanent loss const impermanentLoss = hodlValue - currentValue; const lossPercentage = (impermanentLoss / hodlValue) * 100; return { impermanentLoss: impermanentLoss.toString(), currentValue: currentValue.toString(), hodlValue: hodlValue.toString(), lossPercentage: lossPercentage.toString() }; }

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/testinguser1111111/sei-mcp-server'

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