UNPKG

@sky-mavis/smart-order-router

Version:
804 lines 111 kB
import { ChainId, DEFAULT_ERC20, SWAP_ROUTER_02_ADDRESSES } from '@sky-mavis/katana-core'; import { BigNumber } from '@ethersproject/bignumber'; import { Protocol, SwapRouter, ZERO } from '@uniswap/router-sdk'; import { Fraction, TradeType } from '@uniswap/sdk-core'; import { Pool, Position, SqrtPriceMath, TickMath } from '@uniswap/v3-sdk'; import retry from 'async-retry'; import JSBI from 'jsbi'; import _ from 'lodash'; import NodeCache from 'node-cache'; import { CachedRoutes, CacheMode, CachingGasStationProvider, CachingTokenProviderWithFallback, CachingV2PoolProvider, CachingV2SubgraphProvider, CachingV3PoolProvider, CachingV3SubgraphProvider, EIP1559GasPriceProvider, LegacyGasPriceProvider, NodeJSCache, OnChainGasPriceProvider, OnChainQuoteProvider, StaticV2SubgraphProvider, StaticV3SubgraphProvider, SwapRouterProvider, TokenPropertiesProvider, UniswapMulticallProvider, URISubgraphProvider, V2QuoteProvider, V2SubgraphProviderWithFallBacks, V3SubgraphProviderWithFallBacks, } from '../../providers'; import { CachingTokenListProvider } from '../../providers/caching-token-list-provider'; import { PortionProvider } from '../../providers/portion-provider'; import { OnChainTokenFeeFetcher } from '../../providers/token-fee-fetcher'; import { TokenProvider } from '../../providers/token-provider'; import { TokenValidatorProvider } from '../../providers/token-validator-provider'; import { V2PoolProvider } from '../../providers/v2/pool-provider'; import { V3PoolProvider } from '../../providers/v3/pool-provider'; import { Erc20__factory } from '../../types/other/factories/Erc20__factory'; import { WRAPPED_NATIVE_CURRENCY } from '../../util'; import { CurrencyAmount } from '../../util/amounts'; import { ID_TO_CHAIN_ID, ID_TO_NETWORK_NAME, V2_SUPPORTED } from '../../util/chains'; import { getHighestLiquidityV3NativePool, getHighestLiquidityV3USDPool } from '../../util/gas-factory-helpers'; import { log } from '../../util/log'; import { buildSwapMethodParameters, buildTrade } from '../../util/methodParameters'; import { metric, MetricLoggerUnit } from '../../util/metric'; import { UNSUPPORTED_TOKENS } from '../../util/unsupported-tokens'; import { SwapToRatioStatus, } from '../router'; import { DEFAULT_ROUTING_CONFIG_BY_CHAIN } from './config'; import { getBestSwapRoute } from './functions/best-swap-route'; import { calculateRatioAmountIn } from './functions/calculate-ratio-amount-in'; import { getV2CandidatePools, getV3CandidatePools, } from './functions/get-candidate-pools'; import { MixedRouteHeuristicGasModelFactory } from './gas-models/mixedRoute/mixed-route-heuristic-gas-model'; import { V2HeuristicGasModelFactory } from './gas-models/v2/v2-heuristic-gas-model'; import { NATIVE_OVERHEAD } from './gas-models/v3/gas-costs'; import { V3HeuristicGasModelFactory } from './gas-models/v3/v3-heuristic-gas-model'; import { MixedQuoter, V2Quoter, V3Quoter } from './quoters'; const createTokenInfo = () => { const tokenList = []; Object.entries(DEFAULT_ERC20).map(([chainId, tokens]) => { Object.values(tokens).map(token => { var _a, _c; tokenList.push({ chainId: parseInt(chainId), address: token.address, name: (_a = token.name) !== null && _a !== void 0 ? _a : '', symbol: (_c = token.symbol) !== null && _c !== void 0 ? _c : '', decimals: token.decimals, }); }); }); return tokenList; }; const DEFAULT_TOKEN_LIST = { name: 'Katana Default', timestamp: '2024-10-09T04:22:40.694Z', version: { major: 0, minor: 0, patch: 1, }, tags: {}, keywords: ['katana', 'default'], tokens: createTokenInfo(), }; export class MapWithLowerCaseKey extends Map { set(key, value) { return super.set(key.toLowerCase(), value); } } export class LowerCaseStringArray extends Array { constructor(...items) { // Convert all items to lowercase before calling the parent constructor super(...items.map(item => item.toLowerCase())); } } export class AlphaRouter { constructor({ chainId, provider, multicall2Provider, v3PoolProvider, onChainQuoteProvider, v2PoolProvider, v2QuoteProvider, v2SubgraphProvider, tokenProvider, blockedTokenListProvider, v3SubgraphProvider, gasPriceProvider, v3GasModelFactory, v2GasModelFactory, mixedRouteGasModelFactory, swapRouterProvider, tokenValidatorProvider, simulator, routeCachingProvider, tokenPropertiesProvider, portionProvider, v2Supported, }) { this.chainId = chainId; this.provider = provider; this.multicall2Provider = multicall2Provider !== null && multicall2Provider !== void 0 ? multicall2Provider : new UniswapMulticallProvider(chainId, provider, 375000); this.v3PoolProvider = v3PoolProvider !== null && v3PoolProvider !== void 0 ? v3PoolProvider : new CachingV3PoolProvider(this.chainId, new V3PoolProvider(ID_TO_CHAIN_ID(chainId), this.multicall2Provider), new NodeJSCache(new NodeCache({ stdTTL: 360, useClones: false }))); this.simulator = simulator; this.routeCachingProvider = routeCachingProvider; if (onChainQuoteProvider) { this.onChainQuoteProvider = onChainQuoteProvider; } else { this.onChainQuoteProvider = new OnChainQuoteProvider({ chainId, provider, multicall2Provider: this.multicall2Provider, }); } if (tokenValidatorProvider) { this.tokenValidatorProvider = tokenValidatorProvider; } else if (this.chainId === ChainId.mainnet) { this.tokenValidatorProvider = new TokenValidatorProvider(this.chainId, this.multicall2Provider, new NodeJSCache(new NodeCache({ stdTTL: 30000, useClones: false }))); } if (tokenPropertiesProvider) { this.tokenPropertiesProvider = tokenPropertiesProvider; } else { this.tokenPropertiesProvider = new TokenPropertiesProvider(this.chainId, new NodeJSCache(new NodeCache({ stdTTL: 86400, useClones: false })), new OnChainTokenFeeFetcher(this.chainId, provider)); } this.v2PoolProvider = v2PoolProvider !== null && v2PoolProvider !== void 0 ? v2PoolProvider : new CachingV2PoolProvider(chainId, new V2PoolProvider(chainId, this.multicall2Provider, this.tokenPropertiesProvider), new NodeJSCache(new NodeCache({ stdTTL: 60, useClones: false }))); this.v2QuoteProvider = v2QuoteProvider !== null && v2QuoteProvider !== void 0 ? v2QuoteProvider : new V2QuoteProvider(); this.blockedTokenListProvider = blockedTokenListProvider !== null && blockedTokenListProvider !== void 0 ? blockedTokenListProvider : new CachingTokenListProvider(chainId, UNSUPPORTED_TOKENS, new NodeJSCache(new NodeCache({ stdTTL: 3600, useClones: false }))); this.tokenProvider = tokenProvider !== null && tokenProvider !== void 0 ? tokenProvider : new CachingTokenProviderWithFallback(chainId, new NodeJSCache(new NodeCache({ stdTTL: 3600, useClones: false })), new CachingTokenListProvider(chainId, DEFAULT_TOKEN_LIST, new NodeJSCache(new NodeCache({ stdTTL: 3600, useClones: false }))), new TokenProvider(chainId, this.multicall2Provider)); this.portionProvider = portionProvider !== null && portionProvider !== void 0 ? portionProvider : new PortionProvider(); const chainName = ID_TO_NETWORK_NAME(chainId); // ipfs urls in the following format: `https://cloudflare-ipfs.com/ipns/api.uniswap.org/v1/pools/${protocol}/${chainName}.json`; if (v2SubgraphProvider) { this.v2SubgraphProvider = v2SubgraphProvider; } else { this.v2SubgraphProvider = new V2SubgraphProviderWithFallBacks([ new CachingV2SubgraphProvider(chainId, new URISubgraphProvider(chainId, `https://cloudflare-ipfs.com/ipns/api.uniswap.org/v1/pools/v2/${chainName}.json`, undefined, 0), new NodeJSCache(new NodeCache({ stdTTL: 300, useClones: false }))), new StaticV2SubgraphProvider(chainId), ]); } if (v3SubgraphProvider) { this.v3SubgraphProvider = v3SubgraphProvider; } else { this.v3SubgraphProvider = new V3SubgraphProviderWithFallBacks([ new CachingV3SubgraphProvider(chainId, new URISubgraphProvider(chainId, `https://cloudflare-ipfs.com/ipns/api.uniswap.org/v1/pools/v3/${chainName}.json`, undefined, 0), new NodeJSCache(new NodeCache({ stdTTL: 300, useClones: false }))), new StaticV3SubgraphProvider(chainId, this.v3PoolProvider), ]); } const gasPriceProviderInstance = new OnChainGasPriceProvider(chainId, new EIP1559GasPriceProvider(this.provider), new LegacyGasPriceProvider(this.provider)); this.gasPriceProvider = gasPriceProvider !== null && gasPriceProvider !== void 0 ? gasPriceProvider : new CachingGasStationProvider(chainId, gasPriceProviderInstance, new NodeJSCache(new NodeCache({ stdTTL: 7, useClones: false }))); this.v3GasModelFactory = v3GasModelFactory !== null && v3GasModelFactory !== void 0 ? v3GasModelFactory : new V3HeuristicGasModelFactory(); this.v2GasModelFactory = v2GasModelFactory !== null && v2GasModelFactory !== void 0 ? v2GasModelFactory : new V2HeuristicGasModelFactory(); this.mixedRouteGasModelFactory = mixedRouteGasModelFactory !== null && mixedRouteGasModelFactory !== void 0 ? mixedRouteGasModelFactory : new MixedRouteHeuristicGasModelFactory(); this.swapRouterProvider = swapRouterProvider !== null && swapRouterProvider !== void 0 ? swapRouterProvider : new SwapRouterProvider(this.multicall2Provider, this.chainId); // Initialize the Quoters. // Quoters are an abstraction encapsulating the business logic of fetching routes and quotes. this.v2Quoter = new V2Quoter(this.v2SubgraphProvider, this.v2PoolProvider, this.v2QuoteProvider, this.v2GasModelFactory, this.tokenProvider, this.chainId, this.blockedTokenListProvider, this.tokenValidatorProvider); this.v3Quoter = new V3Quoter(this.v3SubgraphProvider, this.v3PoolProvider, this.onChainQuoteProvider, this.tokenProvider, this.chainId, this.blockedTokenListProvider, this.tokenValidatorProvider); this.mixedQuoter = new MixedQuoter(this.v3SubgraphProvider, this.v3PoolProvider, this.v2SubgraphProvider, this.v2PoolProvider, this.onChainQuoteProvider, this.tokenProvider, this.chainId, this.blockedTokenListProvider, this.tokenValidatorProvider); this.v2Supported = v2Supported !== null && v2Supported !== void 0 ? v2Supported : V2_SUPPORTED; } async routeToRatio(token0Balance, token1Balance, position, swapAndAddConfig, swapAndAddOptions, routingConfig = DEFAULT_ROUTING_CONFIG_BY_CHAIN(this.chainId)) { if (token1Balance.currency.wrapped.sortsBefore(token0Balance.currency.wrapped)) { [token0Balance, token1Balance] = [token1Balance, token0Balance]; } let preSwapOptimalRatio = this.calculateOptimalRatio(position, position.pool.sqrtRatioX96, true); // set up parameters according to which token will be swapped let zeroForOne; if (position.pool.tickCurrent > position.tickUpper) { zeroForOne = true; } else if (position.pool.tickCurrent < position.tickLower) { zeroForOne = false; } else { zeroForOne = new Fraction(token0Balance.quotient, token1Balance.quotient).greaterThan(preSwapOptimalRatio); if (!zeroForOne) preSwapOptimalRatio = preSwapOptimalRatio.invert(); } const [inputBalance, outputBalance] = zeroForOne ? [token0Balance, token1Balance] : [token1Balance, token0Balance]; let optimalRatio = preSwapOptimalRatio; let postSwapTargetPool = position.pool; let exchangeRate = zeroForOne ? position.pool.token0Price : position.pool.token1Price; let swap = null; let ratioAchieved = false; let n = 0; // iterate until we find a swap with a sufficient ratio or return null while (!ratioAchieved) { n++; if (n > swapAndAddConfig.maxIterations) { log.info('max iterations exceeded'); return { status: SwapToRatioStatus.NO_ROUTE_FOUND, error: 'max iterations exceeded', }; } const amountToSwap = calculateRatioAmountIn(optimalRatio, exchangeRate, inputBalance, outputBalance); if (amountToSwap.equalTo(0)) { log.info(`no swap needed: amountToSwap = 0`); return { status: SwapToRatioStatus.NO_SWAP_NEEDED, }; } swap = await this.route(amountToSwap, outputBalance.currency, TradeType.EXACT_INPUT, undefined, { ...DEFAULT_ROUTING_CONFIG_BY_CHAIN(this.chainId), ...routingConfig, /// @dev We do not want to query for mixedRoutes for routeToRatio as they are not supported /// [Protocol.V3, Protocol.V2] will make sure we only query for V3 and V2 protocols: [Protocol.V3, Protocol.V2], }); if (!swap) { log.info('no route found from this.route()'); return { status: SwapToRatioStatus.NO_ROUTE_FOUND, error: 'no route found', }; } const inputBalanceUpdated = inputBalance.subtract(swap.trade.inputAmount); const outputBalanceUpdated = outputBalance.add(swap.trade.outputAmount); const newRatio = inputBalanceUpdated.divide(outputBalanceUpdated); let targetPoolPriceUpdate; swap.route.forEach(route => { if (route.protocol === Protocol.V3) { const v3Route = route; v3Route.route.pools.forEach((pool, i) => { if (pool.token0.equals(position.pool.token0) && pool.token1.equals(position.pool.token1) && pool.fee === position.pool.fee) { targetPoolPriceUpdate = JSBI.BigInt(v3Route.sqrtPriceX96AfterList[i].toString()); optimalRatio = this.calculateOptimalRatio(position, JSBI.BigInt(targetPoolPriceUpdate.toString()), zeroForOne); } }); } }); if (!targetPoolPriceUpdate) { optimalRatio = preSwapOptimalRatio; } ratioAchieved = newRatio.equalTo(optimalRatio) || this.absoluteValue(newRatio.asFraction.divide(optimalRatio).subtract(1)).lessThan(swapAndAddConfig.ratioErrorTolerance); if (ratioAchieved && targetPoolPriceUpdate) { postSwapTargetPool = new Pool(position.pool.token0, position.pool.token1, position.pool.fee, targetPoolPriceUpdate, position.pool.liquidity, TickMath.getTickAtSqrtRatio(targetPoolPriceUpdate), position.pool.tickDataProvider); } exchangeRate = swap.trade.outputAmount.divide(swap.trade.inputAmount); log.info({ exchangeRate: exchangeRate.asFraction.toFixed(18), optimalRatio: optimalRatio.asFraction.toFixed(18), newRatio: newRatio.asFraction.toFixed(18), inputBalanceUpdated: inputBalanceUpdated.asFraction.toFixed(18), outputBalanceUpdated: outputBalanceUpdated.asFraction.toFixed(18), ratioErrorTolerance: swapAndAddConfig.ratioErrorTolerance.toFixed(18), iterationN: n.toString(), }, 'QuoteToRatio Iteration Parameters'); if (exchangeRate.equalTo(0)) { log.info('exchangeRate to 0'); return { status: SwapToRatioStatus.NO_ROUTE_FOUND, error: 'insufficient liquidity to swap to optimal ratio', }; } } if (!swap) { return { status: SwapToRatioStatus.NO_ROUTE_FOUND, error: 'no route found', }; } let methodParameters; if (swapAndAddOptions) { methodParameters = await this.buildSwapAndAddMethodParameters(swap.trade, swapAndAddOptions, { initialBalanceTokenIn: inputBalance, initialBalanceTokenOut: outputBalance, preLiquidityPosition: position, }); } return { status: SwapToRatioStatus.SUCCESS, result: { ...swap, methodParameters, optimalRatio, postSwapTargetPool }, }; } /** * @inheritdoc IRouter */ async route(amount, quoteCurrency, tradeType, swapConfig, partialRoutingConfig = {}) { var _a, _c, _d, _e, _f, _g; const originalAmount = amount; const { currencyIn, currencyOut } = this.determineCurrencyInOutFromTradeType(tradeType, amount, quoteCurrency); const tokenIn = currencyIn.wrapped; const tokenOut = currencyOut.wrapped; const tokenOutProperties = await this.tokenPropertiesProvider.getTokensProperties([tokenOut], partialRoutingConfig); const buyFeeBps = (_c = (_a = tokenOutProperties[tokenOut.address.toLowerCase()]) === null || _a === void 0 ? void 0 : _a.tokenFeeResult) === null || _c === void 0 ? void 0 : _c.buyFeeBps; const tokenOutHasFot = buyFeeBps && buyFeeBps.gt(0); if (tradeType === TradeType.EXACT_OUTPUT) { const portionAmount = this.portionProvider.getPortionAmount(amount, tradeType, tokenOutHasFot, swapConfig); if (portionAmount && portionAmount.greaterThan(ZERO)) { // In case of exact out swap, before we route, we need to make sure that the // token out amount accounts for flat portion, and token in amount after the best swap route contains the token in equivalent of portion. // In other words, in case a pool's LP fee bps is lower than the portion bps (0.01%/0.05% for v3), a pool can go insolvency. // This is because instead of the swapper being responsible for the portion, // the pool instead gets responsible for the portion. // The addition below avoids that situation. amount = amount.add(portionAmount); } } metric.setProperty('chainId', this.chainId); metric.setProperty('pair', `${tokenIn.symbol}/${tokenOut.symbol}`); metric.setProperty('tokenIn', tokenIn.address); metric.setProperty('tokenOut', tokenOut.address); metric.setProperty('tradeType', tradeType === TradeType.EXACT_INPUT ? 'ExactIn' : 'ExactOut'); metric.putMetric(`QuoteRequestedForChain${this.chainId}`, 1, MetricLoggerUnit.Count); // Get a block number to specify in all our calls. Ensures data we fetch from chain is // from the same block. const blockNumber = (_d = partialRoutingConfig.blockNumber) !== null && _d !== void 0 ? _d : this.getBlockNumberPromise(); const routingConfig = _.merge({ // These settings could be changed by the partialRoutingConfig useCachedRoutes: true, writeToCachedRoutes: true, optimisticCachedRoutes: false, }, DEFAULT_ROUTING_CONFIG_BY_CHAIN(this.chainId), partialRoutingConfig, { blockNumber }); if (routingConfig.debugRouting) { log.warn(`Finalized routing config is ${JSON.stringify(routingConfig)}`); } const gasPriceWei = await this.getGasPriceWei(await blockNumber, await partialRoutingConfig.blockNumber); const quoteToken = quoteCurrency.wrapped; // const gasTokenAccessor = await this.tokenProvider.getTokens([routingConfig.gasToken!]); const gasToken = routingConfig.gasToken ? (await this.tokenProvider.getTokens([routingConfig.gasToken])).getTokenByAddress(routingConfig.gasToken) : undefined; const providerConfig = { ...routingConfig, blockNumber, additionalGasOverhead: NATIVE_OVERHEAD(amount.currency, quoteCurrency), gasToken, }; const { v2GasModel: v2GasModel, v3GasModel: v3GasModel, mixedRouteGasModel: mixedRouteGasModel, } = await this.getGasModels(gasPriceWei, amount.currency.wrapped, quoteToken, providerConfig); // Create a Set to sanitize the protocols input, a Set of undefined becomes an empty set, // Then create an Array from the values of that Set. const protocols = Array.from(new Set(routingConfig.protocols).values()); const cacheMode = (_e = routingConfig.overwriteCacheMode) !== null && _e !== void 0 ? _e : (await ((_f = this.routeCachingProvider) === null || _f === void 0 ? void 0 : _f.getCacheMode(this.chainId, amount, quoteToken, tradeType, protocols))); // Fetch CachedRoutes let cachedRoutes; if (routingConfig.useCachedRoutes && cacheMode !== CacheMode.Darkmode) { cachedRoutes = await ((_g = this.routeCachingProvider) === null || _g === void 0 ? void 0 : _g.getCachedRoute(this.chainId, amount, quoteToken, tradeType, protocols, await blockNumber, routingConfig.optimisticCachedRoutes)); } metric.putMetric(routingConfig.useCachedRoutes ? 'GetQuoteUsingCachedRoutes' : 'GetQuoteNotUsingCachedRoutes', 1, MetricLoggerUnit.Count); if (cacheMode && routingConfig.useCachedRoutes && cacheMode !== CacheMode.Darkmode && !cachedRoutes) { metric.putMetric(`GetCachedRoute_miss_${cacheMode}`, 1, MetricLoggerUnit.Count); log.info({ tokenIn: tokenIn.symbol, tokenInAddress: tokenIn.address, tokenOut: tokenOut.symbol, tokenOutAddress: tokenOut.address, cacheMode, amount: amount.toExact(), chainId: this.chainId, tradeType: this.tradeTypeStr(tradeType), }, `GetCachedRoute miss ${cacheMode} for ${this.tokenPairSymbolTradeTypeChainId(tokenIn, tokenOut, tradeType)}`); } else if (cachedRoutes && routingConfig.useCachedRoutes) { metric.putMetric(`GetCachedRoute_hit_${cacheMode}`, 1, MetricLoggerUnit.Count); log.info({ tokenIn: tokenIn.symbol, tokenInAddress: tokenIn.address, tokenOut: tokenOut.symbol, tokenOutAddress: tokenOut.address, cacheMode, amount: amount.toExact(), chainId: this.chainId, tradeType: this.tradeTypeStr(tradeType), }, `GetCachedRoute hit ${cacheMode} for ${this.tokenPairSymbolTradeTypeChainId(tokenIn, tokenOut, tradeType)}`); } let swapRouteFromCachePromise = Promise.resolve(null); if (cachedRoutes) { swapRouteFromCachePromise = this.getSwapRouteFromCache(cachedRoutes, await blockNumber, amount, quoteToken, tradeType, routingConfig, v3GasModel, mixedRouteGasModel, gasPriceWei, swapConfig); } let swapRouteFromChainPromise = Promise.resolve(null); if (!cachedRoutes || cacheMode !== CacheMode.Livemode) { swapRouteFromChainPromise = this.getSwapRouteFromChain(amount, tokenIn, tokenOut, protocols, quoteToken, tradeType, routingConfig, v3GasModel, mixedRouteGasModel, gasPriceWei, v2GasModel, swapConfig); } const [swapRouteFromCache, swapRouteFromChain] = await Promise.all([ swapRouteFromCachePromise, swapRouteFromChainPromise, ]); let swapRouteRaw; let hitsCachedRoute = false; if (cacheMode === CacheMode.Livemode && swapRouteFromCache) { log.info(`CacheMode is ${cacheMode}, and we are using swapRoute from cache`); hitsCachedRoute = true; swapRouteRaw = swapRouteFromCache; } else { log.info(`CacheMode is ${cacheMode}, and we are using materialized swapRoute`); swapRouteRaw = swapRouteFromChain; } if (cacheMode === CacheMode.Tapcompare && swapRouteFromCache && swapRouteFromChain) { const quoteDiff = swapRouteFromChain.quote.subtract(swapRouteFromCache.quote); const quoteGasAdjustedDiff = swapRouteFromChain.quoteGasAdjusted.subtract(swapRouteFromCache.quoteGasAdjusted); const gasUsedDiff = swapRouteFromChain.estimatedGasUsed.sub(swapRouteFromCache.estimatedGasUsed); // Only log if quoteDiff is different from 0, or if quoteGasAdjustedDiff and gasUsedDiff are both different from 0 if (!quoteDiff.equalTo(0) || !(quoteGasAdjustedDiff.equalTo(0) || gasUsedDiff.eq(0))) { // Calculates the percentage of the difference with respect to the quoteFromChain (not from cache) const misquotePercent = quoteGasAdjustedDiff.divide(swapRouteFromChain.quoteGasAdjusted).multiply(100); metric.putMetric(`TapcompareCachedRoute_quoteGasAdjustedDiffPercent`, Number(misquotePercent.toExact()), MetricLoggerUnit.Percent); log.warn({ quoteFromChain: swapRouteFromChain.quote.toExact(), quoteFromCache: swapRouteFromCache.quote.toExact(), quoteDiff: quoteDiff.toExact(), quoteGasAdjustedFromChain: swapRouteFromChain.quoteGasAdjusted.toExact(), quoteGasAdjustedFromCache: swapRouteFromCache.quoteGasAdjusted.toExact(), quoteGasAdjustedDiff: quoteGasAdjustedDiff.toExact(), gasUsedFromChain: swapRouteFromChain.estimatedGasUsed.toString(), gasUsedFromCache: swapRouteFromCache.estimatedGasUsed.toString(), gasUsedDiff: gasUsedDiff.toString(), routesFromChain: swapRouteFromChain.routes.toString(), routesFromCache: swapRouteFromCache.routes.toString(), amount: amount.toExact(), originalAmount: cachedRoutes === null || cachedRoutes === void 0 ? void 0 : cachedRoutes.originalAmount, pair: this.tokenPairSymbolTradeTypeChainId(tokenIn, tokenOut, tradeType), blockNumber, }, `Comparing quotes between Chain and Cache for ${this.tokenPairSymbolTradeTypeChainId(tokenIn, tokenOut, tradeType)}`); } } if (!swapRouteRaw) { return null; } const { quote, quoteGasAdjusted, estimatedGasUsed, routes: routeAmounts, estimatedGasUsedQuoteToken, estimatedGasUsedUSD, estimatedGasUsedGasToken, } = swapRouteRaw; if (this.routeCachingProvider && routingConfig.writeToCachedRoutes && cacheMode !== CacheMode.Darkmode && swapRouteFromChain) { // Generate the object to be cached const routesToCache = CachedRoutes.fromRoutesWithValidQuotes(swapRouteFromChain.routes, this.chainId, tokenIn, tokenOut, protocols.sort(), // sort it for consistency in the order of the protocols. await blockNumber, tradeType, amount.toExact()); if (routesToCache) { // Attempt to insert the entry in cache. This is fire and forget promise. // The catch method will prevent any exception from blocking the normal code execution. this.routeCachingProvider .setCachedRoute(routesToCache, amount) .then(success => { const status = success ? 'success' : 'rejected'; metric.putMetric(`SetCachedRoute_${status}`, 1, MetricLoggerUnit.Count); }) .catch(reason => { log.error({ reason: reason, tokenPair: this.tokenPairSymbolTradeTypeChainId(tokenIn, tokenOut, tradeType), }, `SetCachedRoute failure`); metric.putMetric(`SetCachedRoute_failure`, 1, MetricLoggerUnit.Count); }); } else { metric.putMetric(`SetCachedRoute_unnecessary`, 1, MetricLoggerUnit.Count); } } metric.putMetric(`QuoteFoundForChain${this.chainId}`, 1, MetricLoggerUnit.Count); // Build Trade object that represents the optimal swap. const trade = buildTrade(currencyIn, currencyOut, tradeType, routeAmounts); let methodParameters; // If user provided recipient, deadline etc. we also generate the calldata required to execute // the swap and return it too. if (swapConfig) { methodParameters = buildSwapMethodParameters(trade, swapConfig, this.chainId); } const tokenOutAmount = tradeType === TradeType.EXACT_OUTPUT ? originalAmount // we need to pass in originalAmount instead of amount, because amount already added portionAmount in case of exact out swap : quote; const portionAmount = this.portionProvider.getPortionAmount(tokenOutAmount, tradeType, tokenOutHasFot, swapConfig); const portionQuoteAmount = this.portionProvider.getPortionQuoteAmount(tradeType, quote, amount, // we need to pass in amount instead of originalAmount here, because amount here needs to add the portion for exact out portionAmount); // we need to correct quote and quote gas adjusted for exact output when portion is part of the exact out swap const correctedQuote = this.portionProvider.getQuote(tradeType, quote, portionQuoteAmount); const correctedQuoteGasAdjusted = this.portionProvider.getQuoteGasAdjusted(tradeType, quoteGasAdjusted, portionQuoteAmount); const quoteGasAndPortionAdjusted = this.portionProvider.getQuoteGasAndPortionAdjusted(tradeType, quoteGasAdjusted, portionAmount); const swapRoute = { quote: correctedQuote, quoteGasAdjusted: correctedQuoteGasAdjusted, estimatedGasUsed, estimatedGasUsedQuoteToken, estimatedGasUsedUSD, estimatedGasUsedGasToken, gasPriceWei, route: routeAmounts, trade, methodParameters, blockNumber: BigNumber.from(await blockNumber), hitsCachedRoute: hitsCachedRoute, portionAmount: portionAmount, quoteGasAndPortionAdjusted: quoteGasAndPortionAdjusted, }; if (swapConfig && swapConfig.simulate && methodParameters && methodParameters.calldata) { if (!this.simulator) { throw new Error('Simulator not initialized!'); } log.info(JSON.stringify({ swapConfig, methodParameters, providerConfig }, null, 2), `Starting simulation`); const fromAddress = swapConfig.simulate.fromAddress; const beforeSimulate = Date.now(); const swapRouteWithSimulation = await this.simulator.simulate(fromAddress, swapConfig, swapRoute, amount, // Quote will be in WETH even if quoteCurrency is ETH // So we init a new CurrencyAmount object here CurrencyAmount.fromRawAmount(quoteCurrency, quote.quotient.toString()), providerConfig); metric.putMetric('SimulateTransaction', Date.now() - beforeSimulate, MetricLoggerUnit.Milliseconds); return swapRouteWithSimulation; } return swapRoute; } async getSwapRouteFromCache(cachedRoutes, blockNumber, amount, quoteToken, tradeType, routingConfig, v3GasModel, mixedRouteGasModel, gasPriceWei, swapConfig) { log.info({ protocols: cachedRoutes.protocolsCovered, tradeType: cachedRoutes.tradeType, cachedBlockNumber: cachedRoutes.blockNumber, quoteBlockNumber: blockNumber, }, 'Routing across CachedRoute'); const quotePromises = []; const v3Routes = cachedRoutes.routes.filter(route => route.protocol === Protocol.V3); const v2Routes = cachedRoutes.routes.filter(route => route.protocol === Protocol.V2); const mixedRoutes = cachedRoutes.routes.filter(route => route.protocol === Protocol.MIXED); let percents; let amounts; if (cachedRoutes.routes.length > 1) { // If we have more than 1 route, we will quote the different percents for it, following the regular process [percents, amounts] = this.getAmountDistribution(amount, routingConfig); } else if (cachedRoutes.routes.length == 1) { [percents, amounts] = [[100], [amount]]; } else { // In this case this means that there's no route, so we return null return Promise.resolve(null); } if (v3Routes.length > 0) { const v3RoutesFromCache = v3Routes.map(cachedRoute => cachedRoute.route); metric.putMetric('SwapRouteFromCache_V3_GetQuotes_Request', 1, MetricLoggerUnit.Count); const beforeGetQuotes = Date.now(); quotePromises.push(this.v3Quoter .getQuotes(v3RoutesFromCache, amounts, percents, quoteToken, tradeType, routingConfig, undefined, v3GasModel) .then(result => { metric.putMetric(`SwapRouteFromCache_V3_GetQuotes_Load`, Date.now() - beforeGetQuotes, MetricLoggerUnit.Milliseconds); return result; })); } if (v2Routes.length > 0) { const v2RoutesFromCache = v2Routes.map(cachedRoute => cachedRoute.route); metric.putMetric('SwapRouteFromCache_V2_GetQuotes_Request', 1, MetricLoggerUnit.Count); const beforeGetQuotes = Date.now(); quotePromises.push(this.v2Quoter .refreshRoutesThenGetQuotes(cachedRoutes.tokenIn, cachedRoutes.tokenOut, v2RoutesFromCache, amounts, percents, quoteToken, tradeType, routingConfig, gasPriceWei) .then(result => { metric.putMetric(`SwapRouteFromCache_V2_GetQuotes_Load`, Date.now() - beforeGetQuotes, MetricLoggerUnit.Milliseconds); return result; })); } if (mixedRoutes.length > 0) { const mixedRoutesFromCache = mixedRoutes.map(cachedRoute => cachedRoute.route); metric.putMetric('SwapRouteFromCache_Mixed_GetQuotes_Request', 1, MetricLoggerUnit.Count); const beforeGetQuotes = Date.now(); quotePromises.push(this.mixedQuoter .getQuotes(mixedRoutesFromCache, amounts, percents, quoteToken, tradeType, routingConfig, undefined, mixedRouteGasModel) .then(result => { metric.putMetric(`SwapRouteFromCache_Mixed_GetQuotes_Load`, Date.now() - beforeGetQuotes, MetricLoggerUnit.Milliseconds); return result; })); } const getQuotesResults = await Promise.all(quotePromises); const allRoutesWithValidQuotes = _.flatMap(getQuotesResults, quoteResult => quoteResult.routesWithValidQuotes); return getBestSwapRoute(amount, percents, allRoutesWithValidQuotes, tradeType, this.chainId, routingConfig, this.portionProvider, swapConfig); } async getSwapRouteFromChain(amount, tokenIn, tokenOut, protocols, quoteToken, tradeType, routingConfig, v3GasModel, mixedRouteGasModel, gasPriceWei, v2GasModel, swapConfig) { var _a; // Generate our distribution of amounts, i.e. fractions of the input amount. // We will get quotes for fractions of the input amount for different routes, then // combine to generate split routes. const [percents, amounts] = this.getAmountDistribution(amount, routingConfig); const noProtocolsSpecified = protocols.length === 0; const v3ProtocolSpecified = protocols.includes(Protocol.V3); const v2ProtocolSpecified = protocols.includes(Protocol.V2); const v2SupportedInChain = (_a = this.v2Supported) === null || _a === void 0 ? void 0 : _a.includes(this.chainId); const shouldQueryMixedProtocol = protocols.includes(Protocol.MIXED) || (noProtocolsSpecified && v2SupportedInChain); const mixedProtocolAllowed = tradeType === TradeType.EXACT_INPUT; const beforeGetCandidates = Date.now(); let v3CandidatePoolsPromise = Promise.resolve(undefined); if (v3ProtocolSpecified || noProtocolsSpecified || (shouldQueryMixedProtocol && mixedProtocolAllowed)) { v3CandidatePoolsPromise = getV3CandidatePools({ tokenIn, tokenOut, tokenProvider: this.tokenProvider, blockedTokenListProvider: this.blockedTokenListProvider, poolProvider: this.v3PoolProvider, routeType: tradeType, subgraphProvider: this.v3SubgraphProvider, routingConfig, chainId: this.chainId, }).then(candidatePools => { metric.putMetric('GetV3CandidatePools', Date.now() - beforeGetCandidates, MetricLoggerUnit.Milliseconds); return candidatePools; }); } let v2CandidatePoolsPromise = Promise.resolve(undefined); if ((v2SupportedInChain && (v2ProtocolSpecified || noProtocolsSpecified)) || (shouldQueryMixedProtocol && mixedProtocolAllowed)) { // Fetch all the pools that we will consider routing via. There are thousands // of pools, so we filter them to a set of candidate pools that we expect will // result in good prices. v2CandidatePoolsPromise = getV2CandidatePools({ tokenIn, tokenOut, tokenProvider: this.tokenProvider, blockedTokenListProvider: this.blockedTokenListProvider, poolProvider: this.v2PoolProvider, routeType: tradeType, subgraphProvider: this.v2SubgraphProvider, routingConfig, chainId: this.chainId, }).then(candidatePools => { metric.putMetric('GetV2CandidatePools', Date.now() - beforeGetCandidates, MetricLoggerUnit.Milliseconds); return candidatePools; }); } const quotePromises = []; // Maybe Quote V3 - if V3 is specified, or no protocol is specified if (v3ProtocolSpecified || noProtocolsSpecified) { log.info({ protocols, tradeType }, 'Routing across V3'); metric.putMetric('SwapRouteFromChain_V3_GetRoutesThenQuotes_Request', 1, MetricLoggerUnit.Count); const beforeGetRoutesThenQuotes = Date.now(); quotePromises.push(v3CandidatePoolsPromise.then(v3CandidatePools => this.v3Quoter .getRoutesThenQuotes(tokenIn, tokenOut, amount, amounts, percents, quoteToken, v3CandidatePools, tradeType, routingConfig, v3GasModel) .then(result => { metric.putMetric(`SwapRouteFromChain_V3_GetRoutesThenQuotes_Load`, Date.now() - beforeGetRoutesThenQuotes, MetricLoggerUnit.Milliseconds); return result; }))); } // Maybe Quote V2 - if V2 is specified, or no protocol is specified AND v2 is supported in this chain if (v2SupportedInChain && (v2ProtocolSpecified || noProtocolsSpecified)) { log.info({ protocols, tradeType }, 'Routing across V2'); metric.putMetric('SwapRouteFromChain_V2_GetRoutesThenQuotes_Request', 1, MetricLoggerUnit.Count); const beforeGetRoutesThenQuotes = Date.now(); quotePromises.push(v2CandidatePoolsPromise.then(v2CandidatePools => this.v2Quoter .getRoutesThenQuotes(tokenIn, tokenOut, amount, amounts, percents, quoteToken, v2CandidatePools, tradeType, routingConfig, v2GasModel, gasPriceWei) .then(result => { metric.putMetric(`SwapRouteFromChain_V2_GetRoutesThenQuotes_Load`, Date.now() - beforeGetRoutesThenQuotes, MetricLoggerUnit.Milliseconds); return result; }))); } // Maybe Quote mixed routes // if MixedProtocol is specified or no protocol is specified and v2 is supported AND tradeType is ExactIn // AND is Mainnet or Gorli if (shouldQueryMixedProtocol && mixedProtocolAllowed) { log.info({ protocols, tradeType }, 'Routing across MixedRoutes'); metric.putMetric('SwapRouteFromChain_Mixed_GetRoutesThenQuotes_Request', 1, MetricLoggerUnit.Count); const beforeGetRoutesThenQuotes = Date.now(); quotePromises.push(Promise.all([v3CandidatePoolsPromise, v2CandidatePoolsPromise]).then(([v3CandidatePools, v2CandidatePools]) => this.mixedQuoter .getRoutesThenQuotes(tokenIn, tokenOut, amount, amounts, percents, quoteToken, [v3CandidatePools, v2CandidatePools], tradeType, routingConfig, mixedRouteGasModel) .then(result => { metric.putMetric(`SwapRouteFromChain_Mixed_GetRoutesThenQuotes_Load`, Date.now() - beforeGetRoutesThenQuotes, MetricLoggerUnit.Milliseconds); return result; }))); } const getQuotesResults = await Promise.all(quotePromises); const allRoutesWithValidQuotes = []; const allCandidatePools = []; getQuotesResults.forEach(getQuoteResult => { allRoutesWithValidQuotes.push(...getQuoteResult.routesWithValidQuotes); if (getQuoteResult.candidatePools) { allCandidatePools.push(getQuoteResult.candidatePools); } }); if (allRoutesWithValidQuotes.length === 0) { log.info({ allRoutesWithValidQuotes }, 'Received no valid quotes'); return null; } // Given all the quotes for all the amounts for all the routes, find the best combination. const bestSwapRoute = await getBestSwapRoute(amount, percents, allRoutesWithValidQuotes, tradeType, this.chainId, routingConfig, this.portionProvider, swapConfig); if (bestSwapRoute) { this.emitPoolSelectionMetrics(bestSwapRoute, allCandidatePools); } return bestSwapRoute; } tradeTypeStr(tradeType) { return tradeType === TradeType.EXACT_INPUT ? 'ExactIn' : 'ExactOut'; } tokenPairSymbolTradeTypeChainId(tokenIn, tokenOut, tradeType) { return `${tokenIn.symbol}/${tokenOut.symbol}/${this.tradeTypeStr(tradeType)}/${this.chainId}`; } determineCurrencyInOutFromTradeType(tradeType, amount, quoteCurrency) { if (tradeType === TradeType.EXACT_INPUT) { return { currencyIn: amount.currency, currencyOut: quoteCurrency, }; } else { return { currencyIn: quoteCurrency, currencyOut: amount.currency, }; } } async getGasPriceWei(latestBlockNumber, requestBlockNumber) { // Track how long it takes to resolve this async call. const beforeGasTimestamp = Date.now(); // Get an estimate of the gas price to use when estimating gas cost of different routes. const { gasPriceWei } = await this.gasPriceProvider.getGasPrice(latestBlockNumber, requestBlockNumber); metric.putMetric('GasPriceLoad', Date.now() - beforeGasTimestamp, MetricLoggerUnit.Milliseconds); return gasPriceWei; } async getGasModels(gasPriceWei, amountToken, quoteToken, providerConfig) { var _a; const beforeGasModel = Date.now(); const usdPoolPromise = getHighestLiquidityV3USDPool(this.chainId, this.v3PoolProvider, providerConfig); const nativeCurrency = WRAPPED_NATIVE_CURRENCY[this.chainId]; const nativeAndQuoteTokenV3PoolPromise = !quoteToken.equals(nativeCurrency) ? getHighestLiquidityV3NativePool(quoteToken, this.v3PoolProvider, providerConfig) : Promise.resolve(null); const nativeAndAmountTokenV3PoolPromise = !amountToken.equals(nativeCurrency) ? getHighestLiquidityV3NativePool(amountToken, this.v3PoolProvider, providerConfig) : Promise.resolve(null); // If a specific gas token is specified in the provider config // fetch the highest liq V3 pool with it and the native currency const nativeAndSpecifiedGasTokenV3PoolPromise = (providerConfig === null || providerConfig === void 0 ? void 0 : providerConfig.gasToken) && !(providerConfig === null || providerConfig === void 0 ? void 0 : providerConfig.gasToken.equals(nativeCurrency)) ? getHighestLiquidityV3NativePool(providerConfig === null || providerConfig === void 0 ? void 0 : providerConfig.gasToken, this.v3PoolProvider, providerConfig) : Promise.resolve(null); const [usdPool, nativeAndQuoteTokenV3Pool, nativeAndAmountTokenV3Pool, nativeAndSpecifiedGasTokenV3Pool] = await Promise.all([ usdPoolPromise, nativeAndQuoteTokenV3PoolPromise, nativeAndAmountTokenV3PoolPromise, nativeAndSpecifiedGasTokenV3PoolPromise, ]); const pools = { usdPool: usdPool, nativeAndQuoteTokenV3Pool: nativeAndQuoteTokenV3Pool, nativeAndAmountTokenV3Pool: nativeAndAmountTokenV3Pool, nativeAndSpecifiedGasTokenV3Pool: nativeAndSpecifiedGasTokenV3Pool, }; const v2GasModelPromise = ((_a = this.v2Supported) === null || _a === void 0 ? void 0 : _a.includes(this.chainId)) ? this.v2GasModelFactory .buildGasModel({ chainId: this.chainId, gasPriceWei, poolProvider: this.v2PoolProvider, token: quoteToken, providerConfig: providerConfig, }) .catch(() => undefined) // If v2 model throws uncaught exception, we return undefined v2 gas model, so there's a chance v3 route can go through : Promise.resolve(undefined); const v3GasModelPromise = this.v3GasModelFactory.buildGasModel({ chainId: this.chainId, gasPriceWei, pools, amountToken, quoteToken, v2poolProvider: this.v2PoolProvider, providerConfig: providerConfig, }); const mixedRouteGasModelPromise = this.mixedRouteGasModelFactory.buildGasModel({ chainId: this.chainId, gasPriceWei, pools, amountToken, quoteToken, v2poolProvider: this.v2PoolProvider, providerConfig: providerConfig, }); const [v2GasModel, v3GasModel, mixedRouteGasModel] = await Promise.all([ v2GasModelPromise, v3GasModelPromise, mixedRouteGasModelPromise, ]); metric.putMetric('GasModelCreation', Date.now() - beforeGasModel, MetricLoggerUnit.Milliseconds); return { v2GasModel: v2GasModel, v3GasModel: v3GasModel, mixedRouteGasModel: mixedRouteGasModel, }; } // Note multiplications here can result in a loss of precision in the amounts (e.g. taking 50% of 101) // This is reconcilled at the end of the algorithm by adding any lost precision to one of // the splits in the route. getAmountDistribution(amount, routingConfig) { const { distributionPercent } = routingConfig; const percents = []; const amounts = []; for (let i = 1; i <= 100 / distributionPercent; i++) { percents.push(i * distributionPercent); amounts.push(amount.multiply(new Fraction(i * distributionPercent, 100))); } return [percents, amounts]; } async buildSwapAndAddMethodParameters(trade, swapAndAddOptions, swapAndAddParameters) { const { swapOptions: { recipient, slippageTolerance, deadline, inputTokenPermit }, addLiquidityOptions: addLiquidityConfig, } = swapAndAddOptions; const preLiquidityPosition = swapAndAddParameters.preLiquidityPosition; const finalBalanceTokenIn = swapAndAddParameters.initialBalanceTokenIn.subtract(trade.inputAmount); const finalBalanceTokenOut = swapAndAddParameters.initialBalanceTokenOut.add(trade.outputAmount); const approvalTypes = await this.swapRouterProvider.getApprovalType(finalBalanceTokenIn, finalBalanceTokenOut); const zeroForOne = finalBalanceTokenIn.currency.wrapped.sortsBefore(finalBalanceTokenOut.currency.wrapped); return { ...SwapRouter.swapAndAddCallParameters(trade, { recipient, slippageTolerance, deadlineOrPreviousBlockhash: deadline, inputTokenPermit, }, Position.fromAmounts({ pool: preLiquidityPosition.pool, tickLower: preLiquidityPosition.tickLower, tickUpper: preLiquidityPosition.tickUpper, amount0: zeroForOne ? finalBalanceTokenIn.quotient.toString() : finalBalanceTokenOut.quotient.toString(), amount1: zeroForOne ? finalBalanceTokenOut.quotient.toString() : finalBalanceTokenIn.quotient.toString(), useFullPrecision: false, }), addLiquidityConfig, approvalTypes.approvalTokenIn, approvalTypes.approvalTokenOut), to: SWAP_ROUTER_02_ADDRESSES(this.chainId), }; } emitPoolSelectionMetrics(swapRouteRaw, allPoolsBySelection) { const poolAddressesUsed = new Set(); const { routes: routeAmounts } = swapRouteRaw; _(routeAmounts) .flatMap(routeAmount => { const { poolAddresses } = routeAmount; return poolAddresse