diff --git a/src/routers/alpha-router/functions/get-candidate-pools.ts b/src/routers/alpha-router/functions/get-candidate-pools.ts index 38f43f66d..86950134b 100644 --- a/src/routers/alpha-router/functions/get-candidate-pools.ts +++ b/src/routers/alpha-router/functions/get-candidate-pools.ts @@ -250,10 +250,32 @@ export async function getV3CandidatePools({ const beforePoolsFiltered = Date.now(); - const subgraphPoolsSorted = _(allPools) + // Only consider pools where neither tokens are in the blocked token list. + let filteredPools: V3SubgraphPool[] = allPools; + if (blockedTokenListProvider) { + filteredPools = []; + for (const pool of allPools) { + const token0InBlocklist = + await blockedTokenListProvider.getTokenByAddress(pool.token0.id); + const token1InBlocklist = + await blockedTokenListProvider.getTokenByAddress(pool.token1.id); + + if (token0InBlocklist || token1InBlocklist) { + continue; + } + + filteredPools.push(pool); + } + } + + const subgraphPoolsSorted = _(filteredPools) .sortBy((tokenListPool) => -tokenListPool.tvlUSD) .value(); + log.info( + `After filtering blocked tokens went from ${allPools.length} to ${subgraphPoolsSorted.length}.` + ); + const poolAddressesSoFar = new Set(); const addToAddressSet = (pools: V3SubgraphPool[]) => { _(pools) @@ -261,179 +283,66 @@ export async function getV3CandidatePools({ .forEach((poolAddress) => poolAddressesSoFar.add(poolAddress)); }; - const wrappedNativeAddress = WRAPPED_NATIVE_CURRENCY[chainId]?.address; - - const topByBaseWithTokenInMap: Map> = new Map(); - const topByBaseWithTokenOutMap: Map> = new Map(); - const baseTokens = baseTokensByChain[chainId] ?? []; - const baseTokensAddresses: Set = 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; - - const topByDirectSwapPools: V3SubgraphPool[] = []; - const topByEthQuoteTokenPool: V3SubgraphPool[] = []; - const topByTVLUsingTokenIn: V3SubgraphPool[] = []; - const topByTVLUsingTokenOut: V3SubgraphPool[] = []; - const topByTVL: V3SubgraphPool[] = []; - // 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) { - // Check if we have satisfied all the heuristics, if so, we can stop. - if ( - topByDirectSwapPools.length >= topNDirectSwaps && - topByBaseWithTokenInPoolsFound >= topNWithBaseToken && - topByBaseWithTokenOutPoolsFound >= topNWithBaseToken && - topByEthQuoteTokenPool.length >= 2 && - topByTVL.length >= topN && - topByTVLUsingTokenIn.length >= topNTokenInOut && - topByTVLUsingTokenOut.length >= topNTokenInOut - ) { - // We have satisfied all the heuristics, so we can stop. - break; - } - - // Only consider pools where neither tokens are in the blocked token list. - if (blockedTokenListProvider) { - const [token0InBlocklist, token1InBlocklist] = await Promise.all([ - blockedTokenListProvider.getTokenByAddress(subgraphPool.token0.id), - blockedTokenListProvider.getTokenByAddress(subgraphPool.token1.id) - ]); - - if (token0InBlocklist || token1InBlocklist) { - continue; - } - } - - if ( - topByDirectSwapPools.length < topNDirectSwaps && - ( - (subgraphPool.token0.id == tokenInAddress && subgraphPool.token1.id == tokenOutAddress) || - (subgraphPool.token0.id == tokenOutAddress && subgraphPool.token1.id == tokenInAddress) - ) - ) { - topByDirectSwapPools.push(subgraphPool); - continue; - } - - const tokenInToken0TopByBase = topByBaseWithTokenInMap.get(subgraphPool.token0.id); - if ( - topByBaseWithTokenInPoolsFound < topNWithBaseToken && - tokenInToken0TopByBase && - subgraphPool.token1.id == tokenInAddress - ) { - topByBaseWithTokenInPoolsFound += 1; - tokenInToken0TopByBase.pools.push(subgraphPool); - continue; - } - - const tokenInToken1TopByBase = topByBaseWithTokenInMap.get(subgraphPool.token1.id); - if ( - topByBaseWithTokenInPoolsFound < topNWithBaseToken && - tokenInToken1TopByBase && - subgraphPool.token0.id == tokenInAddress - ) { - topByBaseWithTokenInPoolsFound += 1; - tokenInToken1TopByBase.pools.push(subgraphPool); - continue; - } - - const tokenOutToken0TopByBase = topByBaseWithTokenOutMap.get(subgraphPool.token0.id); - if ( - topByBaseWithTokenOutPoolsFound < topNWithBaseToken && - tokenOutToken0TopByBase && - subgraphPool.token1.id == tokenOutAddress - ) { - topByBaseWithTokenOutPoolsFound += 1; - tokenOutToken0TopByBase.pools.push(subgraphPool); - continue; - } - - const tokenOutToken1TopByBase = topByBaseWithTokenInMap.get(subgraphPool.token1.id); - if ( - topByBaseWithTokenOutPoolsFound < topNWithBaseToken && - tokenOutToken1TopByBase && - subgraphPool.token0.id == tokenOutAddress - ) { - topByBaseWithTokenOutPoolsFound += 1; - tokenOutToken1TopByBase.pools.push(subgraphPool); - continue; - } - - // 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 - // there's no need to add more. - if ( - topByEthQuoteTokenPool.length < 2 && - ( - ( - WRAPPED_NATIVE_CURRENCY[chainId]?.symbol == WRAPPED_NATIVE_CURRENCY[ChainId.MAINNET]?.symbol && - tokenOut.symbol != 'WETH' && - tokenOut.symbol != 'WETH9' && - tokenOut.symbol != 'ETH' - ) || - ( - WRAPPED_NATIVE_CURRENCY[chainId]?.symbol == WMATIC_POLYGON.symbol && - tokenOut.symbol != 'MATIC' && - tokenOut.symbol != 'WMATIC' - ) - ) && - ( - routeType === TradeType.EXACT_INPUT && ( - (subgraphPool.token0.id == wrappedNativeAddress && subgraphPool.token1.id == tokenOutAddress) || - (subgraphPool.token1.id == wrappedNativeAddress && subgraphPool.token0.id == tokenOutAddress) - ) || - routeType === TradeType.EXACT_OUTPUT && ( - (subgraphPool.token0.id == wrappedNativeAddress && subgraphPool.token1.id == tokenInAddress) || - (subgraphPool.token1.id == wrappedNativeAddress && subgraphPool.token0.id == tokenInAddress) - ) - ) - ) { - topByEthQuoteTokenPool.push(subgraphPool); - continue; - } - - if (topByTVL.length < topN) { - topByTVL.push(subgraphPool); - continue; - } + const topByBaseWithTokenIn = _(baseTokens) + .flatMap((token: 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(); - if ( - topByTVLUsingTokenIn.length < topNTokenInOut && - (subgraphPool.token0.id == tokenInAddress || subgraphPool.token1.id == tokenInAddress) - ) { - topByTVLUsingTokenIn.push(subgraphPool); - continue; - } + const topByBaseWithTokenOut = _(baseTokens) + .flatMap((token: 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(); - if ( - topByTVLUsingTokenOut.length < topNTokenInOut && - (subgraphPool.token0.id == tokenOutAddress || subgraphPool.token1.id == tokenOutAddress) - ) { - topByTVLUsingTokenOut.push(subgraphPool); - continue; - } - } + 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(); - // Add DirectSwapPools if more than 0 were requested but none were found - if (topByDirectSwapPools.length == 0 && topNDirectSwaps > 0) { - const directSwapPools = _.map( + 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( @@ -456,151 +365,151 @@ export async function getV3CandidatePools({ }; } ); - topByDirectSwapPools.push(...directSwapPools); - } - - const topByBaseWithTokenIn: V3SubgraphPool[] = []; - for (const topByBaseWithTokenInSelection of topByBaseWithTokenInMap.values()) { - topByBaseWithTokenIn.push(...topByBaseWithTokenInSelection.pools); - } - - const topByBaseWithTokenOut: V3SubgraphPool[] = []; - for (const topByBaseWithTokenOutSelection of topByBaseWithTokenOutMap.values()) { - topByBaseWithTokenOut.push(...topByBaseWithTokenOutSelection.pools); } - // Add the addresses found so far to our address set - addToAddressSet(topByDirectSwapPools); - addToAddressSet(topByBaseWithTokenIn); - addToAddressSet(topByBaseWithTokenOut); - addToAddressSet(topByEthQuoteTokenPool); - addToAddressSet(topByTVLUsingTokenIn); - addToAddressSet(topByTVLUsingTokenOut); - addToAddressSet(topByTVL); + addToAddressSet(top2DirectSwapPool); - // Filtering step for second hops - const topByTVLUsingTokenInSecondHopsMap: Map> = new Map(); - const topByTVLUsingTokenOutSecondHopsMap: Map> = new Map(); - const tokenInSecondHopAddresses = topByTVLUsingTokenIn.map((pool) => - tokenInAddress == pool.token0.id ? pool.token1.id : pool.token0.id - ); - const tokenOutSecondHopAddresses = topByTVLUsingTokenOut.map((pool) => - tokenOutAddress == pool.token0.id ? pool.token1.id : pool.token0.id - ); + const wrappedNativeAddress = WRAPPED_NATIVE_CURRENCY[chainId]?.address; - for (const secondHopId of tokenInSecondHopAddresses) { - topByTVLUsingTokenInSecondHopsMap.set( - secondHopId, - new SubcategorySelectionPools([], topNSecondHopForTokenAddress?.get(secondHopId) ?? topNSecondHop) - ); - } - for (const secondHopId of tokenOutSecondHopAddresses) { - topByTVLUsingTokenOutSecondHopsMap.set( - secondHopId, - new SubcategorySelectionPools([], topNSecondHopForTokenAddress?.get(secondHopId) ?? topNSecondHop) - ); + // 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: V3SubgraphPool[] = []; + if ( + (WRAPPED_NATIVE_CURRENCY[chainId]?.symbol == + WRAPPED_NATIVE_CURRENCY[ChainId.MAINNET]?.symbol && + tokenOut.symbol != 'WETH' && + tokenOut.symbol != 'WETH9' && + tokenOut.symbol != 'ETH') || + (WRAPPED_NATIVE_CURRENCY[chainId]?.symbol == WMATIC_POLYGON.symbol && + tokenOut.symbol != 'MATIC' && + tokenOut.symbol != 'WMATIC') + ) { + 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(); } - for (const subgraphPool of subgraphPoolsSorted) { - 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; - } + addToAddressSet(top2EthQuoteTokenPool); - // Only consider pools where neither tokens are in the blocked token list. - if (blockedTokenListProvider) { - const [token0InBlocklist, token1InBlocklist] = await Promise.all([ - blockedTokenListProvider.getTokenByAddress(subgraphPool.token0.id), - blockedTokenListProvider.getTokenByAddress(subgraphPool.token1.id) - ]); - - if (token0InBlocklist || token1InBlocklist) { - continue; - } - } - - const tokenInToken0SecondHop = topByTVLUsingTokenInSecondHopsMap.get(subgraphPool.token0.id); - - if (tokenInToken0SecondHop && !tokenInToken0SecondHop.hasEnoughPools()) { - tokenInToken0SecondHop.pools.push(subgraphPool); - continue; - } + const topByTVL = _(subgraphPoolsSorted) + .filter((subgraphPool) => { + return !poolAddressesSoFar.has(subgraphPool.id); + }) + .slice(0, topN) + .value(); - const tokenInToken1SecondHop = topByTVLUsingTokenInSecondHopsMap.get(subgraphPool.token1.id); + addToAddressSet(topByTVL); - if (tokenInToken1SecondHop && !tokenInToken1SecondHop.hasEnoughPools()) { - tokenInToken1SecondHop.pools.push(subgraphPool); - continue; - } + const topByTVLUsingTokenIn = _(subgraphPoolsSorted) + .filter((subgraphPool) => { + return ( + !poolAddressesSoFar.has(subgraphPool.id) && + (subgraphPool.token0.id == tokenInAddress || + subgraphPool.token1.id == tokenInAddress) + ); + }) + .slice(0, topNTokenInOut) + .value(); - const tokenOutToken0SecondHop = topByTVLUsingTokenOutSecondHopsMap.get(subgraphPool.token0.id); + addToAddressSet(topByTVLUsingTokenIn); - if (tokenOutToken0SecondHop && !tokenOutToken0SecondHop.hasEnoughPools()) { - tokenOutToken0SecondHop.pools.push(subgraphPool); - continue; - } + const topByTVLUsingTokenOut = _(subgraphPoolsSorted) + .filter((subgraphPool) => { + return ( + !poolAddressesSoFar.has(subgraphPool.id) && + (subgraphPool.token0.id == tokenOutAddress || + subgraphPool.token1.id == tokenOutAddress) + ); + }) + .slice(0, topNTokenInOut) + .value(); - const tokenOutToken1SecondHop = topByTVLUsingTokenOutSecondHopsMap.get(subgraphPool.token1.id); + addToAddressSet(topByTVLUsingTokenOut); - if (tokenOutToken1SecondHop && !tokenOutToken1SecondHop.hasEnoughPools()) { - tokenOutToken1SecondHop.pools.push(subgraphPool); - continue; - } - } + const topByTVLUsingTokenInSecondHops = _(topByTVLUsingTokenIn) + .map((subgraphPool) => { + return tokenInAddress == subgraphPool.token0.id + ? subgraphPool.token1.id + : subgraphPool.token0.id; + }) + .flatMap((secondHopId: string) => { + return _(subgraphPoolsSorted) + .filter((subgraphPool) => { + return ( + !poolAddressesSoFar.has(subgraphPool.id) && + (subgraphPool.token0.id == secondHopId || + subgraphPool.token1.id == secondHopId) + ); + }) + .slice(0, topNSecondHopForTokenAddress?.get(secondHopId) ?? topNSecondHop) + .value(); + }) + .uniqBy((pool) => pool.id) + .value(); - const topByTVLUsingTokenInSecondHops: V3SubgraphPool[] = []; - for (const secondHopPools of topByTVLUsingTokenInSecondHopsMap.values()) { - topByTVLUsingTokenInSecondHops.push(...secondHopPools.pools); - } + addToAddressSet(topByTVLUsingTokenInSecondHops); - const topByTVLUsingTokenOutSecondHops: V3SubgraphPool[] = []; - for (const secondHopPools of topByTVLUsingTokenOutSecondHopsMap.values()) { - topByTVLUsingTokenOutSecondHops.push(...secondHopPools.pools); - } + const topByTVLUsingTokenOutSecondHops = _(topByTVLUsingTokenOut) + .map((subgraphPool) => { + return tokenOutAddress == subgraphPool.token0.id + ? subgraphPool.token1.id + : subgraphPool.token0.id; + }) + .flatMap((secondHopId: string) => { + return _(subgraphPoolsSorted) + .filter((subgraphPool) => { + return ( + !poolAddressesSoFar.has(subgraphPool.id) && + (subgraphPool.token0.id == secondHopId || + subgraphPool.token1.id == secondHopId) + ); + }) + .slice(0, topNSecondHopForTokenAddress?.get(secondHopId) ?? topNSecondHop) + .value(); + }) + .uniqBy((pool) => pool.id) + .value(); - addToAddressSet(topByTVLUsingTokenInSecondHops); addToAddressSet(topByTVLUsingTokenOutSecondHops); - const topPoolsByAllHeuristics = [ + const subgraphPools = _([ ...topByBaseWithTokenIn, ...topByBaseWithTokenOut, - ...topByDirectSwapPools, - ...topByEthQuoteTokenPool, + ...top2DirectSwapPool, + ...top2EthQuoteTokenPool, ...topByTVL, ...topByTVLUsingTokenIn, ...topByTVLUsingTokenOut, ...topByTVLUsingTokenInSecondHops, ...topByTVLUsingTokenOutSecondHops, - ]; - const subgraphPoolsSet: Set = new Set(topPoolsByAllHeuristics); - const subgraphPools = Array.from(subgraphPoolsSet); + ]) + .compact() + .uniqBy((pool) => pool.id) + .value(); - const tokenAddressesSet: Set = new Set(); - for (const pool of subgraphPools) { - tokenAddressesSet.add(pool.token0.id); - tokenAddressesSet.add(pool.token1.id); - } - const tokenAddresses = Array.from(tokenAddressesSet); + 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` @@ -626,8 +535,8 @@ export async function getV3CandidatePools({ topByTVLUsingTokenInSecondHops.map(printV3SubgraphPool), topByTVLUsingTokenOutSecondHops: topByTVLUsingTokenOutSecondHops.map(printV3SubgraphPool), - top2DirectSwap: topByDirectSwapPools.map(printV3SubgraphPool), - top2EthQuotePool: topByEthQuoteTokenPool.map(printV3SubgraphPool), + top2DirectSwap: top2DirectSwapPool.map(printV3SubgraphPool), + top2EthQuotePool: top2EthQuoteTokenPool.map(printV3SubgraphPool), }, `V3 Candidate Pools` ); @@ -688,8 +597,8 @@ export async function getV3CandidatePools({ selections: { topByBaseWithTokenIn, topByBaseWithTokenOut, - topByDirectSwapPool: topByDirectSwapPools, - topByEthQuoteTokenPool, + topByDirectSwapPool: top2DirectSwapPool, + topByEthQuoteTokenPool: top2EthQuoteTokenPool, topByTVL, topByTVLUsingTokenIn, topByTVLUsingTokenOut,