UNPKG

@xspswap/smart-order-router

Version:
763 lines 87.1 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.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("@x-swap-protocol/router-sdk"); const sdk_core_1 = require("@x-swap-protocol/sdk-core"); const v3_sdk_1 = require("@x-swap-protocol/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 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 pool_provider_2 = require("../../providers/v3/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 log_1 = require("../../util/log"); const methodParameters_1 = require("../../util/methodParameters"); const metric_1 = require("../../util/metric"); const unsupported_tokens_1 = require("../../util/unsupported-tokens"); const router_1 = require("../router"); 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 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 quoters_1 = require("./quoters"); class MapWithLowerCaseKey extends Map { set(key, value) { return super.set(key.toLowerCase(), value); } } exports.MapWithLowerCaseKey = MapWithLowerCaseKey; class AlphaRouter { constructor({ chainId, provider, multicall2Provider, v3PoolProvider, onChainQuoteProvider, v2PoolProvider, v2QuoteProvider, v2SubgraphProvider, tokenProvider, blockedTokenListProvider, v3SubgraphProvider, gasPriceProvider, v3GasModelFactory, v2GasModelFactory, mixedRouteGasModelFactory, swapRouterProvider, tokenValidatorProvider, simulator, routeCachingProvider, }) { console.log('1.1.7'); this.chainId = chainId; this.provider = provider; this.multicall2Provider = multicall2Provider !== null && multicall2Provider !== void 0 ? multicall2Provider : new providers_2.UniswapMulticallProvider(chainId, provider, 100000); 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 ChainId.ARBITRUM_ONE: // this.onChainQuoteProvider = new OnChainQuoteProvider( // chainId, // provider, // this.multicall2Provider, // { // retries: 2, // minTimeout: 100, // maxTimeout: 1000, // }, // { // multicallChunk: 10, // gasLimitPerCall: 12_000_000, // quoteMinSuccessRate: 0.1, // }, // { // gasLimitOverride: 30_000_000, // multicallChunk: 6, // }, // { // gasLimitOverride: 30_000_000, // multicallChunk: 6, // } // ); // break; default: this.onChainQuoteProvider = new providers_2.OnChainQuoteProvider(chainId, provider, this.multicall2Provider, { retries: 2, minTimeout: 100, maxTimeout: 5000, }, { multicallChunk: 150, gasLimitPerCall: 1000000, quoteMinSuccessRate: 0.15, }, { gasLimitOverride: 70000, multicallChunk: 100, }); break; } } this.v2PoolProvider = v2PoolProvider !== null && v2PoolProvider !== void 0 ? v2PoolProvider : new providers_2.CachingV2PoolProvider(chainId, new pool_provider_1.V2PoolProvider(chainId, this.multicall2Provider), 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)); // 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.StaticV2SubgraphProvider(chainId), ]); } if (v3SubgraphProvider) { this.v3SubgraphProvider = v3SubgraphProvider; } else { this.v3SubgraphProvider = new providers_2.V3SubgraphProviderWithFallBacks([ new providers_2.StaticV3SubgraphProvider(chainId, this.v3PoolProvider), ]); } 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: 15, useClones: false }))); this.v3GasModelFactory = v3GasModelFactory !== null && v3GasModelFactory !== void 0 ? v3GasModelFactory : new v3_heuristic_gas_model_1.V3HeuristicGasModelFactory(); this.v2GasModelFactory = v2GasModelFactory !== null && v2GasModelFactory !== void 0 ? v2GasModelFactory : new v2_heuristic_gas_model_1.V2HeuristicGasModelFactory(); 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 (tokenValidatorProvider) { this.tokenValidatorProvider = tokenValidatorProvider; } else if (this.chainId === chains_1.ChainId.XDC) { 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 }))); } // 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.v3Quoter = new quoters_1.V3Quoter(this.v3SubgraphProvider, this.v3PoolProvider, this.onChainQuoteProvider, this.tokenProvider, this.chainId, this.blockedTokenListProvider, this.tokenValidatorProvider); this.mixedQuoter = new quoters_1.MixedQuoter(this.v3SubgraphProvider, this.v3PoolProvider, this.v2SubgraphProvider, this.v2PoolProvider, this.onChainQuoteProvider, this.tokenProvider, this.chainId, this.blockedTokenListProvider, this.tokenValidatorProvider); } 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; const { currencyIn, currencyOut } = this.determineCurrencyInOutFromTradeType(tradeType, amount, quoteCurrency); const tokenIn = currencyIn.wrapped; const tokenOut = currencyOut.wrapped; metric_1.metric.setProperty('chainId', this.chainId); metric_1.metric.setProperty('pair', `${tokenIn.symbol}/${tokenOut.symbol}`); metric_1.metric.setProperty('tokenIn', tokenIn.address); metric_1.metric.setProperty('tokenOut', tokenOut.address); 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 = (_a = partialRoutingConfig.blockNumber) !== null && _a !== void 0 ? _a : this.getBlockNumberPromise(); const routingConfig = lodash_1.default.merge({}, (0, config_1.DEFAULT_ROUTING_CONFIG_BY_CHAIN)(this.chainId), partialRoutingConfig, { blockNumber }); const gasPriceWei = await this.getGasPriceWei(); const quoteToken = quoteCurrency.wrapped; const [v3GasModel, mixedRouteGasModel] = await this.getGasModels(gasPriceWei, amount.currency.wrapped, quoteToken); // 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 = await ((_c = this.routeCachingProvider) === null || _c === void 0 ? void 0 : _c.getCacheMode(this.chainId, amount, quoteToken, tradeType, protocols)); // Fetch CachedRoutes let cachedRoutes; if (cacheMode !== providers_2.CacheMode.Darkmode) { cachedRoutes = await ((_d = this.routeCachingProvider) === null || _d === void 0 ? void 0 : _d.getCachedRoute(this.chainId, amount, quoteToken, tradeType, protocols, await blockNumber)); } if (cacheMode && cacheMode !== providers_2.CacheMode.Darkmode && !cachedRoutes) { metric_1.metric.putMetric(`GetCachedRoute_miss_${cacheMode}`, 1, metric_1.MetricLoggerUnit.Count); log_1.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) { metric_1.metric.putMetric(`GetCachedRoute_hit_${cacheMode}`, 1, metric_1.MetricLoggerUnit.Count); log_1.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); } let swapRouteFromChainPromise = Promise.resolve(null); if (!cachedRoutes || cacheMode !== providers_2.CacheMode.Livemode) { swapRouteFromChainPromise = this.getSwapRouteFromChain(amount, tokenIn, tokenOut, protocols, quoteToken, tradeType, routingConfig, v3GasModel, mixedRouteGasModel, gasPriceWei); } const [swapRouteFromCache, swapRouteFromChain] = await Promise.all([ swapRouteFromCachePromise, swapRouteFromChainPromise, ]); let swapRouteRaw; if (cacheMode === providers_2.CacheMode.Livemode && swapRouteFromCache) { log_1.log.info(`CacheMode is ${cacheMode}, and we are using swapRoute from cache`); 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); log_1.log.info({ 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(), pair: this.tokenPairSymbolTradeTypeChainId(tokenIn, tokenOut, tradeType), }, `Comparing quotes between Chain and Cache for ${this.tokenPairSymbolTradeTypeChainId(tokenIn, tokenOut, tradeType)}`); } if (!swapRouteRaw) { return null; } const { quote, quoteGasAdjusted, estimatedGasUsed, routes: routeAmounts, estimatedGasUsedQuoteToken, estimatedGasUsedUSD, } = swapRouteRaw; if (this.routeCachingProvider && cacheMode !== providers_2.CacheMode.Darkmode && swapRouteFromChain) { // Generate the object to be cached const routesToCache = providers_2.CachedRoutes.fromRoutesWithValidQuotes(swapRouteFromChain.routes, this.chainId, tokenIn, tokenOut, protocols.sort(), // sort it for consistency in the order of the protocols. await blockNumber, tradeType); if (routesToCache) { const tokenPairSymbolTradeTypeChainId = this.tokenPairSymbolTradeTypeChainId(tokenIn, tokenOut, tradeType); // 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_1.metric.putMetric(`SetCachedRoute_${tokenPairSymbolTradeTypeChainId}_${status}`, 1, metric_1.MetricLoggerUnit.Count); }) .catch((reason) => { log_1.log.info({ reason: reason, tokenPair: tokenPairSymbolTradeTypeChainId, }, `SetCachedRoute failure`); metric_1.metric.putMetric(`SetCachedRoute_${tokenPairSymbolTradeTypeChainId}_failure`, 1, metric_1.MetricLoggerUnit.Count); }); } } metric_1.metric.putMetric(`QuoteFoundForChain${this.chainId}`, 1, metric_1.MetricLoggerUnit.Count); // Build Trade object that represents the optimal swap. const trade = (0, methodParameters_1.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 = (0, methodParameters_1.buildSwapMethodParameters)(trade, swapConfig, this.chainId); } const swapRoute = { quote, quoteGasAdjusted, estimatedGasUsed, estimatedGasUsedQuoteToken, estimatedGasUsedUSD, gasPriceWei, route: routeAmounts, trade, methodParameters, blockNumber: bignumber_1.BigNumber.from(await blockNumber), }; if (swapConfig && swapConfig.simulate && methodParameters && methodParameters.calldata) { if (!this.simulator) { throw new Error('Simulator not initialized!'); } log_1.log.info({ swapConfig, methodParameters }, '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 amounts_1.CurrencyAmount.fromRawAmount(quoteCurrency, quote.quotient.toString())); metric_1.metric.putMetric('SimulateTransaction', Date.now() - beforeSimulate, metric_1.MetricLoggerUnit.Milliseconds); return swapRouteWithSimulation; } return swapRoute; } async getSwapRouteFromCache(cachedRoutes, blockNumber, amount, quoteToken, tradeType, routingConfig, v3GasModel, mixedRouteGasModel, gasPriceWei) { log_1.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 === router_sdk_1.Protocol.V3); const v2Routes = cachedRoutes.routes.filter((route) => route.protocol === router_sdk_1.Protocol.V2); const mixedRoutes = cachedRoutes.routes.filter((route) => route.protocol === router_sdk_1.Protocol.MIXED); const percents = []; if (v3Routes) { const v3RoutesFromCache = v3Routes.map((cachedRoute) => cachedRoute.route); const v3PercentsFromCache = v3Routes.map((cachedRoute) => cachedRoute.percent); percents.push(...v3PercentsFromCache); const v3Amounts = v3PercentsFromCache.map((percent) => amount.multiply(new sdk_core_1.Fraction(percent, 100))); quotePromises.push(this.v3Quoter.getQuotes(v3RoutesFromCache, v3Amounts, v3PercentsFromCache, quoteToken, tradeType, routingConfig, undefined, v3GasModel)); } if (v2Routes) { const v2RoutesFromCache = v2Routes.map((cachedRoute) => cachedRoute.route); const v2PercentsFromCache = v2Routes.map((cachedRoute) => cachedRoute.percent); percents.push(...v2PercentsFromCache); const v2Amounts = v2PercentsFromCache.map((percent) => amount.multiply(new sdk_core_1.Fraction(percent, 100))); quotePromises.push(this.v2Quoter.getQuotes(v2RoutesFromCache, v2Amounts, v2PercentsFromCache, quoteToken, tradeType, routingConfig, undefined, undefined, gasPriceWei)); } if (mixedRoutes) { const mixedRoutesFromCache = mixedRoutes.map((cachedRoute) => cachedRoute.route); const mixedPercentsFromCache = mixedRoutes.map((cachedRoute) => cachedRoute.percent); percents.push(...mixedPercentsFromCache); const mixedAmounts = mixedPercentsFromCache.map((percent) => amount.multiply(new sdk_core_1.Fraction(percent, 100))); quotePromises.push(this.mixedQuoter.getQuotes(mixedRoutesFromCache, mixedAmounts, mixedPercentsFromCache, quoteToken, tradeType, routingConfig, undefined, mixedRouteGasModel)); } const getQuotesResults = await Promise.all(quotePromises); const allRoutesWithValidQuotes = lodash_1.default.flatMap(getQuotesResults, (quoteResult) => quoteResult.routesWithValidQuotes); return (0, best_swap_route_1.getBestSwapRoute)(amount, percents, allRoutesWithValidQuotes, tradeType, this.chainId, routingConfig, v3GasModel); } async getSwapRouteFromChain(amount, tokenIn, tokenOut, protocols, quoteToken, tradeType, routingConfig, v3GasModel, mixedRouteGasModel, gasPriceWei) { // 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(router_sdk_1.Protocol.V3); const v2ProtocolSpecified = protocols.includes(router_sdk_1.Protocol.V2); const v2SupportedInChain = chains_1.V2_SUPPORTED.includes(this.chainId); const shouldQueryMixedProtocol = protocols.includes(router_sdk_1.Protocol.MIXED) || (noProtocolsSpecified && v2SupportedInChain); const mixedProtocolAllowed = util_1.SUPPORTED_CHAINS.includes(this.chainId) && tradeType === sdk_core_1.TradeType.EXACT_INPUT; const quotePromises = []; // Maybe Quote V3 - if V3 is specified, or no protocol is specified if (v3ProtocolSpecified || noProtocolsSpecified) { // console.log('v3', tokenIn.symbol, tokenOut.symbol); log_1.log.info({ protocols, tradeType }, 'Routing across V3'); quotePromises.push(this.v3Quoter.getRoutesThenQuotes(tokenIn, tokenOut, amounts, percents, quoteToken, tradeType, routingConfig, v3GasModel)); } // Maybe Quote V2 - if V2 is specified, or no protocol is specified AND v2 is supported in this chain if (v2SupportedInChain && (v2ProtocolSpecified || noProtocolsSpecified)) { // console.log('v2', tokenIn.symbol, tokenOut.symbol); log_1.log.info({ protocols, tradeType }, 'Routing across V2'); quotePromises.push(this.v2Quoter.getRoutesThenQuotes(tokenIn, tokenOut, amounts, percents, quoteToken, tradeType, routingConfig, undefined, gasPriceWei)); } // 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) { // console.log('mix', tokenIn.symbol, tokenOut.symbol); log_1.log.info({ protocols, tradeType }, 'Routing across MixedRoutes'); quotePromises.push(this.mixedQuoter.getRoutesThenQuotes(tokenIn, tokenOut, amounts, percents, quoteToken, tradeType, routingConfig, mixedRouteGasModel)); } 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_1.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 (0, best_swap_route_1.getBestSwapRoute)(amount, percents, allRoutesWithValidQuotes, tradeType, this.chainId, routingConfig, v3GasModel); if (bestSwapRoute) { this.emitPoolSelectionMetrics(bestSwapRoute, allCandidatePools); } return bestSwapRoute; } tradeTypeStr(tradeType) { return tradeType === sdk_core_1.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 === sdk_core_1.TradeType.EXACT_INPUT) { return { currencyIn: amount.currency, currencyOut: quoteCurrency, }; } else { return { currencyIn: quoteCurrency, currencyOut: amount.currency, }; } } async getGasPriceWei() { // 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(); metric_1.metric.putMetric('GasPriceLoad', Date.now() - beforeGasTimestamp, metric_1.MetricLoggerUnit.Milliseconds); return gasPriceWei; } async getGasModels(gasPriceWei, amountToken, quoteToken) { const beforeGasModel = Date.now(); const v3GasModelPromise = this.v3GasModelFactory.buildGasModel({ chainId: this.chainId, gasPriceWei, v3poolProvider: this.v3PoolProvider, amountToken, quoteToken, v2poolProvider: this.v2PoolProvider, // l2GasDataProvider: this.l2GasDataProvider, }); const mixedRouteGasModelPromise = this.mixedRouteGasModelFactory.buildGasModel({ chainId: this.chainId, gasPriceWei, v3poolProvider: this.v3PoolProvider, amountToken, quoteToken, v2poolProvider: this.v2PoolProvider, }); const [v3GasModel, mixedRouteGasModel] = await Promise.all([ v3GasModelPromise, mixedRouteGasModelPromise, ]); metric_1.metric.putMetric('GasModelCreation', Date.now() - beforeGasModel, metric_1.MetricLoggerUnit.Milliseconds); return [v3GasModel, 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 sdk_core_1.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 Object.assign(Object.assign({}, router_sdk_1.SwapRouter.swapAndAddCallParameters(trade, { recipient, slippageTolerance, deadlineOrPreviousBlockhash: deadline, inputTokenPermit, }, v3_sdk_1.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: (0, util_1.SWAP_ROUTER_02_ADDRESSES)(this.chainId) }); } emitPoolSelectionMetrics(swapRouteRaw, allPoolsBySelection) { const poolAddressesUsed = new Set(); const { routes: routeAmounts } = swapRouteRaw; (0, lodash_1.default)(routeAmounts) .flatMap((routeAmount) => { const { poolAddresses } = routeAmount; return poolAddresses; }) .forEach((address) => { poolAddressesUsed.add(address.toLowerCase()); }); for (const poolsBySelection of allPoolsBySelection) { const { protocol } = poolsBySelection; lodash_1.default.forIn(poolsBySelection.selections, (pools, topNSelection) => { const topNUsed = lodash_1.default.findLastIndex(pools, (pool) => poolAddressesUsed.has(pool.id.toLowerCase())) + 1; metric_1.metric.putMetric(lodash_1.default.capitalize(`${protocol}${topNSelection}`), topNUsed, metric_1.MetricLoggerUnit.Count); }); } let hasV3Route = false; let hasV2Route = false; let hasMixedRoute = false; for (const routeAmount of routeAmounts) { if (routeAmount.protocol === router_sdk_1.Protocol.V3) { hasV3Route = true; } if (routeAmount.protocol === router_sdk_1.Protocol.V2) { hasV2Route = true; } if (routeAmount.protocol === router_sdk_1.Protocol.MIXED) { hasMixedRoute = true; } } if (hasMixedRoute && (hasV3Route || hasV2Route)) { if (hasV3Route && hasV2Route) { metric_1.metric.putMetric(`MixedAndV3AndV2SplitRoute`, 1, metric_1.MetricLoggerUnit.Count); metric_1.metric.putMetric(`MixedAndV3AndV2SplitRouteForChain${this.chainId}`, 1, metric_1.MetricLoggerUnit.Count); } else if (hasV3Route) { metric_1.metric.putMetric(`MixedAndV3SplitRoute`, 1, metric_1.MetricLoggerUnit.Count); metric_1.metric.putMetric(`MixedAndV3SplitRouteForChain${this.chainId}`, 1, metric_1.MetricLoggerUnit.Count); } else if (hasV2Route) { metric_1.metric.putMetric(`MixedAndV2SplitRoute`, 1, metric_1.MetricLoggerUnit.Count); metric_1.metric.putMetric(`MixedAndV2SplitRouteForChain${this.chainId}`, 1, metric_1.MetricLoggerUnit.Count); } } else if (hasV3Route && hasV2Route) { metric_1.metric.putMetric(`V3AndV2SplitRoute`, 1, metric_1.MetricLoggerUnit.Count); metric_1.metric.putMetric(`V3AndV2SplitRouteForChain${this.chainId}`, 1, metric_1.MetricLoggerUnit.Count); } else if (hasMixedRoute) { if (routeAmounts.length > 1) { metric_1.metric.putMetric(`MixedSplitRoute`, 1, metric_1.MetricLoggerUnit.Count); metric_1.metric.putMetric(`MixedSplitRouteForChain${this.chainId}`, 1, metric_1.MetricLoggerUnit.Count); } else { metric_1.metric.putMetric(`MixedRoute`, 1, metric_1.MetricLoggerUnit.Count); metric_1.metric.putMetric(`MixedRouteForChain${this.chainId}`, 1, metric_1.MetricLoggerUnit.Count); } } else if (hasV3Route) { if (routeAmounts.length > 1) { metric_1.metric.putMetric(`V3SplitRoute`, 1, metric_1.MetricLoggerUnit.Count); metric_1.metric.putMetric(`V3SplitRouteForChain${this.chainId}`, 1, metric_1.MetricLoggerUnit.Count); } else { metric_1.metric.putMetric(`V3Route`, 1, metric_1.MetricLoggerUnit.Count); metric_1.metric.putMetric(`V3RouteForChain${this.chainId}`, 1, metric_1.MetricLoggerUnit.Count); } } else if (hasV2Route) { if (routeAmounts.length > 1) { metric_1.metric.putMetric(`V2SplitRoute`, 1, metric_1.MetricLoggerUnit.Count); metric_1.metric.putMetric(`V2SplitRouteForChain${this.chainId}`, 1, metric_1.MetricLoggerUnit.Count); } else { metric_1.metric.putMetric(`V2Route`, 1, metric_1.MetricLoggerUnit.Count); metric_1.metric.putMetric(`V2RouteForChain${this.chainId}`, 1, metric_1.MetricLoggerUnit.Count); } } } calculateOptimalRatio(position, sqrtRatioX96, zeroForOne) { const upperSqrtRatioX96 = v3_sdk_1.TickMath.getSqrtRatioAtTick(position.tickUpper); const lowerSqrtRatioX96 = v3_sdk_1.TickMath.getSqrtRatioAtTick(position.tickLower); // returns Fraction(0, 1) for any out of range position regardless of zeroForOne. Implication: function // cannot be used to determine the trading direction of out of range positions. if (jsbi_1.default.greaterThan(sqrtRatioX96, upperSqrtRatioX96) || jsbi_1.default.lessThan(sqrtRatioX96, lowerSqrtRatioX96)) { return new sdk_core_1.Fraction(0, 1); } const precision = jsbi_1.default.BigInt('1' + '0'.repeat(18)); let optimalRatio = new sdk_core_1.Fraction(v3_sdk_1.SqrtPriceMath.getAmount0Delta(sqrtRatioX96, upperSqrtRatioX96, precision, true), v3_sdk_1.SqrtPriceMath.getAmount1Delta(sqrtRatioX96, lowerSqrtRatioX96, precision, true)); if (!zeroForOne) optimalRatio = optimalRatio.invert(); return optimalRatio; } async userHasSufficientBalance(fromAddress, tradeType, amount, quote) { try { const neededBalance = tradeType === sdk_core_1.TradeType.EXACT_INPUT ? amount : quote; let balance; if (neededBalance.currency.isNative) { balance = await this.provider.getBalance(fromAddress); } else { const tokenContract = Erc20__factory_1.Erc20__factory.connect(neededBalance.currency.address, this.provider); balance = await tokenContract.balanceOf(fromAddress); } return balance.gte(bignumber_1.BigNumber.from(neededBalance.quotient.toString())); } catch (e) { log_1.log.error(e, 'Error while checking user balance'); return false; } } absoluteValue(fraction) { const numeratorAbs = jsbi_1.default.lessThan(fraction.numerator, jsbi_1.default.BigInt(0)) ? jsbi_1.default.unaryMinus(fraction.numerator) : fraction.numerator; const denominatorAbs = jsbi_1.default.lessThan(fraction.denominator, jsbi_1.default.BigInt(0)) ? jsbi_1.default.unaryMinus(fraction.denominator) : fraction.denominator; return new sdk_core_1.Fraction(numeratorAbs, denominatorAbs); } getBlockNumberPromise() { return (0, async_retry_1.default)(async (_b, attempt) => { if (attempt > 1) { log_1.log.info(`Get block number attempt ${attempt}`); } return this.provider.getBlockNumber(); }, { retries: 2, minTimeout: 100, maxTimeout: 1000, }); } } exports.AlphaRouter = AlphaRouter; //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYWxwaGEtcm91dGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL3JvdXRlcnMvYWxwaGEtcm91dGVyL2FscGhhLXJvdXRlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7QUFBQSx3REFBcUQ7QUFDckQsd0RBQXlFO0FBQ3pFLHFGQUE2RDtBQUU3RCw0REFBMEU7QUFDMUUsd0RBS21DO0FBQ25DLG9EQUtpQztBQUNqQyw4REFBZ0M7QUFDaEMsZ0RBQXdCO0FBQ3hCLG9EQUF1QjtBQUN2Qiw0REFBbUM7QUFFbkMsK0NBMEJ5QjtBQUN6Qiw2RkFHcUQ7QUFLckQsbUVBQStFO0FBQy9FLHVGQUdrRDtBQUNsRCxvRUFHMEM7QUFNMUMsb0VBRzBDO0FBRTFDLCtFQUE0RTtBQUM1RSxxQ0FBd0U7QUFDeEUsZ0RBQW9EO0FBQ3BELDhDQUEwRTtBQUMxRSx3Q0FBcUM7QUFDckMsa0VBR3FDO0FBQ3JDLDhDQUE2RDtBQUM3RCxzRUFBbUU7QUFDbkUsc0NBY21CO0FBRW5CLHFDQUdrQjtBQU1sQixpRUFBOEU7QUFDOUUscUZBQStFO0FBVS9FLDZHQUE2RztBQUM3RyxtRkFBb0Y7QUFDcEYsbUZBQW9GO0FBQ3BGLHVDQUE2RTtBQXdHN0UsTUFBYSxtQkFBdUIsU0FBUSxHQUFjO0lBQy9DLEdBQUcsQ0FBQyxHQUFXLEVBQUUsS0FBUTtRQUNoQyxPQUFPLEtBQUssQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLFdBQVcsRUFBRSxFQUFFLEtBQUssQ0FBQyxDQUFDO0lBQzdDLENBQUM7Q0FDRjtBQUpELGtEQUlDO0FBOEdELE1BQWEsV0FBVztJQStCdEIsWUFBWSxFQUNWLE9BQU8sRUFDUCxRQUFRLEVBQ1Isa0JBQWtCLEVBQ2xCLGNBQWMsRUFDZCxvQkFBb0IsRUFDcEIsY0FBYyxFQUNkLGVBQWUsRUFDZixrQkFBa0IsRUFDbEIsYUFBYSxFQUNiLHdCQUF3QixFQUN4QixrQkFBa0IsRUFDbEIsZ0JBQWdCLEVBQ2hCLGlCQUFp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