UNPKG

@syncswap/sdk

Version:

SyncSwap TypeScript SDK for building DeFi applications

358 lines 14.5 kB
import fetchRoutePools from "./fetchRoutePools.js"; import { PoolTypes, RouteDirection, } from "./types.js"; import { calculateGroupAmounts, calculatePathAmountsByInput, computeGasFee, createPath, getPoolKey, hasLiquidity, splitAmount } from "../sdkHelper.js"; import { Address } from "../utils/address.js"; import { DECIMALS_6, ETHER, ZERO } from "../utils/constants.js"; import { TokenRegistry } from "../tokens/tokenRegistry.js"; import { Numbers } from "../utils/numbers.js"; import { stateStore } from "../statestore/statestore.js"; export function getRoutePools(userAccount, // using 0x1 by default tokenA, tokenB, routerCommonBasesTokens) { const route = fetchRoutePools(userAccount, tokenA, tokenB, routerCommonBasesTokens); return route; } export class SwapRouter { static routeTimestamp() { return this.latestRouteTimestamp; } } SwapRouter.latestRouteTimestamp = 0; const blacklistedPools = new Set(Array.from([], (item) => Address.normalizeAddress(item))); // find all possible paths from token in to token out with route pools. // this is the first step of processing route pools after fetched. export function findAllPossiblePaths(tokenIn, ts, amountIn) { const paths = []; // all viable paths for swapping //console.log('findAllPossiblePaths routePools', routePools); const routePools = stateStore().currentRoutePools; if (!routePools) { throw Error('Route pools not found'); } // collect paths: direct for (const pool of routePools.pools.poolsDirect) { const allowed = stateStore().aquaPoolOnly ? (pool && (stateStore().allowUniswapV3Pools ? pool.poolType == PoolTypes.UNISWAP_V3 : pool.poolType == PoolTypes.AQUA)) : pool; if (allowed && hasLiquidity(pool, tokenIn) && !blacklistedPools.has(pool.pool.toLowerCase())) { const path = createPath([{ pool: pool, tokenIn: tokenIn, }]); paths.push(path); } } if (ts !== SwapRouter.routeTimestamp()) { throw Error('expired'); } // exit if hops are disabled if (!stateStore().enableHops) { //console.log('[findAllPossiblePaths] Multi-hop is disabled, paths', paths); return paths; } // put pools to map for search const poolByTokens = new Map(); const addPool = (pool) => { if (!stateStore().aquaPoolOnly || (stateStore().allowUniswapV3Pools ? pool.poolType == PoolTypes.UNISWAP_V3 : pool.poolType == PoolTypes.AQUA)) { if (!blacklistedPools.has(pool.pool.toLowerCase())) { const poolKey = getPoolKey(pool.tokenA, pool.tokenB); let pools = poolByTokens.get(poolKey); if (!pools) { poolByTokens.set(poolKey, [pool]); } else { pools.push(pool); } } } }; for (const pool of routePools.pools.poolsA) { if (hasLiquidity(pool, tokenIn, amountIn)) { addPool(pool); } } if (ts !== SwapRouter.routeTimestamp()) { throw Error('expired'); } for (const pool of routePools.pools.poolsB) { if (hasLiquidity(pool)) { addPool(pool); } } if (ts !== SwapRouter.routeTimestamp()) { throw Error('expired'); } for (const pool of routePools?.pools.poolsBase) { if (hasLiquidity(pool)) { addPool(pool); } } if (ts !== SwapRouter.routeTimestamp()) { throw Error('expired'); } //console.log('[findAllPossiblePaths] poolByTokens', poolByTokens); // multi-step paths with hops const length = routePools.routeTokens.length; const tokenOut = (tokenIn === routePools.tokenA ? routePools.tokenB : routePools.tokenA).toLowerCase(); // collect paths: 1 hop for (let i = 0; i < length; i++) { const baseToken = routePools.routeTokens[i].toLowerCase(); // skip invalid paths if (baseToken === tokenIn || baseToken === tokenOut) { continue; } if (ts !== SwapRouter.routeTimestamp()) { throw Error('expired'); } const poolsA = poolByTokens.get(getPoolKey(tokenIn, baseToken)); //console.log('[findAllPossiblePaths] hop 1 search poolA', [tokenIn, baseToken], poolA); // skip if pool A has no liquidity if (!poolsA || poolsA.length === 0) { continue; } const poolsB = poolByTokens.get(getPoolKey(baseToken, tokenOut)); //console.log('[findAllPossiblePaths] hop 1 search poolA', [baseToken, tokenOut], poolB); // skip if pool B has no liquidity if (!poolsB || poolsB.length === 0) { continue; } function createPaths(poolA, poolB) { if (poolA && poolB) { const path = createPath([{ pool: poolA, tokenIn: tokenIn, }, { pool: poolB, tokenIn: baseToken, }]); paths.push(path); } } // A1 - B1, A1 - B2, A1 - B3, A2 - B1, A2 - B2, A2 - B3, A3 - B1, A3 - B2, A3 - B3 for (const poolA of poolsA) { for (const poolB of poolsB) { createPaths(poolA, poolB); } } } if (ts !== SwapRouter.routeTimestamp()) { throw Error('expired'); } // collect paths: 2 hop for (let i = 0; i < length; i++) { // base token 1 const baseToken1 = routePools.routeTokens[i].toLowerCase(); // skip invalid paths if (baseToken1 === tokenIn || baseToken1 === tokenOut) { continue; } if (ts !== SwapRouter.routeTimestamp()) { throw Error('expired'); } const poolsA = poolByTokens.get(getPoolKey(tokenIn, baseToken1)); // skip if pool A has no liquidity if (!poolsA || poolsA.length === 0) { continue; } for (let j = i + 1; j < length; j++) { // base token 2 // skip identical bases if (i === j) { continue; } const baseToken2 = routePools.routeTokens[j].toLowerCase(); // skip invalid paths if (baseToken2 === tokenIn || baseToken2 === tokenOut || baseToken2 === baseToken1) { continue; } const poolsBase = poolByTokens.get(getPoolKey(baseToken1, baseToken2)); // skip if pool Base has no liquidity if (!poolsBase || poolsBase.length === 0) { continue; } if (ts !== SwapRouter.routeTimestamp()) { throw Error('expired'); } const poolsB = poolByTokens.get(getPoolKey(baseToken2, tokenOut)); // skip if pool B has no liquidity if (!poolsB || poolsB.length === 0) { continue; } function createPaths(poolA, poolBase, poolB) { if (poolA && poolBase && poolB) { const path = createPath([{ pool: poolA, tokenIn: tokenIn, }, { pool: poolBase, tokenIn: baseToken1, }, { pool: poolB, tokenIn: baseToken2, }]); paths.push(path); } } for (const poolA of poolsA) { for (const poolBase of poolsBase) { for (const poolB of poolsB) { createPaths(poolA, poolBase, poolB); } } } } } if (ts !== SwapRouter.routeTimestamp()) { throw Error('expired'); } return paths; } const MAX_PATHS = 2; // max path count to use export async function filterPathsToUseExactIn(paths, tokenIn, amountIn, ts, enableSplits = true) { if (paths.length <= MAX_PATHS) { return paths; } const availablePaths = []; const availablePathAmounts = new Map(); // filter available paths with output amount const sessionId = Date.now(); for (const path of paths) { const amounts = await calculatePathAmountsByInput(path, amountIn, sessionId); // skip failed paths if (amounts !== null) { availablePaths.push(path); availablePathAmounts.set(path, amounts); } } if (availablePaths.length <= MAX_PATHS) { return availablePaths; } if (ts !== SwapRouter.routeTimestamp()) { throw Error('expired'); } // sort available paths by output amount availablePaths.sort((pathA, pathB) => { const amountsA = availablePathAmounts.get(pathA); const amountsB = availablePathAmounts.get(pathB); //invariant(amountsA && amountsB, 'No amounts for available path'); return amountsA.amountOut.gt(amountsB.amountOut) ? -1 : 1; }); if (ts !== SwapRouter.routeTimestamp()) { throw Error('expired'); } const pathsToUse = availablePaths.slice(0, enableSplits ? MAX_PATHS : 1 // split count ); return pathsToUse; } // find the best amounts for given paths with all possible input amount groups. // this is the final step of processing route pools, after paths filtered. export async function findBestAmountsForPathsExactIn(paths, amountIn, ts, tokenOut) { const pathAmounts = await splitAmount(amountIn, paths.length); if (ts !== SwapRouter.routeTimestamp()) { throw Error('expired'); } const groups = []; const groupPromises = []; for (const amounts of pathAmounts) { const promise = new Promise((resolve, reject) => { calculateGroupAmounts(paths, amounts).then(group => { if (group === null) { reject('expired'); } else { //groups.push(group); const { amountOut, quoteOut, pathsWithAmounts } = group; if (!amountOut.isZero() && !quoteOut.isZero()) { group.gas = computeGasFee(pathsWithAmounts); //amountGranularity = amountGranularity.add(amountOut); //gasGranularity = gasGranularity.add(group.gas); groups.push(group); } resolve(true); } }); }); groupPromises.push(promise); } if (ts !== SwapRouter.routeTimestamp()) { throw Error('expired'); } await Promise.all(groupPromises); if (ts !== SwapRouter.routeTimestamp()) { throw Error('expired'); } let bestPathsWithAmounts = []; let bestAmountOut = ZERO; let bestQuoteOut = ZERO; let bestPriceImpact = null; let bestVal = null; const tokenOutData = TokenRegistry.getTokenByAddress(tokenOut); const testAmountOut = tokenOutData ? Numbers.pow(tokenOutData.decimals) : ETHER; // one token const gasGranularity = DECIMALS_6; // value for test gas price // const network = ContractRegistry.getNetworkName(); const [tokenOutTestPrice, pathGasPrice] = [ZERO, ZERO]; const computeWithoutGasPrice = pathGasPrice.isZero() || tokenOutTestPrice.isZero(); for (const group of groups) { const groupAmountOut = group.amountOut; if (!groupAmountOut.isZero()) { // Check and update price impact if (!group.quoteOut.isZero()) { const amountLoss = group.quoteOut.sub(groupAmountOut); const groupPriceImpact = amountLoss.mul(ETHER).div(group.quoteOut); if (bestPriceImpact === null || groupPriceImpact.lt(bestPriceImpact)) { bestPriceImpact = groupPriceImpact; } } let bestTempVal = ZERO; if (computeWithoutGasPrice || !group.gas) { bestTempVal = groupAmountOut; } else { const groupAmountOutPrice = groupAmountOut.mul(tokenOutTestPrice).div(testAmountOut); //console.log('groupAmountOutPrice', groupAmountOutPrice.toString(), 'amountGranularity', amountGranularity.toString(), 'tokenOutTestPrice', tokenOutTestPrice.toString()); const groupPathGasPrice = group.gas.mul(pathGasPrice).div(gasGranularity); const finallyPrice = groupAmountOutPrice.sub(groupPathGasPrice); bestTempVal = finallyPrice; } // Check and update amount out //if (groupAmountOut.gt(bestAmountOut)) { const hasBetterValue = !bestVal || bestTempVal.gt(bestVal); //const hasBetterAmountOut = groupAmountOut.gt(bestAmountOut); if (hasBetterValue) { // set as best if has more output, // there is not need to check path length as the case is rare bestPathsWithAmounts = group.pathsWithAmounts; bestAmountOut = groupAmountOut; bestQuoteOut = group.quoteOut; bestVal = bestTempVal; // Check and update price impact if (!group.quoteOut.isZero()) { const amountLoss = group.quoteOut.sub(groupAmountOut); const groupPriceImpact = amountLoss.mul(ETHER).div(group.quoteOut); bestPriceImpact = groupPriceImpact; } else { bestPriceImpact = ZERO; } } } } if (ts !== SwapRouter.routeTimestamp()) { throw Error('expired'); } const found = bestAmountOut !== null; // && !bestAmountOut.isZero() && bestPathsWithAmounts.length !== 0; // estimate gas //startTime = Date.now(); let gas; if (found) { gas = computeGasFee(bestPathsWithAmounts); } else { gas = ZERO; } return { found: found, direction: RouteDirection.EXACT_IN, pathsWithAmounts: bestPathsWithAmounts, amountIn: amountIn, amountOut: bestAmountOut, quote: bestQuoteOut, bestPriceImpact: bestPriceImpact, gas: gas, }; } //# sourceMappingURL=index.js.map