UNPKG

@sky-mavis/smart-order-router

Version:
783 lines 82.5 kB
import { ChainId } from '@sky-mavis/katana-core'; import { Protocol } from '@uniswap/router-sdk'; import { TradeType } from '@uniswap/sdk-core'; import { FeeAmount } from '@uniswap/v3-sdk'; import _ from 'lodash'; import { unparseFeeAmount, WRAPPED_NATIVE_CURRENCY } from '../../../util'; import { parseFeeAmount } from '../../../util/amounts'; import { log } from '../../../util/log'; import { metric, MetricLoggerUnit } from '../../../util/metric'; import { USDC_RONIN_MAINNET, USDC_RONIN_TESTNET } from '../gas-models'; const baseTokensByChain = { [ChainId.mainnet]: [WRAPPED_NATIVE_CURRENCY[ChainId.mainnet], USDC_RONIN_MAINNET], [ChainId.testnet]: [WRAPPED_NATIVE_CURRENCY[ChainId.testnet], USDC_RONIN_TESTNET], }; class SubcategorySelectionPools { constructor(pools, poolsNeeded) { this.pools = pools; this.poolsNeeded = poolsNeeded; } hasEnoughPools() { return this.pools.length >= this.poolsNeeded; } } export async function getV3CandidatePools({ tokenIn, tokenOut, routeType, routingConfig, subgraphProvider, tokenProvider, poolProvider, blockedTokenListProvider, chainId, }) { var _a, _b, _c; const { blockNumber, v3PoolSelection: { topN, topNDirectSwaps, topNTokenInOut, topNSecondHop, topNSecondHopForTokenAddress, tokensToAvoidOnSecondHops, topNWithEachBaseToken, topNWithBaseToken, }, } = routingConfig; const tokenInAddress = tokenIn.address.toLowerCase(); const tokenOutAddress = tokenOut.address.toLowerCase(); const beforeSubgraphPools = Date.now(); const allPools = await subgraphProvider.getPools(tokenIn, tokenOut, { blockNumber, }); log.info({ samplePools: allPools.slice(0, 3) }, 'Got all pools from V3 subgraph provider'); // Although this is less of an optimization than the V2 equivalent, // save some time copying objects by mutating the underlying pool directly. for (const pool of allPools) { pool.token0.id = pool.token0.id.toLowerCase(); pool.token1.id = pool.token1.id.toLowerCase(); } metric.putMetric('V3SubgraphPoolsLoad', Date.now() - beforeSubgraphPools, MetricLoggerUnit.Milliseconds); const beforePoolsFiltered = Date.now(); // Only consider pools where neither tokens are in the blocked token list. let filteredPools = allPools; if (blockedTokenListProvider) { filteredPools = []; for (const pool of allPools) { const token0InBlocklist = await blockedTokenListProvider.hasTokenByAddress(pool.token0.id); const token1InBlocklist = await blockedTokenListProvider.hasTokenByAddress(pool.token1.id); if (token0InBlocklist || token1InBlocklist) { continue; } filteredPools.push(pool); } } // Sort by tvlUSD in descending order const subgraphPoolsSorted = filteredPools.sort((a, b) => b.tvlUSD - a.tvlUSD); log.info(`After filtering blocked tokens went from ${allPools.length} to ${subgraphPoolsSorted.length}.`); const poolAddressesSoFar = new Set(); const addToAddressSet = (pools) => { _(pools) .map(pool => pool.id) .forEach(poolAddress => poolAddressesSoFar.add(poolAddress)); }; const baseTokens = (_a = baseTokensByChain[chainId]) !== null && _a !== void 0 ? _a : []; const topByBaseWithTokenIn = _(baseTokens) .flatMap((token) => { return _(subgraphPoolsSorted) .filter(subgraphPool => { const tokenAddress = token.address.toLowerCase(); return ((subgraphPool.token0.id == tokenAddress && subgraphPool.token1.id == tokenInAddress) || (subgraphPool.token1.id == tokenAddress && subgraphPool.token0.id == tokenInAddress)); }) .sortBy(tokenListPool => -tokenListPool.tvlUSD) .slice(0, topNWithEachBaseToken) .value(); }) .sortBy(tokenListPool => -tokenListPool.tvlUSD) .slice(0, topNWithBaseToken) .value(); const topByBaseWithTokenOut = _(baseTokens) .flatMap((token) => { return _(subgraphPoolsSorted) .filter(subgraphPool => { const tokenAddress = token.address.toLowerCase(); return ((subgraphPool.token0.id == tokenAddress && subgraphPool.token1.id == tokenOutAddress) || (subgraphPool.token1.id == tokenAddress && subgraphPool.token0.id == tokenOutAddress)); }) .sortBy(tokenListPool => -tokenListPool.tvlUSD) .slice(0, topNWithEachBaseToken) .value(); }) .sortBy(tokenListPool => -tokenListPool.tvlUSD) .slice(0, topNWithBaseToken) .value(); let top2DirectSwapPool = _(subgraphPoolsSorted) .filter(subgraphPool => { return (!poolAddressesSoFar.has(subgraphPool.id) && ((subgraphPool.token0.id == tokenInAddress && subgraphPool.token1.id == tokenOutAddress) || (subgraphPool.token1.id == tokenInAddress && subgraphPool.token0.id == tokenOutAddress))); }) .slice(0, topNDirectSwaps) .value(); if (top2DirectSwapPool.length == 0 && topNDirectSwaps > 0) { // If we requested direct swap pools but did not find any in the subgraph query. // Optimistically add them into the query regardless. Invalid pools ones will be dropped anyway // when we query the pool on-chain. Ensures that new pools for new pairs can be swapped on immediately. top2DirectSwapPool = _.map([FeeAmount.HIGH, FeeAmount.MEDIUM, FeeAmount.LOW, FeeAmount.LOWEST], feeAmount => { const { token0, token1, poolAddress } = poolProvider.getPoolAddress(tokenIn, tokenOut, feeAmount); return { id: poolAddress, feeTier: unparseFeeAmount(feeAmount), liquidity: '10000', token0: { id: token0.address, }, token1: { id: token1.address, }, tvlETH: 10000, tvlUSD: 10000, }; }); } addToAddressSet(top2DirectSwapPool); const wrappedNativeAddress = (_b = WRAPPED_NATIVE_CURRENCY[chainId]) === null || _b === void 0 ? void 0 : _b.address.toLowerCase(); // Main reason we need this is for gas estimates, only needed if token out is not native. // We don't check the seen address set because if we've already added pools for getting native quotes // theres no need to add more. let top2EthQuoteTokenPool = []; if (((_c = WRAPPED_NATIVE_CURRENCY[chainId]) === null || _c === void 0 ? void 0 : _c.symbol) == 'WRON' && tokenOut.symbol != 'RON' && tokenOut.symbol != 'WRON') { top2EthQuoteTokenPool = _(subgraphPoolsSorted) .filter(subgraphPool => { if (routeType == TradeType.EXACT_INPUT) { return ((subgraphPool.token0.id == wrappedNativeAddress && subgraphPool.token1.id == tokenOutAddress) || (subgraphPool.token1.id == wrappedNativeAddress && subgraphPool.token0.id == tokenOutAddress)); } else { return ((subgraphPool.token0.id == wrappedNativeAddress && subgraphPool.token1.id == tokenInAddress) || (subgraphPool.token1.id == wrappedNativeAddress && subgraphPool.token0.id == tokenInAddress)); } }) .slice(0, 1) .value(); } addToAddressSet(top2EthQuoteTokenPool); const topByTVL = _(subgraphPoolsSorted) .filter(subgraphPool => { return !poolAddressesSoFar.has(subgraphPool.id); }) .slice(0, topN) .value(); addToAddressSet(topByTVL); const topByTVLUsingTokenIn = _(subgraphPoolsSorted) .filter(subgraphPool => { return (!poolAddressesSoFar.has(subgraphPool.id) && (subgraphPool.token0.id == tokenInAddress || subgraphPool.token1.id == tokenInAddress)); }) .slice(0, topNTokenInOut) .value(); addToAddressSet(topByTVLUsingTokenIn); const topByTVLUsingTokenOut = _(subgraphPoolsSorted) .filter(subgraphPool => { return (!poolAddressesSoFar.has(subgraphPool.id) && (subgraphPool.token0.id == tokenOutAddress || subgraphPool.token1.id == tokenOutAddress)); }) .slice(0, topNTokenInOut) .value(); addToAddressSet(topByTVLUsingTokenOut); const topByTVLUsingTokenInSecondHops = _(topByTVLUsingTokenIn) .map(subgraphPool => { return tokenInAddress == subgraphPool.token0.id ? subgraphPool.token1.id : subgraphPool.token0.id; }) .flatMap((secondHopId) => { var _a; return _(subgraphPoolsSorted) .filter(subgraphPool => { return (!poolAddressesSoFar.has(subgraphPool.id) && !(tokensToAvoidOnSecondHops === null || tokensToAvoidOnSecondHops === void 0 ? void 0 : tokensToAvoidOnSecondHops.includes(secondHopId.toLowerCase())) && (subgraphPool.token0.id == secondHopId || subgraphPool.token1.id == secondHopId)); }) .slice(0, (_a = topNSecondHopForTokenAddress === null || topNSecondHopForTokenAddress === void 0 ? void 0 : topNSecondHopForTokenAddress.get(secondHopId)) !== null && _a !== void 0 ? _a : topNSecondHop) .value(); }) .uniqBy(pool => pool.id) .value(); addToAddressSet(topByTVLUsingTokenInSecondHops); const topByTVLUsingTokenOutSecondHops = _(topByTVLUsingTokenOut) .map(subgraphPool => { return tokenOutAddress == subgraphPool.token0.id ? subgraphPool.token1.id : subgraphPool.token0.id; }) .flatMap((secondHopId) => { var _a; return _(subgraphPoolsSorted) .filter(subgraphPool => { return (!poolAddressesSoFar.has(subgraphPool.id) && !(tokensToAvoidOnSecondHops === null || tokensToAvoidOnSecondHops === void 0 ? void 0 : tokensToAvoidOnSecondHops.includes(secondHopId.toLowerCase())) && (subgraphPool.token0.id == secondHopId || subgraphPool.token1.id == secondHopId)); }) .slice(0, (_a = topNSecondHopForTokenAddress === null || topNSecondHopForTokenAddress === void 0 ? void 0 : topNSecondHopForTokenAddress.get(secondHopId)) !== null && _a !== void 0 ? _a : topNSecondHop) .value(); }) .uniqBy(pool => pool.id) .value(); addToAddressSet(topByTVLUsingTokenOutSecondHops); const subgraphPools = _([ ...topByBaseWithTokenIn, ...topByBaseWithTokenOut, ...top2DirectSwapPool, ...top2EthQuoteTokenPool, ...topByTVL, ...topByTVLUsingTokenIn, ...topByTVLUsingTokenOut, ...topByTVLUsingTokenInSecondHops, ...topByTVLUsingTokenOutSecondHops, ]) .compact() .uniqBy(pool => pool.id) .value(); const tokenAddresses = _(subgraphPools) .flatMap(subgraphPool => [subgraphPool.token0.id, subgraphPool.token1.id]) .compact() .uniq() .value(); log.info(`Getting the ${tokenAddresses.length} tokens within the ${subgraphPools.length} V3 pools we are considering`); const tokenAccessor = await tokenProvider.getTokens(tokenAddresses, { blockNumber, }); const printV3SubgraphPool = (s) => { var _a, _b, _c, _d; return `${(_b = (_a = tokenAccessor.getTokenByAddress(s.token0.id)) === null || _a === void 0 ? void 0 : _a.symbol) !== null && _b !== void 0 ? _b : s.token0.id}/${(_d = (_c = tokenAccessor.getTokenByAddress(s.token1.id)) === null || _c === void 0 ? void 0 : _c.symbol) !== null && _d !== void 0 ? _d : s.token1.id}/${s.feeTier}`; }; log.info({ topByBaseWithTokenIn: topByBaseWithTokenIn.map(printV3SubgraphPool), topByBaseWithTokenOut: topByBaseWithTokenOut.map(printV3SubgraphPool), topByTVL: topByTVL.map(printV3SubgraphPool), topByTVLUsingTokenIn: topByTVLUsingTokenIn.map(printV3SubgraphPool), topByTVLUsingTokenOut: topByTVLUsingTokenOut.map(printV3SubgraphPool), topByTVLUsingTokenInSecondHops: topByTVLUsingTokenInSecondHops.map(printV3SubgraphPool), topByTVLUsingTokenOutSecondHops: topByTVLUsingTokenOutSecondHops.map(printV3SubgraphPool), top2DirectSwap: top2DirectSwapPool.map(printV3SubgraphPool), top2EthQuotePool: top2EthQuoteTokenPool.map(printV3SubgraphPool), }, `V3 Candidate Pools`); const tokenPairsRaw = _.map(subgraphPools, subgraphPool => { const tokenA = tokenAccessor.getTokenByAddress(subgraphPool.token0.id); const tokenB = tokenAccessor.getTokenByAddress(subgraphPool.token1.id); let fee; try { fee = parseFeeAmount(subgraphPool.feeTier); } catch (err) { log.info({ subgraphPool }, `Dropping candidate pool for ${subgraphPool.token0.id}/${subgraphPool.token1.id}/${subgraphPool.feeTier} because fee tier not supported`); return undefined; } if (!tokenA || !tokenB) { log.info(`Dropping candidate pool for ${subgraphPool.token0.id}/${subgraphPool.token1.id}/${fee} because ${tokenA ? subgraphPool.token1.id : subgraphPool.token0.id} not found by token provider`); return undefined; } return [tokenA, tokenB, fee]; }); const tokenPairs = _.compact(tokenPairsRaw); metric.putMetric('V3PoolsFilterLoad', Date.now() - beforePoolsFiltered, MetricLoggerUnit.Milliseconds); const beforePoolsLoad = Date.now(); const poolAccessor = await poolProvider.getPools(tokenPairs, { blockNumber, }); metric.putMetric('V3PoolsLoad', Date.now() - beforePoolsLoad, MetricLoggerUnit.Milliseconds); const poolsBySelection = { protocol: Protocol.V3, selections: { topByBaseWithTokenIn, topByBaseWithTokenOut, topByDirectSwapPool: top2DirectSwapPool, topByEthQuoteTokenPool: top2EthQuoteTokenPool, topByTVL, topByTVLUsingTokenIn, topByTVLUsingTokenOut, topByTVLUsingTokenInSecondHops, topByTVLUsingTokenOutSecondHops, }, }; return { poolAccessor, candidatePools: poolsBySelection, subgraphPools }; } export async function getV2CandidatePools({ tokenIn, tokenOut, routeType, routingConfig, subgraphProvider, tokenProvider, poolProvider, blockedTokenListProvider, chainId, }) { var _a; const { blockNumber, v2PoolSelection: { topN, topNDirectSwaps, topNTokenInOut, topNSecondHop, tokensToAvoidOnSecondHops, topNWithEachBaseToken, topNWithBaseToken, }, } = routingConfig; const tokenInAddress = tokenIn.address.toLowerCase(); const tokenOutAddress = tokenOut.address.toLowerCase(); const beforeSubgraphPools = Date.now(); const allPoolsRaw = await subgraphProvider.getPools(tokenIn, tokenOut, { blockNumber, }); // With tens of thousands of V2 pools, operations that copy pools become costly. // Mutate the pool directly rather than creating a new pool / token to optimmize for speed. for (const pool of allPoolsRaw) { pool.token0.id = pool.token0.id.toLowerCase(); pool.token1.id = pool.token1.id.toLowerCase(); } metric.putMetric('V2SubgraphPoolsLoad', Date.now() - beforeSubgraphPools, MetricLoggerUnit.Milliseconds); const beforePoolsFiltered = Date.now(); // Sort by pool reserve in descending order. const subgraphPoolsSorted = allPoolsRaw.sort((a, b) => b.reserve - a.reserve); const poolAddressesSoFar = new Set(); // Always add the direct swap pool into the mix regardless of if it exists in the subgraph pool list. // Ensures that new pools can be swapped on immediately, and that if a pool was filtered out of the // subgraph query for some reason (e.g. trackedReserveETH was 0), then we still consider it. let topByDirectSwapPool = []; if (topNDirectSwaps > 0) { const { token0, token1, poolAddress } = poolProvider.getPoolAddress(tokenIn, tokenOut); poolAddressesSoFar.add(poolAddress.toLowerCase()); topByDirectSwapPool = [ { id: poolAddress, token0: { id: token0.address, }, token1: { id: token1.address, }, supply: 10000, reserve: 10000, reserveUSD: 10000, // Not used. Set to arbitrary number. }, ]; } const wethAddress = WRAPPED_NATIVE_CURRENCY[chainId].address.toLowerCase(); const topByBaseWithTokenInMap = new Map(); const topByBaseWithTokenOutMap = new Map(); const baseTokens = (_a = baseTokensByChain[chainId]) !== null && _a !== void 0 ? _a : []; const baseTokensAddresses = new Set(); baseTokens.forEach(token => { const baseTokenAddr = token.address.toLowerCase(); baseTokensAddresses.add(baseTokenAddr); topByBaseWithTokenInMap.set(baseTokenAddr, new SubcategorySelectionPools([], topNWithEachBaseToken)); topByBaseWithTokenOutMap.set(baseTokenAddr, new SubcategorySelectionPools([], topNWithEachBaseToken)); }); let topByBaseWithTokenInPoolsFound = 0; let topByBaseWithTokenOutPoolsFound = 0; // Main reason we need this is for gas estimates // There can ever only be 1 Token/ETH pool, so we will only look for 1 let topNEthQuoteToken = 1; // but, we only need it if token out is not ETH. if (tokenOut.symbol == 'RON' || tokenOut.symbol == 'WRON') { // if it's eth we change the topN to 0, so we can break early from the loop. topNEthQuoteToken = 0; } const topByEthQuoteTokenPool = []; const topByTVLUsingTokenIn = []; const topByTVLUsingTokenOut = []; const topByTVL = []; // Used to track how many iterations we do in the first loop let loopsInFirstIteration = 0; // Filtering step for up to first hop // The pools are pre-sorted, so we can just iterate through them and fill our heuristics. for (const subgraphPool of subgraphPoolsSorted) { loopsInFirstIteration += 1; // Check if we have satisfied all the heuristics, if so, we can stop. if (topByBaseWithTokenInPoolsFound >= topNWithBaseToken && topByBaseWithTokenOutPoolsFound >= topNWithBaseToken && topByEthQuoteTokenPool.length >= topNEthQuoteToken && topByTVL.length >= topN && topByTVLUsingTokenIn.length >= topNTokenInOut && topByTVLUsingTokenOut.length >= topNTokenInOut) { // We have satisfied all the heuristics, so we can stop. break; } if (poolAddressesSoFar.has(subgraphPool.id)) { // We've already added this pool, so skip it. continue; } // Only consider pools where neither tokens are in the blocked token list. if (blockedTokenListProvider) { const [token0InBlocklist, token1InBlocklist] = await Promise.all([ blockedTokenListProvider.hasTokenByAddress(subgraphPool.token0.id), blockedTokenListProvider.hasTokenByAddress(subgraphPool.token1.id), ]); if (token0InBlocklist || token1InBlocklist) { continue; } } const tokenInToken0TopByBase = topByBaseWithTokenInMap.get(subgraphPool.token0.id); if (topByBaseWithTokenInPoolsFound < topNWithBaseToken && tokenInToken0TopByBase && subgraphPool.token0.id != tokenOutAddress && subgraphPool.token1.id == tokenInAddress) { topByBaseWithTokenInPoolsFound += 1; poolAddressesSoFar.add(subgraphPool.id); if (topByTVLUsingTokenIn.length < topNTokenInOut) { topByTVLUsingTokenIn.push(subgraphPool); } if (routeType === TradeType.EXACT_OUTPUT && subgraphPool.token0.id == wethAddress) { topByEthQuoteTokenPool.push(subgraphPool); } tokenInToken0TopByBase.pools.push(subgraphPool); continue; } const tokenInToken1TopByBase = topByBaseWithTokenInMap.get(subgraphPool.token1.id); if (topByBaseWithTokenInPoolsFound < topNWithBaseToken && tokenInToken1TopByBase && subgraphPool.token0.id == tokenInAddress && subgraphPool.token1.id != tokenOutAddress) { topByBaseWithTokenInPoolsFound += 1; poolAddressesSoFar.add(subgraphPool.id); if (topByTVLUsingTokenIn.length < topNTokenInOut) { topByTVLUsingTokenIn.push(subgraphPool); } if (routeType === TradeType.EXACT_OUTPUT && subgraphPool.token1.id == wethAddress) { topByEthQuoteTokenPool.push(subgraphPool); } tokenInToken1TopByBase.pools.push(subgraphPool); continue; } const tokenOutToken0TopByBase = topByBaseWithTokenOutMap.get(subgraphPool.token0.id); if (topByBaseWithTokenOutPoolsFound < topNWithBaseToken && tokenOutToken0TopByBase && subgraphPool.token0.id != tokenInAddress && subgraphPool.token1.id == tokenOutAddress) { topByBaseWithTokenOutPoolsFound += 1; poolAddressesSoFar.add(subgraphPool.id); if (topByTVLUsingTokenOut.length < topNTokenInOut) { topByTVLUsingTokenOut.push(subgraphPool); } if (routeType === TradeType.EXACT_INPUT && subgraphPool.token0.id == wethAddress) { topByEthQuoteTokenPool.push(subgraphPool); } tokenOutToken0TopByBase.pools.push(subgraphPool); continue; } const tokenOutToken1TopByBase = topByBaseWithTokenOutMap.get(subgraphPool.token1.id); if (topByBaseWithTokenOutPoolsFound < topNWithBaseToken && tokenOutToken1TopByBase && subgraphPool.token0.id == tokenOutAddress && subgraphPool.token1.id != tokenInAddress) { topByBaseWithTokenOutPoolsFound += 1; poolAddressesSoFar.add(subgraphPool.id); if (topByTVLUsingTokenOut.length < topNTokenInOut) { topByTVLUsingTokenOut.push(subgraphPool); } if (routeType === TradeType.EXACT_INPUT && subgraphPool.token1.id == wethAddress) { topByEthQuoteTokenPool.push(subgraphPool); } tokenOutToken1TopByBase.pools.push(subgraphPool); continue; } // Note: we do not need to check other native currencies for the V2 Protocol if (topByEthQuoteTokenPool.length < topNEthQuoteToken && ((routeType === TradeType.EXACT_INPUT && ((subgraphPool.token0.id == wethAddress && subgraphPool.token1.id == tokenOutAddress) || (subgraphPool.token1.id == wethAddress && subgraphPool.token0.id == tokenOutAddress))) || (routeType === TradeType.EXACT_OUTPUT && ((subgraphPool.token0.id == wethAddress && subgraphPool.token1.id == tokenInAddress) || (subgraphPool.token1.id == wethAddress && subgraphPool.token0.id == tokenInAddress))))) { poolAddressesSoFar.add(subgraphPool.id); topByEthQuoteTokenPool.push(subgraphPool); continue; } if (topByTVL.length < topN) { poolAddressesSoFar.add(subgraphPool.id); topByTVL.push(subgraphPool); continue; } if (topByTVLUsingTokenIn.length < topNTokenInOut && (subgraphPool.token0.id == tokenInAddress || subgraphPool.token1.id == tokenInAddress)) { poolAddressesSoFar.add(subgraphPool.id); topByTVLUsingTokenIn.push(subgraphPool); continue; } if (topByTVLUsingTokenOut.length < topNTokenInOut && (subgraphPool.token0.id == tokenOutAddress || subgraphPool.token1.id == tokenOutAddress)) { poolAddressesSoFar.add(subgraphPool.id); topByTVLUsingTokenOut.push(subgraphPool); continue; } } metric.putMetric('V2SubgraphLoopsInFirstIteration', loopsInFirstIteration, MetricLoggerUnit.Count); const topByBaseWithTokenIn = []; for (const topByBaseWithTokenInSelection of topByBaseWithTokenInMap.values()) { topByBaseWithTokenIn.push(...topByBaseWithTokenInSelection.pools); } const topByBaseWithTokenOut = []; for (const topByBaseWithTokenOutSelection of topByBaseWithTokenOutMap.values()) { topByBaseWithTokenOut.push(...topByBaseWithTokenOutSelection.pools); } // Filtering step for second hops const topByTVLUsingTokenInSecondHopsMap = new Map(); const topByTVLUsingTokenOutSecondHopsMap = new Map(); const tokenInSecondHopAddresses = topByTVLUsingTokenIn .filter(pool => { // filtering second hops if (tokenInAddress === pool.token0.id) { return !(tokensToAvoidOnSecondHops === null || tokensToAvoidOnSecondHops === void 0 ? void 0 : tokensToAvoidOnSecondHops.includes(pool.token1.id.toLowerCase())); } else { return !(tokensToAvoidOnSecondHops === null || tokensToAvoidOnSecondHops === void 0 ? void 0 : tokensToAvoidOnSecondHops.includes(pool.token0.id.toLowerCase())); } }) .map(pool => (tokenInAddress === pool.token0.id ? pool.token1.id : pool.token0.id)); const tokenOutSecondHopAddresses = topByTVLUsingTokenOut .filter(pool => { // filtering second hops if (tokenOutAddress === pool.token0.id) { return !(tokensToAvoidOnSecondHops === null || tokensToAvoidOnSecondHops === void 0 ? void 0 : tokensToAvoidOnSecondHops.includes(pool.token1.id.toLowerCase())); } else { return !(tokensToAvoidOnSecondHops === null || tokensToAvoidOnSecondHops === void 0 ? void 0 : tokensToAvoidOnSecondHops.includes(pool.token0.id.toLowerCase())); } }) .map(pool => (tokenOutAddress === pool.token0.id ? pool.token1.id : pool.token0.id)); for (const secondHopId of tokenInSecondHopAddresses) { topByTVLUsingTokenInSecondHopsMap.set(secondHopId, new SubcategorySelectionPools([], topNSecondHop)); } for (const secondHopId of tokenOutSecondHopAddresses) { topByTVLUsingTokenOutSecondHopsMap.set(secondHopId, new SubcategorySelectionPools([], topNSecondHop)); } // Used to track how many iterations we do in the second loop let loopsInSecondIteration = 0; if (tokenInSecondHopAddresses.length > 0 || tokenOutSecondHopAddresses.length > 0) { for (const subgraphPool of subgraphPoolsSorted) { loopsInSecondIteration += 1; let allTokenInSecondHopsHaveTheirTopN = true; for (const secondHopPools of topByTVLUsingTokenInSecondHopsMap.values()) { if (!secondHopPools.hasEnoughPools()) { allTokenInSecondHopsHaveTheirTopN = false; break; } } let allTokenOutSecondHopsHaveTheirTopN = true; for (const secondHopPools of topByTVLUsingTokenOutSecondHopsMap.values()) { if (!secondHopPools.hasEnoughPools()) { allTokenOutSecondHopsHaveTheirTopN = false; break; } } if (allTokenInSecondHopsHaveTheirTopN && allTokenOutSecondHopsHaveTheirTopN) { // We have satisfied all the heuristics, so we can stop. break; } if (poolAddressesSoFar.has(subgraphPool.id)) { continue; } // Only consider pools where neither tokens are in the blocked token list. if (blockedTokenListProvider) { const [token0InBlocklist, token1InBlocklist] = await Promise.all([ blockedTokenListProvider.hasTokenByAddress(subgraphPool.token0.id), blockedTokenListProvider.hasTokenByAddress(subgraphPool.token1.id), ]); if (token0InBlocklist || token1InBlocklist) { continue; } } const tokenInToken0SecondHop = topByTVLUsingTokenInSecondHopsMap.get(subgraphPool.token0.id); if (tokenInToken0SecondHop && !tokenInToken0SecondHop.hasEnoughPools()) { poolAddressesSoFar.add(subgraphPool.id); tokenInToken0SecondHop.pools.push(subgraphPool); continue; } const tokenInToken1SecondHop = topByTVLUsingTokenInSecondHopsMap.get(subgraphPool.token1.id); if (tokenInToken1SecondHop && !tokenInToken1SecondHop.hasEnoughPools()) { poolAddressesSoFar.add(subgraphPool.id); tokenInToken1SecondHop.pools.push(subgraphPool); continue; } const tokenOutToken0SecondHop = topByTVLUsingTokenOutSecondHopsMap.get(subgraphPool.token0.id); if (tokenOutToken0SecondHop && !tokenOutToken0SecondHop.hasEnoughPools()) { poolAddressesSoFar.add(subgraphPool.id); tokenOutToken0SecondHop.pools.push(subgraphPool); continue; } const tokenOutToken1SecondHop = topByTVLUsingTokenOutSecondHopsMap.get(subgraphPool.token1.id); if (tokenOutToken1SecondHop && !tokenOutToken1SecondHop.hasEnoughPools()) { poolAddressesSoFar.add(subgraphPool.id); tokenOutToken1SecondHop.pools.push(subgraphPool); continue; } } } metric.putMetric('V2SubgraphLoopsInSecondIteration', loopsInSecondIteration, MetricLoggerUnit.Count); const topByTVLUsingTokenInSecondHops = []; for (const secondHopPools of topByTVLUsingTokenInSecondHopsMap.values()) { topByTVLUsingTokenInSecondHops.push(...secondHopPools.pools); } const topByTVLUsingTokenOutSecondHops = []; for (const secondHopPools of topByTVLUsingTokenOutSecondHopsMap.values()) { topByTVLUsingTokenOutSecondHops.push(...secondHopPools.pools); } const subgraphPools = _([ ...topByBaseWithTokenIn, ...topByBaseWithTokenOut, ...topByDirectSwapPool, ...topByEthQuoteTokenPool, ...topByTVL, ...topByTVLUsingTokenIn, ...topByTVLUsingTokenOut, ...topByTVLUsingTokenInSecondHops, ...topByTVLUsingTokenOutSecondHops, ]) .uniqBy(pool => pool.id) .value(); const tokenAddressesSet = new Set(); for (const pool of subgraphPools) { tokenAddressesSet.add(pool.token0.id); tokenAddressesSet.add(pool.token1.id); } const tokenAddresses = Array.from(tokenAddressesSet); log.info(`Getting the ${tokenAddresses.length} tokens within the ${subgraphPools.length} V2 pools we are considering`); const tokenAccessor = await tokenProvider.getTokens(tokenAddresses, { blockNumber, }); const printV2SubgraphPool = (s) => { var _a, _b, _c, _d; return `${(_b = (_a = tokenAccessor.getTokenByAddress(s.token0.id)) === null || _a === void 0 ? void 0 : _a.symbol) !== null && _b !== void 0 ? _b : s.token0.id}/${(_d = (_c = tokenAccessor.getTokenByAddress(s.token1.id)) === null || _c === void 0 ? void 0 : _c.symbol) !== null && _d !== void 0 ? _d : s.token1.id}`; }; log.info({ topByBaseWithTokenIn: topByBaseWithTokenIn.map(printV2SubgraphPool), topByBaseWithTokenOut: topByBaseWithTokenOut.map(printV2SubgraphPool), topByTVL: topByTVL.map(printV2SubgraphPool), topByTVLUsingTokenIn: topByTVLUsingTokenIn.map(printV2SubgraphPool), topByTVLUsingTokenOut: topByTVLUsingTokenOut.map(printV2SubgraphPool), topByTVLUsingTokenInSecondHops: topByTVLUsingTokenInSecondHops.map(printV2SubgraphPool), topByTVLUsingTokenOutSecondHops: topByTVLUsingTokenOutSecondHops.map(printV2SubgraphPool), top2DirectSwap: topByDirectSwapPool.map(printV2SubgraphPool), top2EthQuotePool: topByEthQuoteTokenPool.map(printV2SubgraphPool), }, `V2 Candidate pools`); const tokenPairsRaw = _.map(subgraphPools, subgraphPool => { const tokenA = tokenAccessor.getTokenByAddress(subgraphPool.token0.id); const tokenB = tokenAccessor.getTokenByAddress(subgraphPool.token1.id); if (!tokenA || !tokenB) { log.info(`Dropping candidate pool for ${subgraphPool.token0.id}/${subgraphPool.token1.id}`); return undefined; } return [tokenA, tokenB]; }); const tokenPairs = _.compact(tokenPairsRaw); metric.putMetric('V2PoolsFilterLoad', Date.now() - beforePoolsFiltered, MetricLoggerUnit.Milliseconds); const beforePoolsLoad = Date.now(); // this should be the only place to enable fee-on-transfer fee fetching, // because this places loads pools (pairs of tokens with fot taxes) from the subgraph const poolAccessor = await poolProvider.getPools(tokenPairs, routingConfig); metric.putMetric('V2PoolsLoad', Date.now() - beforePoolsLoad, MetricLoggerUnit.Milliseconds); const poolsBySelection = { protocol: Protocol.V2, selections: { topByBaseWithTokenIn, topByBaseWithTokenOut, topByDirectSwapPool, topByEthQuoteTokenPool, topByTVL, topByTVLUsingTokenIn, topByTVLUsingTokenOut, topByTVLUsingTokenInSecondHops, topByTVLUsingTokenOutSecondHops, }, }; return { poolAccessor, candidatePools: poolsBySelection, subgraphPools }; } export async function getMixedRouteCandidatePools({ v3CandidatePools, v2CandidatePools, routingConfig, tokenProvider, v3poolProvider, v2poolProvider, }) { const beforeSubgraphPools = Date.now(); const [{ subgraphPools: V3subgraphPools, candidatePools: V3candidatePools }, { subgraphPools: V2subgraphPools, candidatePools: V2candidatePools },] = [v3CandidatePools, v2CandidatePools]; metric.putMetric('MixedSubgraphPoolsLoad', Date.now() - beforeSubgraphPools, MetricLoggerUnit.Milliseconds); const beforePoolsFiltered = Date.now(); /** * Main heuristic for pruning mixedRoutes: * - we pick V2 pools with higher liq than respective V3 pools, or if the v3 pool doesn't exist * * This way we can reduce calls to our provider since it's possible to generate a lot of mixed routes */ /// We only really care about pools involving the tokenIn or tokenOut explictly, /// since there's no way a long tail token in V2 would be routed through as an intermediary const V2topByTVLPoolIds = new Set([ ...V2candidatePools.selections.topByTVLUsingTokenIn, ...V2candidatePools.selections.topByBaseWithTokenIn, /// tokenOut: ...V2candidatePools.selections.topByTVLUsingTokenOut, ...V2candidatePools.selections.topByBaseWithTokenOut, /// Direct swap: ...V2candidatePools.selections.topByDirectSwapPool, ].map(poolId => poolId.id)); const V2topByTVLSortedPools = _(V2subgraphPools) .filter(pool => V2topByTVLPoolIds.has(pool.id)) .sortBy(pool => -pool.reserveUSD) .value(); /// we consider all returned V3 pools for this heuristic to "fill in the gaps" const V3sortedPools = _(V3subgraphPools) .sortBy(pool => -pool.tvlUSD) .value(); /// Finding pools with greater reserveUSD on v2 than tvlUSD on v3, or if there is no v3 liquidity const buildV2Pools = []; V2topByTVLSortedPools.forEach(V2subgraphPool => { const V3subgraphPool = V3sortedPools.find(pool => (pool.token0.id == V2subgraphPool.token0.id && pool.token1.id == V2subgraphPool.token1.id) || (pool.token0.id == V2subgraphPool.token1.id && pool.token1.id == V2subgraphPool.token0.id)); if (V3subgraphPool) { if (V2subgraphPool.reserveUSD > V3subgraphPool.tvlUSD) { log.info({ token0: V2subgraphPool.token0.id, token1: V2subgraphPool.token1.id, v2reserveUSD: V2subgraphPool.reserveUSD, v3tvlUSD: V3subgraphPool.tvlUSD, }, `MixedRoute heuristic, found a V2 pool with higher liquidity than its V3 counterpart`); buildV2Pools.push(V2subgraphPool); } } else { log.info({ token0: V2subgraphPool.token0.id, token1: V2subgraphPool.token1.id, v2reserveUSD: V2subgraphPool.reserveUSD, }, `MixedRoute heuristic, found a V2 pool with no V3 counterpart`); buildV2Pools.push(V2subgraphPool); } }); log.info(buildV2Pools.length, `Number of V2 candidate pools that fit first heuristic`); const subgraphPools = [...buildV2Pools, ...V3sortedPools]; const tokenAddresses = _(subgraphPools) .flatMap(subgraphPool => [subgraphPool.token0.id, subgraphPool.token1.id]) .compact() .uniq() .value(); log.info(`Getting the ${tokenAddresses.length} tokens within the ${subgraphPools.length} pools we are considering`); const tokenAccessor = await tokenProvider.getTokens(tokenAddresses, routingConfig); const V3tokenPairsRaw = _.map(V3sortedPools, subgraphPool => { const tokenA = tokenAccessor.getTokenByAddress(subgraphPool.token0.id); const tokenB = tokenAccessor.getTokenByAddress(subgraphPool.token1.id); let fee; try { fee = parseFeeAmount(subgraphPool.feeTier); } catch (err) { log.info({ subgraphPool }, `Dropping candidate pool for ${subgraphPool.token0.id}/${subgraphPool.token1.id}/${subgraphPool.feeTier} because fee tier not supported`); return undefined; } if (!tokenA || !tokenB) { log.info(`Dropping candidate pool for ${subgraphPool.token0.id}/${subgraphPool.token1.id}/${fee} because ${tokenA ? subgraphPool.token1.id : subgraphPool.token0.id} not found by token provider`); return undefined; } return [tokenA, tokenB, fee]; }); const V3tokenPairs = _.compact(V3tokenPairsRaw); const V2tokenPairsRaw = _.map(buildV2Pools, subgraphPool => { const tokenA = tokenAccessor.getTokenByAddress(subgraphPool.token0.id); const tokenB = tokenAccessor.getTokenByAddress(subgraphPool.token1.id); if (!tokenA || !tokenB) { log.info(`Dropping candidate pool for ${subgraphPool.token0.id}/${subgraphPool.token1.id}`); return undefined; } return [tokenA, tokenB]; }); const V2tokenPairs = _.compact(V2tokenPairsRaw); metric.putMetric('MixedPoolsFilterLoad', Date.now() - beforePoolsFiltered, MetricLoggerUnit.Milliseconds); const beforePoolsLoad = Date.now(); const [V2poolAccessor, V3poolAccessor] = await Promise.all([ v2poolProvider.getPools(V2tokenPairs, routingConfig), v3poolProvider.getPools(V3tokenPairs, routingConfig), ]); metric.putMetric('MixedPoolsLoad', Date.now() - beforePoolsLoad, MetricLoggerUnit.Milliseconds); /// @dev a bit tricky here since the original V2CandidateSelections object included pools that we may have dropped /// as part of the heuristic. We need to reconstruct a new object with the v3 pools too. const buildPoolsBySelection = (key) => { return [ ...buildV2Pools.filter(pool => V2candidatePools.selections[key].map(p => p.id).includes(pool.id)), ...V3candidatePools.selections[key], ]; }; const poolsBySelection = { protocol: Protocol.MIXED, selections: { topByBaseWithTokenIn: buildPoolsBySelection('topByBaseWithTokenIn'), topByBaseWithTokenOut: buildPoolsBySelection('topByBaseWithTokenOut'), topByDirectSwapPool: buildPoolsBySelection('topByDirectSwapPool'), topByEthQuoteTokenPool: buildPoolsBySelection('topByEthQuoteTokenPool'), topByTVL: buildPoolsBySelection('topByTVL'), topByTVLUsingTokenIn: buildPoolsBySelection('topByTVLUsingTokenIn'), topByTVLUsingTokenOut: buildPoolsBySelection('topByTVLUsingTokenOut'), topByTVLUsingTokenInSecondHops: buildPoolsBySelection('topByTVLUsingTokenInSecondHops'), topByTVLUsingTokenOutSecondHops: buildPoolsBySelection('topByTVLUsingTokenOutSecondHops'), }, }; return { V2poolAccessor, V3poolAccessor, candidatePools: poolsBySelection, subgraphPools, }; } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZ2V0LWNhbmRpZGF0ZS1wb29scy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3NyYy9yb3V0ZXJzL2FscGhhLXJvdXRlci9mdW5jdGlvbnMvZ2V0LWNhbmRpZGF0ZS1wb29scy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsT0FBTyxFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFDakQsT0FBTyxFQUFFLFFBQVEsRUFBRSxNQUFNLHFCQUFxQixDQUFDO0FBQy9DLE9BQU8sRUFBUyxTQUFTLEVBQUUsTUFBTSxtQkFBbUIsQ0FBQztBQUNyRCxPQUFPLEVBQUUsU0FBUyxFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFDNUMsT0FBTyxDQUFDLE1BQU0sUUFBUSxDQUFDO0FBTXZCLE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSx1QkFBdUIsRUFBRSxNQUFNLGVBQWUsQ0FBQztBQUMxRSxPQUFPLEVBQUUsY0FBYyxFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFDdkQsT0FBTyxFQUFFLEdBQUcsRUFBRSxNQUFNLG1CQUFtQixDQUFDO0FBQ3hDLE9BQU8sRUFBRSxNQUFNLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxzQkFBc0IsQ0FBQztBQUVoRSxPQUFPLEVBQUUsa0JBQWtCLEVBQUUsa0JBQWtCLEVBQUUsTUFBTSxlQUFlLENBQUM7QUF3RHZFLE1BQU0saUJBQWlCLEdBQXVDO0lBQzVELENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxFQUFFLENBQUMsdUJBQXVCLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxFQUFFLGtCQUFrQixDQUFDO0lBQ2pGLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxFQUFFLENBQUMsdUJBQXVCLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxFQUFFLGtCQUFrQixDQUFDO0NBQ2xGLENBQUM7QUFFRixNQUFNLHlCQUF5QjtJQUM3QixZQUNTLEtBQXFCLEVBQ1osV0FBbUI7UUFENUIsVUFBSyxHQUFM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