UNPKG

@uniswap/smart-order-router

Version:
722 lines 168 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AlphaRouter = exports.LowerCaseStringArray = exports.MapWithLowerCaseKey = void 0; const bignumber_1 = require("@ethersproject/bignumber"); const providers_1 = require("@ethersproject/providers"); const default_token_list_1 = __importDefault(require("@uniswap/default-token-list")); const router_sdk_1 = require("@uniswap/router-sdk"); const sdk_core_1 = require("@uniswap/sdk-core"); const v3_sdk_1 = require("@uniswap/v3-sdk"); const async_retry_1 = __importDefault(require("async-retry")); const jsbi_1 = __importDefault(require("jsbi")); const lodash_1 = __importDefault(require("lodash")); const node_cache_1 = __importDefault(require("node-cache")); const providers_2 = require("../../providers"); const caching_token_list_provider_1 = require("../../providers/caching-token-list-provider"); const portion_provider_1 = require("../../providers/portion-provider"); const token_fee_fetcher_1 = require("../../providers/token-fee-fetcher"); const token_provider_1 = require("../../providers/token-provider"); const token_validator_provider_1 = require("../../providers/token-validator-provider"); const pool_provider_1 = require("../../providers/v2/pool-provider"); const gas_data_provider_1 = require("../../providers/v3/gas-data-provider"); const pool_provider_2 = require("../../providers/v3/pool-provider"); const caching_pool_provider_1 = require("../../providers/v4/caching-pool-provider"); const pool_provider_3 = require("../../providers/v4/pool-provider"); const Erc20__factory_1 = require("../../types/other/factories/Erc20__factory"); const util_1 = require("../../util"); const amounts_1 = require("../../util/amounts"); const chains_1 = require("../../util/chains"); const gas_factory_helpers_1 = require("../../util/gas-factory-helpers"); const log_1 = require("../../util/log"); const methodParameters_1 = require("../../util/methodParameters"); const metric_1 = require("../../util/metric"); const onchainQuoteProviderConfigs_1 = require("../../util/onchainQuoteProviderConfigs"); const unsupported_tokens_1 = require("../../util/unsupported-tokens"); const router_1 = require("../router"); const universal_router_sdk_1 = require("@uniswap/universal-router-sdk"); const defaultBlocksToLive_1 = require("../../util/defaultBlocksToLive"); const intent_1 = require("../../util/intent"); const config_1 = require("./config"); const best_swap_route_1 = require("./functions/best-swap-route"); const calculate_ratio_amount_in_1 = require("./functions/calculate-ratio-amount-in"); const get_candidate_pools_1 = require("./functions/get-candidate-pools"); const gas_costs_1 = require("./gas-models/gas-costs"); const mixed_route_heuristic_gas_model_1 = require("./gas-models/mixedRoute/mixed-route-heuristic-gas-model"); const v2_heuristic_gas_model_1 = require("./gas-models/v2/v2-heuristic-gas-model"); const v3_heuristic_gas_model_1 = require("./gas-models/v3/v3-heuristic-gas-model"); const v4_heuristic_gas_model_1 = require("./gas-models/v4/v4-heuristic-gas-model"); const quoters_1 = require("./quoters"); const v4_quoter_1 = require("./quoters/v4-quoter"); class MapWithLowerCaseKey extends Map { set(key, value) { return super.set(key.toLowerCase(), value); } } exports.MapWithLowerCaseKey = MapWithLowerCaseKey; class LowerCaseStringArray extends Array { constructor(...items) { // Convert all items to lowercase before calling the parent constructor super(...items.map((item) => item.toLowerCase())); } } exports.LowerCaseStringArray = LowerCaseStringArray; 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 providers_2.UniswapMulticallProvider(chainId, provider, 375000); this.v4PoolProvider = v4PoolProvider !== null && v4PoolProvider !== void 0 ? v4PoolProvider : new caching_pool_provider_1.CachingV4PoolProvider(this.chainId, new pool_provider_3.V4PoolProvider((0, chains_1.ID_TO_CHAIN_ID)(chainId), this.multicall2Provider), new providers_2.NodeJSCache(new node_cache_1.default({ stdTTL: 360, useClones: false }))); this.v3PoolProvider = v3PoolProvider !== null && v3PoolProvider !== void 0 ? v3PoolProvider : new providers_2.CachingV3PoolProvider(this.chainId, new pool_provider_2.V3PoolProvider((0, chains_1.ID_TO_CHAIN_ID)(chainId), this.multicall2Provider), new providers_2.NodeJSCache(new node_cache_1.default({ stdTTL: 360, useClones: false }))); this.simulator = simulator; this.routeCachingProvider = routeCachingProvider; if (onChainQuoteProvider) { this.onChainQuoteProvider = onChainQuoteProvider; } else { switch (chainId) { case sdk_core_1.ChainId.OPTIMISM: case sdk_core_1.ChainId.OPTIMISM_GOERLI: case sdk_core_1.ChainId.OPTIMISM_SEPOLIA: this.onChainQuoteProvider = new providers_2.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 sdk_core_1.ChainId.BASE: case sdk_core_1.ChainId.BLAST: case sdk_core_1.ChainId.ZORA: case sdk_core_1.ChainId.WORLDCHAIN: case sdk_core_1.ChainId.UNICHAIN_SEPOLIA: case sdk_core_1.ChainId.MONAD_TESTNET: case sdk_core_1.ChainId.BASE_SEPOLIA: case sdk_core_1.ChainId.UNICHAIN: case sdk_core_1.ChainId.BASE_GOERLI: this.onChainQuoteProvider = new providers_2.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 sdk_core_1.ChainId.ZKSYNC: this.onChainQuoteProvider = new providers_2.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 sdk_core_1.ChainId.ARBITRUM_ONE: case sdk_core_1.ChainId.ARBITRUM_GOERLI: case sdk_core_1.ChainId.ARBITRUM_SEPOLIA: this.onChainQuoteProvider = new providers_2.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 sdk_core_1.ChainId.CELO: case sdk_core_1.ChainId.CELO_ALFAJORES: this.onChainQuoteProvider = new providers_2.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 sdk_core_1.ChainId.POLYGON_MUMBAI: case sdk_core_1.ChainId.SEPOLIA: case sdk_core_1.ChainId.MAINNET: case sdk_core_1.ChainId.POLYGON: this.onChainQuoteProvider = new providers_2.OnChainQuoteProvider(chainId, provider, this.multicall2Provider, onchainQuoteProviderConfigs_1.RETRY_OPTIONS[chainId], (_) => onchainQuoteProviderConfigs_1.BATCH_PARAMS[chainId], (_) => onchainQuoteProviderConfigs_1.GAS_ERROR_FAILURE_OVERRIDES[chainId], (_) => onchainQuoteProviderConfigs_1.SUCCESS_RATE_FAILURE_OVERRIDES[chainId], (_) => onchainQuoteProviderConfigs_1.BLOCK_NUMBER_CONFIGS[chainId]); break; default: this.onChainQuoteProvider = new providers_2.OnChainQuoteProvider(chainId, provider, this.multicall2Provider, onchainQuoteProviderConfigs_1.DEFAULT_RETRY_OPTIONS, (_) => onchainQuoteProviderConfigs_1.DEFAULT_BATCH_PARAMS, (_) => onchainQuoteProviderConfigs_1.DEFAULT_GAS_ERROR_FAILURE_OVERRIDES, (_) => onchainQuoteProviderConfigs_1.DEFAULT_SUCCESS_RATE_FAILURE_OVERRIDES, (_) => onchainQuoteProviderConfigs_1.DEFAULT_BLOCK_NUMBER_CONFIGS); break; } } if (tokenValidatorProvider) { this.tokenValidatorProvider = tokenValidatorProvider; } else if (this.chainId === sdk_core_1.ChainId.MAINNET) { this.tokenValidatorProvider = new token_validator_provider_1.TokenValidatorProvider(this.chainId, this.multicall2Provider, new providers_2.NodeJSCache(new node_cache_1.default({ stdTTL: 30000, useClones: false }))); } if (tokenPropertiesProvider) { this.tokenPropertiesProvider = tokenPropertiesProvider; } else { this.tokenPropertiesProvider = new providers_2.TokenPropertiesProvider(this.chainId, new providers_2.NodeJSCache(new node_cache_1.default({ stdTTL: 86400, useClones: false })), new token_fee_fetcher_1.OnChainTokenFeeFetcher(this.chainId, provider)); } this.v2PoolProvider = v2PoolProvider !== null && v2PoolProvider !== void 0 ? v2PoolProvider : new providers_2.CachingV2PoolProvider(chainId, new pool_provider_1.V2PoolProvider(chainId, this.multicall2Provider, this.tokenPropertiesProvider), new providers_2.NodeJSCache(new node_cache_1.default({ stdTTL: 60, useClones: false }))); this.v2QuoteProvider = v2QuoteProvider !== null && v2QuoteProvider !== void 0 ? v2QuoteProvider : new providers_2.V2QuoteProvider(); this.blockedTokenListProvider = blockedTokenListProvider !== null && blockedTokenListProvider !== void 0 ? blockedTokenListProvider : new caching_token_list_provider_1.CachingTokenListProvider(chainId, unsupported_tokens_1.UNSUPPORTED_TOKENS, new providers_2.NodeJSCache(new node_cache_1.default({ stdTTL: 3600, useClones: false }))); this.tokenProvider = tokenProvider !== null && tokenProvider !== void 0 ? tokenProvider : new providers_2.CachingTokenProviderWithFallback(chainId, new providers_2.NodeJSCache(new node_cache_1.default({ stdTTL: 3600, useClones: false })), new caching_token_list_provider_1.CachingTokenListProvider(chainId, default_token_list_1.default, new providers_2.NodeJSCache(new node_cache_1.default({ stdTTL: 3600, useClones: false }))), new token_provider_1.TokenProvider(chainId, this.multicall2Provider)); this.portionProvider = portionProvider !== null && portionProvider !== void 0 ? portionProvider : new portion_provider_1.PortionProvider(); const chainName = (0, chains_1.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 providers_2.V2SubgraphProviderWithFallBacks([ new providers_2.CachingV2SubgraphProvider(chainId, new providers_2.URISubgraphProvider(chainId, `https://cloudflare-ipfs.com/ipns/api.uniswap.org/v1/pools/v2/${chainName}.json`, undefined, 0), new providers_2.NodeJSCache(new node_cache_1.default({ stdTTL: 300, useClones: false }))), new providers_2.StaticV2SubgraphProvider(chainId), ]); } if (v3SubgraphProvider) { this.v3SubgraphProvider = v3SubgraphProvider; } else { this.v3SubgraphProvider = new providers_2.V3SubgraphProviderWithFallBacks([ new providers_2.CachingV3SubgraphProvider(chainId, new providers_2.URISubgraphProvider(chainId, `https://cloudflare-ipfs.com/ipns/api.uniswap.org/v1/pools/v3/${chainName}.json`, undefined, 0), new providers_2.NodeJSCache(new node_cache_1.default({ stdTTL: 300, useClones: false }))), new providers_2.StaticV3SubgraphProvider(chainId, this.v3PoolProvider), ]); } this.v4PoolParams = v4PoolParams !== null && v4PoolParams !== void 0 ? v4PoolParams : (0, util_1.getApplicableV4FeesTickspacingsHooks)(chainId); if (v4SubgraphProvider) { this.v4SubgraphProvider = v4SubgraphProvider; } else { this.v4SubgraphProvider = new providers_2.V4SubgraphProviderWithFallBacks([ new providers_2.CachingV4SubgraphProvider(chainId, new providers_2.URISubgraphProvider(chainId, `https://cloudflare-ipfs.com/ipns/api.uniswap.org/v1/pools/v4/${chainName}.json`, undefined, 0), new providers_2.NodeJSCache(new node_cache_1.default({ stdTTL: 300, useClones: false }))), new providers_2.StaticV4SubgraphProvider(chainId, this.v4PoolProvider, this.v4PoolParams), ]); } let gasPriceProviderInstance; if (providers_1.JsonRpcProvider.isProvider(this.provider)) { gasPriceProviderInstance = new providers_2.OnChainGasPriceProvider(chainId, new providers_2.EIP1559GasPriceProvider(this.provider), new providers_2.LegacyGasPriceProvider(this.provider)); } else { gasPriceProviderInstance = new providers_2.ETHGasStationInfoProvider(config_1.ETH_GAS_STATION_API_URL); } this.gasPriceProvider = gasPriceProvider !== null && gasPriceProvider !== void 0 ? gasPriceProvider : new providers_2.CachingGasStationProvider(chainId, gasPriceProviderInstance, new providers_2.NodeJSCache(new node_cache_1.default({ stdTTL: 7, useClones: false }))); this.v4GasModelFactory = v4GasModelFactory !== null && v4GasModelFactory !== void 0 ? v4GasModelFactory : new v4_heuristic_gas_model_1.V4HeuristicGasModelFactory(this.provider); this.v3GasModelFactory = v3GasModelFactory !== null && v3GasModelFactory !== void 0 ? v3GasModelFactory : new v3_heuristic_gas_model_1.V3HeuristicGasModelFactory(this.provider); this.v2GasModelFactory = v2GasModelFactory !== null && v2GasModelFactory !== void 0 ? v2GasModelFactory : new v2_heuristic_gas_model_1.V2HeuristicGasModelFactory(this.provider); this.mixedRouteGasModelFactory = mixedRouteGasModelFactory !== null && mixedRouteGasModelFactory !== void 0 ? mixedRouteGasModelFactory : new mixed_route_heuristic_gas_model_1.MixedRouteHeuristicGasModelFactory(); this.swapRouterProvider = swapRouterProvider !== null && swapRouterProvider !== void 0 ? swapRouterProvider : new providers_2.SwapRouterProvider(this.multicall2Provider, this.chainId); if (chainId === sdk_core_1.ChainId.ARBITRUM_ONE || chainId === sdk_core_1.ChainId.ARBITRUM_GOERLI) { this.l2GasDataProvider = arbitrumGasDataProvider !== null && arbitrumGasDataProvider !== void 0 ? arbitrumGasDataProvider : new gas_data_provider_1.ArbitrumGasDataProvider(chainId, this.provider); } // Initialize the Quoters. // Quoters are an abstraction encapsulating the business logic of fetching routes and quotes. this.v2Quoter = new quoters_1.V2Quoter(this.v2SubgraphProvider, this.v2PoolProvider, this.v2QuoteProvider, this.v2GasModelFactory, this.tokenProvider, this.chainId, this.blockedTokenListProvider, this.tokenValidatorProvider, this.l2GasDataProvider); this.v3Quoter = new quoters_1.V3Quoter(this.v3SubgraphProvider, this.v3PoolProvider, this.onChainQuoteProvider, this.tokenProvider, this.chainId, this.blockedTokenListProvider, this.tokenValidatorProvider); this.v4Quoter = new v4_quoter_1.V4Quoter(this.v4SubgraphProvider, this.v4PoolProvider, this.onChainQuoteProvider, this.tokenProvider, this.chainId, this.blockedTokenListProvider, this.tokenValidatorProvider); this.mixedQuoter = new quoters_1.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 : chains_1.V2_SUPPORTED; this.v4Supported = v4Supported !== null && v4Supported !== void 0 ? v4Supported : util_1.V4_SUPPORTED; this.mixedSupported = mixedSupported !== null && mixedSupported !== void 0 ? mixedSupported : util_1.MIXED_SUPPORTED; this.cachedRoutesCacheInvalidationFixRolloutPercentage = cachedRoutesCacheInvalidationFixRolloutPercentage; } async routeToRatio(token0Balance, token1Balance, position, swapAndAddConfig, swapAndAddOptions, routingConfig = (0, config_1.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 sdk_core_1.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_1.log.info('max iterations exceeded'); return { status: router_1.SwapToRatioStatus.NO_ROUTE_FOUND, error: 'max iterations exceeded', }; } const amountToSwap = (0, calculate_ratio_amount_in_1.calculateRatioAmountIn)(optimalRatio, exchangeRate, inputBalance, outputBalance); if (amountToSwap.equalTo(0)) { log_1.log.info(`no swap needed: amountToSwap = 0`); return { status: router_1.SwapToRatioStatus.NO_SWAP_NEEDED, }; } swap = await this.route(amountToSwap, outputBalance.currency, sdk_core_1.TradeType.EXACT_INPUT, undefined, Object.assign(Object.assign(Object.assign({}, (0, config_1.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: [router_sdk_1.Protocol.V3, router_sdk_1.Protocol.V2] })); if (!swap) { log_1.log.info('no route found from this.route()'); return { status: router_1.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 === router_sdk_1.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_1.default.BigInt(v3Route.sqrtPriceX96AfterList[i].toString()); optimalRatio = this.calculateOptimalRatio(position, jsbi_1.default.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 v3_sdk_1.Pool(position.pool.token0, position.pool.token1, position.pool.fee, targetPoolPriceUpdate, position.pool.liquidity, v3_sdk_1.TickMath.getTickAtSqrtRatio(targetPoolPriceUpdate), position.pool.tickDataProvider); } exchangeRate = swap.trade.outputAmount.divide(swap.trade.inputAmount); log_1.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_1.log.info('exchangeRate to 0'); return { status: router_1.SwapToRatioStatus.NO_ROUTE_FOUND, error: 'insufficient liquidity to swap to optimal ratio', }; } } if (!swap) { return { status: router_1.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: router_1.SwapToRatioStatus.SUCCESS, result: Object.assign(Object.assign({}, 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[(0, util_1.getAddressLowerCase)(currencyOut)]) === null || _a === void 0 ? void 0 : _a.tokenFeeResult) === null || _c === void 0 ? void 0 : _c.feeTakenOnTransfer; const externalTransferFailed = (_e = (_d = tokenOutProperties[(0, util_1.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[(0, util_1.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[(0, util_1.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) === router_1.SwapType.UNIVERSAL_ROUTER) { swapConfig.fee = undefined; swapConfig.flatFee = undefined; } metric_1.metric.putMetric('TokenOutFeeOnTransferNotTakingFee', 1, metric_1.MetricLoggerUnit.Count); } else { metric_1.metric.putMetric('TokenOutFeeOnTransferTakingFee', 1, metric_1.MetricLoggerUnit.Count); } } if (tradeType === sdk_core_1.TradeType.EXACT_OUTPUT) { const portionAmount = this.portionProvider.getPortionAmount(amount, tradeType, feeTakenOnTransfer, externalTransferFailed, swapConfig); if (portionAmount && portionAmount.greaterThan(router_sdk_1.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_1.metric.setProperty('chainId', this.chainId); metric_1.metric.setProperty('pair', `${currencyIn.symbol}/${currencyOut.symbol}`); metric_1.metric.setProperty('tokenIn', (0, util_1.getAddress)(currencyIn)); metric_1.metric.setProperty('tokenOut', (0, util_1.getAddress)(currencyOut)); metric_1.metric.setProperty('tradeType', tradeType === sdk_core_1.TradeType.EXACT_INPUT ? 'ExactIn' : 'ExactOut'); metric_1.metric.putMetric(`QuoteRequestedForChain${this.chainId}`, 1, metric_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 = lodash_1.default.merge({ // These settings could be changed by the partialRoutingConfig useCachedRoutes: true, writeToCachedRoutes: true, optimisticCachedRoutes: false, }, (0, config_1.DEFAULT_ROUTING_CONFIG_BY_CHAIN)(this.chainId), partialRoutingConfig, { blockNumber }); if (routingConfig.debugRouting) { log_1.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 = Object.assign(Object.assign({}, routingConfig), { blockNumber, additionalGasOverhead: (0, gas_costs_1.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(router_sdk_1.Protocol)); const requestedProtocolsSet = new Set(protocols); if (!swapConfig || swapConfig.type === router_1.SwapType.SWAP_ROUTER_02 || (swapConfig.type === router_1.SwapType.UNIVERSAL_ROUTER && swapConfig.version === universal_router_sdk_1.UniversalRouterVersion.V1_2)) { availableProtocolsSet.delete(router_sdk_1.Protocol.V4); if (requestedProtocolsSet.has(router_sdk_1.Protocol.V4)) { requestedProtocolsSet.delete(router_sdk_1.Protocol.V4); } } const enabledAndRequestedProtocolsMatch = availableProtocolsSet.size === requestedProtocolsSet.size && [...availableProtocolsSet].every((protocol) => requestedProtocolsSet.has(protocol)); log_1.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) === router_1.SwapType.UNIVERSAL_ROUTER ? swapConfig === null || swapConfig === void 0 ? void 0 : swapConfig.version : 'N/A', }); if (routingConfig.useCachedRoutes && cacheMode !== providers_2.CacheMode.Darkmode) { if (enabledAndRequestedProtocolsMatch) { if (protocols.includes(router_sdk_1.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, amounts_1.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, [router_sdk_1.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 providers_2.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 : defaultBlocksToLive_1.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 ((0, util_1.shouldWipeoutCachedRoutes)(cachedRoutes, routingConfig)) { cachedRoutes = undefined; } metric_1.metric.putMetric(routingConfig.useCachedRoutes ? 'GetQuoteUsingCachedRoutes' : 'GetQuoteNotUsingCachedRoutes', 1, metric_1.MetricLoggerUnit.Count); if (cacheMode && routingConfig.useCachedRoutes && cacheMode !== providers_2.CacheMode.Darkmode && !cachedRoutes) { metric_1.metric.putMetric(`GetCachedRoute_miss_${cacheMode}`, 1, metric_1.MetricLoggerUnit.Count); log_1.log.info({ currencyIn: currencyIn.symbol, currencyInAddress: (0, util_1.getAddress)(currencyIn), currencyOut: currencyOut.symbol, currencyOutAddress: (0, util_1.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_1.metric.putMetric(`GetCachedRoute_hit_${cacheMode}`, 1, metric_1.MetricLoggerUnit.Count); log_1.log.info({ currencyIn: currencyIn.symbol, currencyInAddress: (0, util_1.getAddress)(currencyIn), currencyOut: currencyOut.symbol, currencyOutAddress: (0, util_1.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 !== providers_2.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 === providers_2.CacheMode.Livemode && swapRouteFromCache) { log_1.log.info(`CacheMode is ${cacheMode}, and we are using swapRoute from cache`); hitsCachedRoute = true; swapRouteRaw = swapRouteFromCache; } else { log_1.log.info(`CacheMode is ${cacheMode}, and we are using materialized swapRoute`); swapRouteRaw = swapRouteFromChain; } if (cacheMode === providers_2.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_1.metric.putMetric(`TapcompareCachedRoute_quoteGasAdjustedDiffPercent`, Number(misquotePercent.toExact()), metric_1.MetricLoggerUnit.Percent); log_1.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_1.log.error({ quoteGasAdjustedDiff: quoteGasAdjustedDiff.toExact(), swapRouteFromChainQuoteGasAdjusted: swapRouteFromChain.quoteGasAdjusted.toExact(), }, 'Error calculating misquote percent'); metric_1.metric.putMetric(`TapcompareCachedRoute_quoteGasAdjustedDiffPercent_divzero`, 1, metric_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_1.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_1.metric.putMetric(`SetCachedRoute_NewPath`, 1, metric_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 // bef