@sky-mavis/smart-order-router
Version:
Ronin Swap Smart Order Router
783 lines • 82.5 kB
JavaScript
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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