@uniswap/smart-order-router
Version:
Uniswap Smart Order Router
722 lines • 168 kB
JavaScript
"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