UNPKG

@maxosllc/smart-order-router

Version:
749 lines 172 kB
import { BigNumber } from '@ethersproject/bignumber'; import { JsonRpcProvider } from '@ethersproject/providers'; import DEFAULT_TOKEN_LIST from '@uniswap/default-token-list'; import { Protocol, SwapRouter, ZERO } from '@uniswap/router-sdk'; import { Fraction, TradeType, } from '@uniswap/sdk-core'; import { ChainId } from '../../../src/util/chains'; 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, CachingV4SubgraphProvider, EIP1559GasPriceProvider, ETHGasStationInfoProvider, LegacyGasPriceProvider, NodeJSCache, OnChainGasPriceProvider, OnChainQuoteProvider, StaticV2SubgraphProvider, StaticV3SubgraphProvider, StaticV4SubgraphProvider, SwapRouterProvider, TokenPropertiesProvider, UniswapMulticallProvider, URISubgraphProvider, V2QuoteProvider, V2SubgraphProviderWithFallBacks, V3SubgraphProviderWithFallBacks, V4SubgraphProviderWithFallBacks, } 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 { ArbitrumGasDataProvider, } from '../../providers/v3/gas-data-provider'; import { V3PoolProvider, } from '../../providers/v3/pool-provider'; import { CachingV4PoolProvider } from '../../providers/v4/caching-pool-provider'; import { V4PoolProvider, } from '../../providers/v4/pool-provider'; import { Erc20__factory } from '../../types/other/factories/Erc20__factory'; import { getAddress, getAddressLowerCase, getApplicableV4FeesTickspacingsHooks, HooksOptions, MIXED_SUPPORTED, shouldWipeoutCachedRoutes, SWAP_ROUTER_02_ADDRESSES, V4_SUPPORTED, 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 { BATCH_PARAMS, BLOCK_NUMBER_CONFIGS, DEFAULT_BATCH_PARAMS, DEFAULT_BLOCK_NUMBER_CONFIGS, DEFAULT_GAS_ERROR_FAILURE_OVERRIDES, DEFAULT_RETRY_OPTIONS, DEFAULT_SUCCESS_RATE_FAILURE_OVERRIDES, GAS_ERROR_FAILURE_OVERRIDES, RETRY_OPTIONS, SUCCESS_RATE_FAILURE_OVERRIDES, } from '../../util/onchainQuoteProviderConfigs'; import { UNSUPPORTED_TOKENS } from '../../util/unsupported-tokens'; import { SwapToRatioStatus, SwapType, } from '../router'; import { UniversalRouterVersion } from '@uniswap/universal-router-sdk'; import { DEFAULT_BLOCKS_TO_LIVE } from '../../util/defaultBlocksToLive'; import { INTENT } from '../../util/intent'; import { serializeRouteIds } from '../../util/serializeRouteIds'; import { DEFAULT_ROUTING_CONFIG_BY_CHAIN, ETH_GAS_STATION_API_URL, } from './config'; import { getBestSwapRoute } from './functions/best-swap-route'; import { calculateRatioAmountIn } from './functions/calculate-ratio-amount-in'; import { getMixedCrossLiquidityCandidatePools, getV2CandidatePools, getV3CandidatePools, getV4CandidatePools, } from './functions/get-candidate-pools'; import { NATIVE_OVERHEAD } from './gas-models/gas-costs'; import { MixedRouteHeuristicGasModelFactory } from './gas-models/mixedRoute/mixed-route-heuristic-gas-model'; import { V2HeuristicGasModelFactory } from './gas-models/v2/v2-heuristic-gas-model'; import { V3HeuristicGasModelFactory } from './gas-models/v3/v3-heuristic-gas-model'; import { V4HeuristicGasModelFactory } from './gas-models/v4/v4-heuristic-gas-model'; import { MixedQuoter, V2Quoter, V3Quoter } from './quoters'; import { V4Quoter } from './quoters/v4-quoter'; 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, v4SubgraphProvider, v4PoolProvider, v3PoolProvider, onChainQuoteProvider, v2PoolProvider, v2QuoteProvider, v2SubgraphProvider, tokenProvider, blockedTokenListProvider, v3SubgraphProvider, gasPriceProvider, v4GasModelFactory, v3GasModelFactory, v2GasModelFactory, mixedRouteGasModelFactory, swapRouterProvider, tokenValidatorProvider, arbitrumGasDataProvider, simulator, routeCachingProvider, tokenPropertiesProvider, portionProvider, v2Supported, v4Supported, mixedSupported, v4PoolParams, cachedRoutesCacheInvalidationFixRolloutPercentage, }) { this.chainId = chainId; this.provider = provider; this.multicall2Provider = multicall2Provider !== null && multicall2Provider !== void 0 ? multicall2Provider : new UniswapMulticallProvider(chainId, provider, 375000); this.v4PoolProvider = v4PoolProvider !== null && v4PoolProvider !== void 0 ? v4PoolProvider : new CachingV4PoolProvider(this.chainId, new V4PoolProvider(ID_TO_CHAIN_ID(chainId), this.multicall2Provider), new NodeJSCache(new NodeCache({ stdTTL: 360, useClones: false }))); 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 { switch (chainId) { case ChainId.OPTIMISM: case ChainId.OPTIMISM_GOERLI: case ChainId.OPTIMISM_SEPOLIA: this.onChainQuoteProvider = new OnChainQuoteProvider(chainId, provider, this.multicall2Provider, { retries: 2, minTimeout: 100, maxTimeout: 1000, }, (_) => { return { multicallChunk: 110, gasLimitPerCall: 1200000, quoteMinSuccessRate: 0.1, }; }, (_) => { return { gasLimitOverride: 3000000, multicallChunk: 45, }; }, (_) => { return { gasLimitOverride: 3000000, multicallChunk: 45, }; }, (_) => { return { baseBlockOffset: -10, rollback: { enabled: true, attemptsBeforeRollback: 1, rollbackBlockOffset: -10, }, }; }); break; case ChainId.BASE: case ChainId.BLAST: case ChainId.ZORA: case ChainId.WORLDCHAIN: case ChainId.UNICHAIN_SEPOLIA: case ChainId.MONAD_TESTNET: case ChainId.BASE_SEPOLIA: case ChainId.UNICHAIN: case ChainId.BASE_GOERLI: case ChainId.SONEIUM: this.onChainQuoteProvider = new OnChainQuoteProvider(chainId, provider, this.multicall2Provider, { retries: 2, minTimeout: 100, maxTimeout: 1000, }, (_) => { return { multicallChunk: 80, gasLimitPerCall: 1200000, quoteMinSuccessRate: 0.1, }; }, (_) => { return { gasLimitOverride: 3000000, multicallChunk: 45, }; }, (_) => { return { gasLimitOverride: 3000000, multicallChunk: 45, }; }, (_) => { return { baseBlockOffset: -10, rollback: { enabled: true, attemptsBeforeRollback: 1, rollbackBlockOffset: -10, }, }; }); break; case ChainId.ZKSYNC: this.onChainQuoteProvider = new OnChainQuoteProvider(chainId, provider, this.multicall2Provider, { retries: 2, minTimeout: 100, maxTimeout: 1000, }, (_) => { return { multicallChunk: 27, gasLimitPerCall: 3000000, quoteMinSuccessRate: 0.1, }; }, (_) => { return { gasLimitOverride: 6000000, multicallChunk: 13, }; }, (_) => { return { gasLimitOverride: 6000000, multicallChunk: 13, }; }, (_) => { return { baseBlockOffset: -10, rollback: { enabled: true, attemptsBeforeRollback: 1, rollbackBlockOffset: -10, }, }; }); break; case ChainId.ARBITRUM_ONE: case ChainId.ARBITRUM_GOERLI: case ChainId.ARBITRUM_SEPOLIA: this.onChainQuoteProvider = new OnChainQuoteProvider(chainId, provider, this.multicall2Provider, { retries: 2, minTimeout: 100, maxTimeout: 1000, }, (_) => { return { multicallChunk: 10, gasLimitPerCall: 12000000, quoteMinSuccessRate: 0.1, }; }, (_) => { return { gasLimitOverride: 30000000, multicallChunk: 6, }; }, (_) => { return { gasLimitOverride: 30000000, multicallChunk: 6, }; }); break; case ChainId.CELO: case ChainId.CELO_ALFAJORES: this.onChainQuoteProvider = new OnChainQuoteProvider(chainId, provider, this.multicall2Provider, { retries: 2, minTimeout: 100, maxTimeout: 1000, }, (_) => { return { multicallChunk: 10, gasLimitPerCall: 5000000, quoteMinSuccessRate: 0.1, }; }, (_) => { return { gasLimitOverride: 5000000, multicallChunk: 5, }; }, (_) => { return { gasLimitOverride: 6250000, multicallChunk: 4, }; }); break; case ChainId.POLYGON_MUMBAI: case ChainId.SEPOLIA: case ChainId.MAINNET: case ChainId.POLYGON: this.onChainQuoteProvider = new OnChainQuoteProvider(chainId, provider, this.multicall2Provider, RETRY_OPTIONS[chainId], (_) => BATCH_PARAMS[chainId], (_) => GAS_ERROR_FAILURE_OVERRIDES[chainId], (_) => SUCCESS_RATE_FAILURE_OVERRIDES[chainId], (_) => BLOCK_NUMBER_CONFIGS[chainId]); break; default: this.onChainQuoteProvider = new OnChainQuoteProvider(chainId, provider, this.multicall2Provider, DEFAULT_RETRY_OPTIONS, (_) => DEFAULT_BATCH_PARAMS, (_) => DEFAULT_GAS_ERROR_FAILURE_OVERRIDES, (_) => DEFAULT_SUCCESS_RATE_FAILURE_OVERRIDES, (_) => DEFAULT_BLOCK_NUMBER_CONFIGS); break; } } 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), ]); } this.v4PoolParams = v4PoolParams !== null && v4PoolParams !== void 0 ? v4PoolParams : getApplicableV4FeesTickspacingsHooks(chainId); if (v4SubgraphProvider) { this.v4SubgraphProvider = v4SubgraphProvider; } else { this.v4SubgraphProvider = new V4SubgraphProviderWithFallBacks([ new CachingV4SubgraphProvider(chainId, new URISubgraphProvider(chainId, `https://cloudflare-ipfs.com/ipns/api.uniswap.org/v1/pools/v4/${chainName}.json`, undefined, 0), new NodeJSCache(new NodeCache({ stdTTL: 300, useClones: false }))), new StaticV4SubgraphProvider(chainId, this.v4PoolProvider, this.v4PoolParams), ]); } let gasPriceProviderInstance; if (JsonRpcProvider.isProvider(this.provider)) { gasPriceProviderInstance = new OnChainGasPriceProvider(chainId, new EIP1559GasPriceProvider(this.provider), new LegacyGasPriceProvider(this.provider)); } else { gasPriceProviderInstance = new ETHGasStationInfoProvider(ETH_GAS_STATION_API_URL); } this.gasPriceProvider = gasPriceProvider !== null && gasPriceProvider !== void 0 ? gasPriceProvider : new CachingGasStationProvider(chainId, gasPriceProviderInstance, new NodeJSCache(new NodeCache({ stdTTL: 7, useClones: false }))); this.v4GasModelFactory = v4GasModelFactory !== null && v4GasModelFactory !== void 0 ? v4GasModelFactory : new V4HeuristicGasModelFactory(this.provider); this.v3GasModelFactory = v3GasModelFactory !== null && v3GasModelFactory !== void 0 ? v3GasModelFactory : new V3HeuristicGasModelFactory(this.provider); this.v2GasModelFactory = v2GasModelFactory !== null && v2GasModelFactory !== void 0 ? v2GasModelFactory : new V2HeuristicGasModelFactory(this.provider); this.mixedRouteGasModelFactory = mixedRouteGasModelFactory !== null && mixedRouteGasModelFactory !== void 0 ? mixedRouteGasModelFactory : new MixedRouteHeuristicGasModelFactory(); this.swapRouterProvider = swapRouterProvider !== null && swapRouterProvider !== void 0 ? swapRouterProvider : new SwapRouterProvider(this.multicall2Provider, this.chainId); if (chainId === ChainId.ARBITRUM_ONE || chainId === ChainId.ARBITRUM_GOERLI) { this.l2GasDataProvider = arbitrumGasDataProvider !== null && arbitrumGasDataProvider !== void 0 ? arbitrumGasDataProvider : new ArbitrumGasDataProvider(chainId, this.provider); } // 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.l2GasDataProvider); this.v3Quoter = new V3Quoter(this.v3SubgraphProvider, this.v3PoolProvider, this.onChainQuoteProvider, this.tokenProvider, this.chainId, this.blockedTokenListProvider, this.tokenValidatorProvider); this.v4Quoter = new V4Quoter(this.v4SubgraphProvider, this.v4PoolProvider, this.onChainQuoteProvider, this.tokenProvider, this.chainId, this.blockedTokenListProvider, this.tokenValidatorProvider); this.mixedQuoter = new MixedQuoter(this.v4SubgraphProvider, this.v4PoolProvider, 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; this.v4Supported = v4Supported !== null && v4Supported !== void 0 ? v4Supported : V4_SUPPORTED; this.mixedSupported = mixedSupported !== null && mixedSupported !== void 0 ? mixedSupported : MIXED_SUPPORTED; this.cachedRoutesCacheInvalidationFixRolloutPercentage = cachedRoutesCacheInvalidationFixRolloutPercentage; } 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, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z; const originalAmount = amount; const { currencyIn, currencyOut } = this.determineCurrencyInOutFromTradeType(tradeType, amount, quoteCurrency); const tokenOutProperties = await this.tokenPropertiesProvider.getTokensProperties([currencyOut], partialRoutingConfig); const feeTakenOnTransfer = (_c = (_a = tokenOutProperties[getAddressLowerCase(currencyOut)]) === null || _a === void 0 ? void 0 : _a.tokenFeeResult) === null || _c === void 0 ? void 0 : _c.feeTakenOnTransfer; const externalTransferFailed = (_e = (_d = tokenOutProperties[getAddressLowerCase(currencyOut)]) === null || _d === void 0 ? void 0 : _d.tokenFeeResult) === null || _e === void 0 ? void 0 : _e.externalTransferFailed; // We want to log the fee on transfer output tokens that we are taking fee or not // Ideally the trade size (normalized in USD) would be ideal to log here, but we don't have spot price of output tokens here. // We have to make sure token out is FOT with either buy/sell fee bps > 0 if (((_h = (_g = (_f = tokenOutProperties[getAddressLowerCase(currencyOut)]) === null || _f === void 0 ? void 0 : _f.tokenFeeResult) === null || _g === void 0 ? void 0 : _g.buyFeeBps) === null || _h === void 0 ? void 0 : _h.gt(0)) || ((_l = (_k = (_j = tokenOutProperties[getAddressLowerCase(currencyOut)]) === null || _j === void 0 ? void 0 : _j.tokenFeeResult) === null || _k === void 0 ? void 0 : _k.sellFeeBps) === null || _l === void 0 ? void 0 : _l.gt(0))) { if (feeTakenOnTransfer || externalTransferFailed) { // also to be extra safe, in case of FOT with feeTakenOnTransfer or externalTransferFailed, // we nullify the fee and flat fee to avoid any potential issues. // although neither web nor wallet should use the calldata returned from routing/SOR if ((swapConfig === null || swapConfig === void 0 ? void 0 : swapConfig.type) === SwapType.UNIVERSAL_ROUTER) { swapConfig.fee = undefined; swapConfig.flatFee = undefined; } metric.putMetric('TokenOutFeeOnTransferNotTakingFee', 1, MetricLoggerUnit.Count); } else { metric.putMetric('TokenOutFeeOnTransferTakingFee', 1, MetricLoggerUnit.Count); } } if (tradeType === TradeType.EXACT_OUTPUT) { const portionAmount = this.portionProvider.getPortionAmount(amount, tradeType, feeTakenOnTransfer, externalTransferFailed, 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', `${currencyIn.symbol}/${currencyOut.symbol}`); metric.setProperty('tokenIn', getAddress(currencyIn)); metric.setProperty('tokenOut', getAddress(currencyOut)); 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 = (_m = partialRoutingConfig.blockNumber) !== null && _m !== void 0 ? _m : 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 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(this.chainId, amount.currency, quoteCurrency), gasToken, externalTransferFailed, feeTakenOnTransfer, }; const { v2GasModel: v2GasModel, v3GasModel: v3GasModel, v4GasModel: v4GasModel, mixedRouteGasModel: mixedRouteGasModel, } = await this.getGasModels(gasPriceWei, amount.currency.wrapped, quoteCurrency.wrapped, 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 = (_o = routingConfig.overwriteCacheMode) !== null && _o !== void 0 ? _o : (await ((_p = this.routeCachingProvider) === null || _p === void 0 ? void 0 : _p.getCacheMode(this.chainId, amount, quoteCurrency, tradeType, protocols))); // Fetch CachedRoutes let cachedRoutes; // Decide whether to use cached routes or not - If |enabledAndRequestedProtocolsMatch| is true we are good to use cached routes. // In order to use cached routes, we need to have all enabled protocols specified in the request. // By default, all protocols are enabled but for UniversalRouterVersion.V1_2, V4 is not. // - ref: https://github.com/Uniswap/routing-api/blob/663b607d80d9249f85e7ab0925a611ec3701da2a/lib/util/supportedProtocolVersions.ts#L15 // So we take this into account when deciding whether to use cached routes or not. // We only want to use cache if all enabled protocols are specified (V2,V3,V4? + MIXED). In any other case, use onchain path. // - Cache is optimized for global search, not for specific protocol(s) search. // For legacy systems (SWAP_ROUTER_02) or missing swapConfig, follow UniversalRouterVersion.V1_2 logic. const availableProtocolsSet = new Set(Object.values(Protocol)); const requestedProtocolsSet = new Set(protocols); const swapRouter = !swapConfig || swapConfig.type === SwapType.SWAP_ROUTER_02 || (swapConfig.type === SwapType.UNIVERSAL_ROUTER && swapConfig.version === UniversalRouterVersion.V1_2); if (swapRouter) { availableProtocolsSet.delete(Protocol.V4); if (requestedProtocolsSet.has(Protocol.V4)) { requestedProtocolsSet.delete(Protocol.V4); } } const enabledAndRequestedProtocolsMatch = availableProtocolsSet.size === requestedProtocolsSet.size && [...availableProtocolsSet].every((protocol) => requestedProtocolsSet.has(protocol)); // If the requested protocols do not match the enabled protocols, we need to set the hooks options to NO_HOOKS. if (!requestedProtocolsSet.has(Protocol.V4)) { routingConfig.hooksOptions = HooksOptions.NO_HOOKS; } // If hooksOptions not specified and it's not a swapRouter (i.e. Universal Router it is), // we should also set it to HOOKS_INCLUSIVE, as this is default behavior even without hooksOptions. if (!routingConfig.hooksOptions) { routingConfig.hooksOptions = HooksOptions.HOOKS_INCLUSIVE; } log.debug('UniversalRouterVersion_CacheGate_Check', { availableProtocolsSet: Array.from(availableProtocolsSet), requestedProtocolsSet: Array.from(requestedProtocolsSet), enabledAndRequestedProtocolsMatch, swapConfigType: swapConfig === null || swapConfig === void 0 ? void 0 : swapConfig.type, swapConfigUniversalRouterVersion: (swapConfig === null || swapConfig === void 0 ? void 0 : swapConfig.type) === SwapType.UNIVERSAL_ROUTER ? swapConfig === null || swapConfig === void 0 ? void 0 : swapConfig.version : 'N/A', }); if (routingConfig.useCachedRoutes && cacheMode !== CacheMode.Darkmode && AlphaRouter.isAllowedToEnterCachedRoutes(routingConfig.intent, routingConfig.hooksOptions, swapRouter)) { if (enabledAndRequestedProtocolsMatch) { if (protocols.includes(Protocol.V4) && (currencyIn.isNative || currencyOut.isNative)) { const [wrappedNativeCachedRoutes, nativeCachedRoutes] = await Promise.all([ (_q = this.routeCachingProvider) === null || _q === void 0 ? void 0 : _q.getCachedRoute(this.chainId, CurrencyAmount.fromRawAmount(amount.currency.wrapped, amount.quotient), quoteCurrency.wrapped, tradeType, protocols, await blockNumber, routingConfig.optimisticCachedRoutes), (_r = this.routeCachingProvider) === null || _r === void 0 ? void 0 : _r.getCachedRoute(this.chainId, amount, quoteCurrency, tradeType, [Protocol.V4], await blockNumber, routingConfig.optimisticCachedRoutes), ]); if ((wrappedNativeCachedRoutes && (wrappedNativeCachedRoutes === null || wrappedNativeCachedRoutes === void 0 ? void 0 : wrappedNativeCachedRoutes.routes.length) > 0) || (nativeCachedRoutes && (nativeCachedRoutes === null || nativeCachedRoutes === void 0 ? void 0 : nativeCachedRoutes.routes.length) > 0)) { cachedRoutes = new CachedRoutes({ routes: [ ...((_s = nativeCachedRoutes === null || nativeCachedRoutes === void 0 ? void 0 : nativeCachedRoutes.routes) !== null && _s !== void 0 ? _s : []), ...((_t = wrappedNativeCachedRoutes === null || wrappedNativeCachedRoutes === void 0 ? void 0 : wrappedNativeCachedRoutes.routes) !== null && _t !== void 0 ? _t : []), ], chainId: this.chainId, currencyIn: currencyIn, currencyOut: currencyOut, protocolsCovered: protocols, blockNumber: await blockNumber, tradeType: tradeType, originalAmount: (_v = (_u = wrappedNativeCachedRoutes === null || wrappedNativeCachedRoutes === void 0 ? void 0 : wrappedNativeCachedRoutes.originalAmount) !== null && _u !== void 0 ? _u : nativeCachedRoutes === null || nativeCachedRoutes === void 0 ? void 0 : nativeCachedRoutes.originalAmount) !== null && _v !== void 0 ? _v : amount.quotient.toString(), blocksToLive: (_x = (_w = wrappedNativeCachedRoutes === null || wrappedNativeCachedRoutes === void 0 ? void 0 : wrappedNativeCachedRoutes.blocksToLive) !== null && _w !== void 0 ? _w : nativeCachedRoutes === null || nativeCachedRoutes === void 0 ? void 0 : nativeCachedRoutes.blocksToLive) !== null && _x !== void 0 ? _x : DEFAULT_BLOCKS_TO_LIVE[this.chainId], }); } } else { cachedRoutes = await ((_y = this.routeCachingProvider) === null || _y === void 0 ? void 0 : _y.getCachedRoute(this.chainId, amount, quoteCurrency, tradeType, protocols, await blockNumber, routingConfig.optimisticCachedRoutes)); } } } if (shouldWipeoutCachedRoutes(cachedRoutes, routingConfig)) { cachedRoutes = undefined; } 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({ currencyIn: currencyIn.symbol, currencyInAddress: getAddress(currencyIn), currencyOut: currencyOut.symbol, currencyOutAddress: getAddress(currencyOut), cacheMode, amount: amount.toExact(), chainId: this.chainId, tradeType: this.tradeTypeStr(tradeType), }, `GetCachedRoute miss ${cacheMode} for ${this.tokenPairSymbolTradeTypeChainId(currencyIn, currencyOut, tradeType)}`); } else if (cachedRoutes && routingConfig.useCachedRoutes) { metric.putMetric(`GetCachedRoute_hit_${cacheMode}`, 1, MetricLoggerUnit.Count); log.info({ currencyIn: currencyIn.symbol, currencyInAddress: getAddress(currencyIn), currencyOut: currencyOut.symbol, currencyOutAddress: getAddress(currencyOut), cacheMode, amount: amount.toExact(), chainId: this.chainId, tradeType: this.tradeTypeStr(tradeType), }, `GetCachedRoute hit ${cacheMode} for ${this.tokenPairSymbolTradeTypeChainId(currencyIn, currencyOut, tradeType)}`); } let swapRouteFromCachePromise = Promise.resolve(null); if (cachedRoutes) { swapRouteFromCachePromise = this.getSwapRouteFromCache(currencyIn, currencyOut, cachedRoutes, await blockNumber, amount, quoteCurrency, tradeType, routingConfig, v3GasModel, v4GasModel, mixedRouteGasModel, gasPriceWei, v2GasModel, swapConfig, providerConfig); } let swapRouteFromChainPromise = Promise.resolve(null); if (!cachedRoutes || cacheMode !== CacheMode.Livemode) { swapRouteFromChainPromise = this.getSwapRouteFromChain(amount, currencyIn, currencyOut, protocols, quoteCurrency, tradeType, routingConfig, v3GasModel, v4GasModel, mixedRouteGasModel, gasPriceWei, v2GasModel, swapConfig, providerConfig); } 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))) { try { // 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(currencyIn, currencyOut, tradeType), blockNumber, }, `Comparing quotes between Chain and Cache for ${this.tokenPairSymbolTradeTypeChainId(currencyIn, currencyOut, tradeType)}`); } catch (error) { // This is in response to the 'division by zero' error // during https://uniswapteam.slack.com/archives/C059TGEC57W/p1723997015399579 if (error instanceof RangeError && error.message.includes('Division by zero')) { log.error({ quoteGasAdjustedDiff: quoteGasAdjustedDiff.toExact(), swapRouteFromChainQuoteGasAdjusted: swapRouteFromChain.quoteGasAdjusted.toExact(), }, 'Error calculating misquote percent'); metric.putMetric(`TapcompareCachedRoute_quoteGasAdjustedDiffPercent_divzero`, 1, MetricLoggerUnit.Count); } // Log but don't throw here - this is only for logging. } } } let newSetCachedRoutesPath = false; const shouldEnableCachedRoutesCacheInvalidationFix = Math.random() * 100 < ((_z = this.cachedRoutesCacheInvalidationFixRolloutPercentage) !== null && _z !== void 0 ? _z : 0); // we have to write cached routes right before checking swapRouteRaw is null or not // because getCachedRoutes in routing-api do not use the blocks-to-live to filter out the expired routes at all // there's a possibility the cachedRoutes is always populated, but swapRouteFromCache is always null, because we don't update cachedRoutes in this case at all, // as long as it's within 24 hours sliding window TTL if (shouldEnableCachedRoutesCacheInvalidationFix) { // theoretically, when routingConfig.intent === INTENT.CACHING, optimisticCachedRoutes should be false // so that we can always pass in cachedRoutes?.notExpired(await blockNumber, !routingConfig.optimisticCachedRoutes) // but just to be safe, we just hardcode true when checking the cached routes expiry for write update // we decide to not check cached routes expiry in the read path anyway if (!(cachedRoutes === null || cachedRoutes === void 0 ? void 0 : cachedRoutes.notExpired(await blockNumber, true))) { // optimisticCachedRoutes === false means at routing-api level, we only want to set cached routes during intent=caching, not intent=quote // this means during the online quote endpoint path, we should not reset cached routes if (routingConfig.intent === INTENT.CACHING) { // due to fire and forget nature, we already take note that we should set new cached routes during the new path newSetCachedRoutesPath = true; metric.putMetric(`SetCachedRoute_NewPath`, 1, MetricLoggerUnit.Count); // there's a chance that swapRouteFromChain might be populated already, // when there's no cachedroutes in the dynamo DB. // in that case, we don't try to swap route from chain again const swapRouteFromChainAgain = swapRouteFromChain !== null && swapRouteFromChain !== void 0 ? swapRouteFromChain : // we have to intentionally await here, because routing-api lambda has a chance to return the swapRoute/swapRouteWithSimulation // before the routing-api quote handler can finish running getSwapRouteFromChain (getSwapRouteFromChain is runtime intensive) (await this.getSwapRouteFromChain(amount, currencyIn, currencyOut, protocols, quoteCurrency, tradeType, routingConfig, v3GasModel, v4GasModel, mixedRouteGasModel, gasPriceWei, v2GasModel, swapConfig, providerConfig)); if (swapRouteFromChainAgain) { const routesToCache = CachedRoutes.fromRoutesWithValidQuotes(swapRouteFromChainAgain.routes, this.chainId, currencyIn, currencyOut, protocols.sort(), await blockNumber, tradeType, amount.toExact()); await this.setCachedRoutesAndLog(amount, currencyIn, currencyOut, tradeType, 'SetCachedRoute_NewPath', routesToCache, routingConfig.cachedRoutesRouteIds); } } } } if (!swapRouteRaw) { return null; } const { quote, quoteGasAdjusted,