EDUCHAIN Agent Kit

  • src
import { request, gql } from 'graphql-request'; import { ethers } from 'ethers'; import { Provider } from 'ethers'; import * as blockchain from './blockchain.js'; // SailFish V3 subgraph URL const SUBGRAPH_URL = 'https://api.goldsky.com/api/public/project_cm5nst0b7iiqy01t6hxww7gao/subgraphs/sailfish-v3-occ-mainnet/1.0.0/gn'; // Query to find direct pools between two tokens const DIRECT_POOLS_QUERY = gql` query findDirectPools($token0: String!, $token1: String!) { pools( where: { token0_in: [$token0, $token1] token1_in: [$token0, $token1] liquidity_gt: 0 } ) { id token0 { id symbol decimals name } token1 { id symbol decimals name } feeTier liquidity token0Price token1Price totalValueLockedUSD } } `; // Query to find pools for indirect routes const INDIRECT_POOLS_QUERY = gql` query findIndirectPools($tokenIn: String!, $tokenOut: String!) { # First, find pools containing tokenIn pools0: pools( where: { or: [ { token0: $tokenIn, liquidity_gt: 0 } { token1: $tokenIn, liquidity_gt: 0 } ] } ) { id token0 { id symbol decimals name } token1 { id symbol decimals name } feeTier liquidity totalValueLockedUSD } # Then, find pools containing tokenOut pools1: pools( where: { or: [ { token0: $tokenOut, liquidity_gt: 0 } { token1: $tokenOut, liquidity_gt: 0 } ] } ) { id token0 { id symbol decimals name } token1 { id symbol decimals name } feeTier liquidity totalValueLockedUSD } } `; // Interface for token information export interface TokenInfo { id: string; address: string; symbol: string; decimals: number; name: string; } // Interface for pool information export interface PoolInfo { id: string; token0: TokenInfo; token1: TokenInfo; feeTier: string; liquidity: string; token0Price?: string; token1Price?: string; totalValueLockedUSD: string; } // Interface for route information export interface RouteInfo { type: 'direct' | 'indirect'; path: PoolInfo[]; intermediaryToken?: TokenInfo; totalFee: number; } // Interface for routes result export interface RoutesResult { type: 'direct' | 'indirect'; routes: RouteInfo[]; } /** * Find all possible routes between two tokens * @param tokenInAddress The address of the input token * @param tokenOutAddress The address of the output token * @returns All possible routes between the two tokens */ export async function findAllRoutes(tokenInAddress: string, tokenOutAddress: string): Promise<RoutesResult> { try { // First, try to find direct routes const directPools = await request<{ pools: PoolInfo[] }>(SUBGRAPH_URL, DIRECT_POOLS_QUERY, { token0: tokenInAddress.toLowerCase(), token1: tokenOutAddress.toLowerCase(), }); // If direct routes exist, return them if (directPools.pools.length > 0) { return { type: 'direct', routes: directPools.pools.map((pool) => ({ type: 'direct' as const, path: [ { ...pool, token0: { id: pool.token0.id, address: pool.token0.id, symbol: pool.token0.symbol, decimals: parseInt(pool.token0.decimals.toString()), name: pool.token0.name, }, token1: { id: pool.token1.id, address: pool.token1.id, symbol: pool.token1.symbol, decimals: parseInt(pool.token1.decimals.toString()), name: pool.token1.name, }, }, ], totalFee: parseInt(pool.feeTier) / 1000000, })).sort((a, b) => parseFloat(b.path[0].totalValueLockedUSD) - parseFloat(a.path[0].totalValueLockedUSD) ), }; } // If no direct routes, look for indirect routes const indirectPools = await request<{ pools0: PoolInfo[], pools1: PoolInfo[] }>(SUBGRAPH_URL, INDIRECT_POOLS_QUERY, { tokenIn: tokenInAddress.toLowerCase(), tokenOut: tokenOutAddress.toLowerCase(), }); // Find common tokens between pools containing tokenIn and tokenOut const intermediaryTokens = findIntermediaryTokens( indirectPools.pools0, indirectPools.pools1 ); // Construct indirect routes const routes = constructIndirectRoutes( indirectPools.pools0, indirectPools.pools1, intermediaryTokens ); return { type: 'indirect', routes: routes, }; } catch (error) { console.error('Error finding routes:', error); throw error; } } /** * Find intermediary tokens between two sets of pools * @param pools0 Pools containing the input token * @param pools1 Pools containing the output token * @returns Array of intermediary token addresses */ function findIntermediaryTokens(pools0: PoolInfo[], pools1: PoolInfo[]): string[] { const tokens0 = new Set<string>(); const tokens1 = new Set<string>(); // Collect all tokens from first hop pools pools0.forEach((pool) => { tokens0.add(pool.token0.id); tokens0.add(pool.token1.id); }); // Collect all tokens from second hop pools pools1.forEach((pool) => { tokens1.add(pool.token0.id); tokens1.add(pool.token1.id); }); // Find intersection of tokens (potential intermediary tokens) return Array.from(tokens0).filter((token) => tokens1.has(token)); } /** * Construct indirect routes between two tokens * @param pools0 Pools containing the input token * @param pools1 Pools containing the output token * @param intermediaryTokens Array of intermediary token addresses * @returns Array of indirect routes */ function constructIndirectRoutes(pools0: PoolInfo[], pools1: PoolInfo[], intermediaryTokens: string[]): RouteInfo[] { const routes: RouteInfo[] = []; intermediaryTokens.forEach((intermediaryToken) => { const firstHopPools = pools0.filter( (pool) => pool.token0.id === intermediaryToken || pool.token1.id === intermediaryToken ); const secondHopPools = pools1.filter( (pool) => pool.token0.id === intermediaryToken || pool.token1.id === intermediaryToken ); firstHopPools.forEach((firstPool) => { secondHopPools.forEach((secondPool) => { const intermediaryTokenDetails: TokenInfo = { id: intermediaryToken, address: intermediaryToken, symbol: firstPool.token0.id === intermediaryToken ? firstPool.token0.symbol : firstPool.token1.symbol, decimals: firstPool.token0.id === intermediaryToken ? parseInt(firstPool.token0.decimals.toString()) : parseInt(firstPool.token1.decimals.toString()), name: firstPool.token0.id === intermediaryToken ? firstPool.token0.name : firstPool.token1.name, }; routes.push({ type: 'indirect' as const, path: [ { ...firstPool, token0: { id: firstPool.token0.id, address: firstPool.token0.id, symbol: firstPool.token0.symbol, decimals: parseInt(firstPool.token0.decimals.toString()), name: firstPool.token0.name, }, token1: { id: firstPool.token1.id, address: firstPool.token1.id, symbol: firstPool.token1.symbol, decimals: parseInt(firstPool.token1.decimals.toString()), name: firstPool.token1.name, }, }, { ...secondPool, token0: { id: secondPool.token0.id, address: secondPool.token0.id, symbol: secondPool.token0.symbol, decimals: parseInt(secondPool.token0.decimals.toString()), name: secondPool.token0.name, }, token1: { id: secondPool.token1.id, address: secondPool.token1.id, symbol: secondPool.token1.symbol, decimals: parseInt(secondPool.token1.decimals.toString()), name: secondPool.token1.name, }, }, ], intermediaryToken: intermediaryTokenDetails, totalFee: (parseInt(firstPool.feeTier) + parseInt(secondPool.feeTier)) / 1000000, }); }); }); }); // Sort routes by total liquidity (sum of both pools) return routes.sort( (a, b) => parseFloat(b.path[0].totalValueLockedUSD) + parseFloat(b.path[1].totalValueLockedUSD) - (parseFloat(a.path[0].totalValueLockedUSD) + parseFloat(a.path[1].totalValueLockedUSD)) ); } /** * Get pool information from the contract * @param poolAddress The address of the pool * @param provider The ethers provider * @returns Pool information */ export async function getPoolInfo(poolAddress: string, provider: Provider) { const IUniswapV3PoolABI = [ 'function token0() external view returns (address)', 'function token1() external view returns (address)', 'function fee() external view returns (uint24)', 'function liquidity() external view returns (uint128)', 'function slot0() external view returns (uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked)', ]; const ERC20_ABI = [ 'function balanceOf(address owner) view returns (uint256)', ]; const poolContract = new ethers.Contract( poolAddress, IUniswapV3PoolABI, provider ); const [token0, token1, fee, liquidity, slot0] = await Promise.all([ poolContract.token0(), poolContract.token1(), poolContract.fee(), poolContract.liquidity(), poolContract.slot0(), ]); const [reserve0, reserve1] = await Promise.all([ new ethers.Contract(token0, ERC20_ABI, provider).balanceOf(poolAddress), new ethers.Contract(token1, ERC20_ABI, provider).balanceOf(poolAddress), ]); return { token0, token1, fee: Number(fee), liquidity, sqrtPriceX96: slot0[0], tick: slot0[1], reserve0, reserve1, }; } /** * Get the best route for a swap * @param tokenInAddress The address of the input token * @param tokenOutAddress The address of the output token * @returns The best route for the swap */ export async function getBestRoute(tokenInAddress: string, tokenOutAddress: string): Promise<RouteInfo> { const routes = await findAllRoutes(tokenInAddress, tokenOutAddress); if (routes.routes.length === 0) { throw new Error('No route found'); } // Return the route with the highest liquidity return routes.routes[0]; }