@maxosllc/smart-order-router
Version:
BlockDAG Smart Order Router
749 lines • 172 kB
JavaScript
import { BigNumber } from '@ethersproject/bignumber';
import { JsonRpcProvider } from '@ethersproject/providers';
import DEFAULT_TOKEN_LIST from '@uniswap/default-token-list';
import { Protocol, SwapRouter, ZERO } from '@uniswap/router-sdk';
import { Fraction, TradeType, } from '@uniswap/sdk-core';
import { ChainId } from '../../../src/util/chains';
import { Pool, Position, SqrtPriceMath, TickMath } from '@uniswap/v3-sdk';
import retry from 'async-retry';
import JSBI from 'jsbi';
import _ from 'lodash';
import NodeCache from 'node-cache';
import { CachedRoutes, CacheMode, CachingGasStationProvider, CachingTokenProviderWithFallback, CachingV2PoolProvider, CachingV2SubgraphProvider, CachingV3PoolProvider, CachingV3SubgraphProvider, CachingV4SubgraphProvider, EIP1559GasPriceProvider, ETHGasStationInfoProvider, LegacyGasPriceProvider, NodeJSCache, OnChainGasPriceProvider, OnChainQuoteProvider, StaticV2SubgraphProvider, StaticV3SubgraphProvider, StaticV4SubgraphProvider, SwapRouterProvider, TokenPropertiesProvider, UniswapMulticallProvider, URISubgraphProvider, V2QuoteProvider, V2SubgraphProviderWithFallBacks, V3SubgraphProviderWithFallBacks, V4SubgraphProviderWithFallBacks, } from '../../providers';
import { CachingTokenListProvider, } from '../../providers/caching-token-list-provider';
import { PortionProvider, } from '../../providers/portion-provider';
import { OnChainTokenFeeFetcher } from '../../providers/token-fee-fetcher';
import { TokenProvider } from '../../providers/token-provider';
import { TokenValidatorProvider, } from '../../providers/token-validator-provider';
import { V2PoolProvider, } from '../../providers/v2/pool-provider';
import { ArbitrumGasDataProvider, } from '../../providers/v3/gas-data-provider';
import { V3PoolProvider, } from '../../providers/v3/pool-provider';
import { CachingV4PoolProvider } from '../../providers/v4/caching-pool-provider';
import { V4PoolProvider, } from '../../providers/v4/pool-provider';
import { Erc20__factory } from '../../types/other/factories/Erc20__factory';
import { getAddress, getAddressLowerCase, getApplicableV4FeesTickspacingsHooks, HooksOptions, MIXED_SUPPORTED, shouldWipeoutCachedRoutes, SWAP_ROUTER_02_ADDRESSES, V4_SUPPORTED, WRAPPED_NATIVE_CURRENCY, } from '../../util';
import { CurrencyAmount } from '../../util/amounts';
import { ID_TO_CHAIN_ID, ID_TO_NETWORK_NAME, V2_SUPPORTED, } from '../../util/chains';
import { getHighestLiquidityV3NativePool, getHighestLiquidityV3USDPool, } from '../../util/gas-factory-helpers';
import { log } from '../../util/log';
import { buildSwapMethodParameters, buildTrade, } from '../../util/methodParameters';
import { metric, MetricLoggerUnit } from '../../util/metric';
import { BATCH_PARAMS, BLOCK_NUMBER_CONFIGS, DEFAULT_BATCH_PARAMS, DEFAULT_BLOCK_NUMBER_CONFIGS, DEFAULT_GAS_ERROR_FAILURE_OVERRIDES, DEFAULT_RETRY_OPTIONS, DEFAULT_SUCCESS_RATE_FAILURE_OVERRIDES, GAS_ERROR_FAILURE_OVERRIDES, RETRY_OPTIONS, SUCCESS_RATE_FAILURE_OVERRIDES, } from '../../util/onchainQuoteProviderConfigs';
import { UNSUPPORTED_TOKENS } from '../../util/unsupported-tokens';
import { SwapToRatioStatus, SwapType, } from '../router';
import { UniversalRouterVersion } from '@uniswap/universal-router-sdk';
import { DEFAULT_BLOCKS_TO_LIVE } from '../../util/defaultBlocksToLive';
import { INTENT } from '../../util/intent';
import { serializeRouteIds } from '../../util/serializeRouteIds';
import { DEFAULT_ROUTING_CONFIG_BY_CHAIN, ETH_GAS_STATION_API_URL, } from './config';
import { getBestSwapRoute } from './functions/best-swap-route';
import { calculateRatioAmountIn } from './functions/calculate-ratio-amount-in';
import { getMixedCrossLiquidityCandidatePools, getV2CandidatePools, getV3CandidatePools, getV4CandidatePools, } from './functions/get-candidate-pools';
import { NATIVE_OVERHEAD } from './gas-models/gas-costs';
import { MixedRouteHeuristicGasModelFactory } from './gas-models/mixedRoute/mixed-route-heuristic-gas-model';
import { V2HeuristicGasModelFactory } from './gas-models/v2/v2-heuristic-gas-model';
import { V3HeuristicGasModelFactory } from './gas-models/v3/v3-heuristic-gas-model';
import { V4HeuristicGasModelFactory } from './gas-models/v4/v4-heuristic-gas-model';
import { MixedQuoter, V2Quoter, V3Quoter } from './quoters';
import { V4Quoter } from './quoters/v4-quoter';
export class MapWithLowerCaseKey extends Map {
set(key, value) {
return super.set(key.toLowerCase(), value);
}
}
export class LowerCaseStringArray extends Array {
constructor(...items) {
// Convert all items to lowercase before calling the parent constructor
super(...items.map((item) => item.toLowerCase()));
}
}
export class AlphaRouter {
constructor({ chainId, provider, multicall2Provider, v4SubgraphProvider, v4PoolProvider, v3PoolProvider, onChainQuoteProvider, v2PoolProvider, v2QuoteProvider, v2SubgraphProvider, tokenProvider, blockedTokenListProvider, v3SubgraphProvider, gasPriceProvider, v4GasModelFactory, v3GasModelFactory, v2GasModelFactory, mixedRouteGasModelFactory, swapRouterProvider, tokenValidatorProvider, arbitrumGasDataProvider, simulator, routeCachingProvider, tokenPropertiesProvider, portionProvider, v2Supported, v4Supported, mixedSupported, v4PoolParams, cachedRoutesCacheInvalidationFixRolloutPercentage, }) {
this.chainId = chainId;
this.provider = provider;
this.multicall2Provider =
multicall2Provider !== null && multicall2Provider !== void 0 ? multicall2Provider : new UniswapMulticallProvider(chainId, provider, 375000);
this.v4PoolProvider =
v4PoolProvider !== null && v4PoolProvider !== void 0 ? v4PoolProvider : new CachingV4PoolProvider(this.chainId, new V4PoolProvider(ID_TO_CHAIN_ID(chainId), this.multicall2Provider), new NodeJSCache(new NodeCache({ stdTTL: 360, useClones: false })));
this.v3PoolProvider =
v3PoolProvider !== null && v3PoolProvider !== void 0 ? v3PoolProvider : new CachingV3PoolProvider(this.chainId, new V3PoolProvider(ID_TO_CHAIN_ID(chainId), this.multicall2Provider), new NodeJSCache(new NodeCache({ stdTTL: 360, useClones: false })));
this.simulator = simulator;
this.routeCachingProvider = routeCachingProvider;
if (onChainQuoteProvider) {
this.onChainQuoteProvider = onChainQuoteProvider;
}
else {
switch (chainId) {
case ChainId.OPTIMISM:
case ChainId.OPTIMISM_GOERLI:
case ChainId.OPTIMISM_SEPOLIA:
this.onChainQuoteProvider = new OnChainQuoteProvider(chainId, provider, this.multicall2Provider, {
retries: 2,
minTimeout: 100,
maxTimeout: 1000,
}, (_) => {
return {
multicallChunk: 110,
gasLimitPerCall: 1200000,
quoteMinSuccessRate: 0.1,
};
}, (_) => {
return {
gasLimitOverride: 3000000,
multicallChunk: 45,
};
}, (_) => {
return {
gasLimitOverride: 3000000,
multicallChunk: 45,
};
}, (_) => {
return {
baseBlockOffset: -10,
rollback: {
enabled: true,
attemptsBeforeRollback: 1,
rollbackBlockOffset: -10,
},
};
});
break;
case ChainId.BASE:
case ChainId.BLAST:
case ChainId.ZORA:
case ChainId.WORLDCHAIN:
case ChainId.UNICHAIN_SEPOLIA:
case ChainId.MONAD_TESTNET:
case ChainId.BASE_SEPOLIA:
case ChainId.UNICHAIN:
case ChainId.BASE_GOERLI:
case ChainId.SONEIUM:
this.onChainQuoteProvider = new OnChainQuoteProvider(chainId, provider, this.multicall2Provider, {
retries: 2,
minTimeout: 100,
maxTimeout: 1000,
}, (_) => {
return {
multicallChunk: 80,
gasLimitPerCall: 1200000,
quoteMinSuccessRate: 0.1,
};
}, (_) => {
return {
gasLimitOverride: 3000000,
multicallChunk: 45,
};
}, (_) => {
return {
gasLimitOverride: 3000000,
multicallChunk: 45,
};
}, (_) => {
return {
baseBlockOffset: -10,
rollback: {
enabled: true,
attemptsBeforeRollback: 1,
rollbackBlockOffset: -10,
},
};
});
break;
case ChainId.ZKSYNC:
this.onChainQuoteProvider = new OnChainQuoteProvider(chainId, provider, this.multicall2Provider, {
retries: 2,
minTimeout: 100,
maxTimeout: 1000,
}, (_) => {
return {
multicallChunk: 27,
gasLimitPerCall: 3000000,
quoteMinSuccessRate: 0.1,
};
}, (_) => {
return {
gasLimitOverride: 6000000,
multicallChunk: 13,
};
}, (_) => {
return {
gasLimitOverride: 6000000,
multicallChunk: 13,
};
}, (_) => {
return {
baseBlockOffset: -10,
rollback: {
enabled: true,
attemptsBeforeRollback: 1,
rollbackBlockOffset: -10,
},
};
});
break;
case ChainId.ARBITRUM_ONE:
case ChainId.ARBITRUM_GOERLI:
case ChainId.ARBITRUM_SEPOLIA:
this.onChainQuoteProvider = new OnChainQuoteProvider(chainId, provider, this.multicall2Provider, {
retries: 2,
minTimeout: 100,
maxTimeout: 1000,
}, (_) => {
return {
multicallChunk: 10,
gasLimitPerCall: 12000000,
quoteMinSuccessRate: 0.1,
};
}, (_) => {
return {
gasLimitOverride: 30000000,
multicallChunk: 6,
};
}, (_) => {
return {
gasLimitOverride: 30000000,
multicallChunk: 6,
};
});
break;
case ChainId.CELO:
case ChainId.CELO_ALFAJORES:
this.onChainQuoteProvider = new OnChainQuoteProvider(chainId, provider, this.multicall2Provider, {
retries: 2,
minTimeout: 100,
maxTimeout: 1000,
}, (_) => {
return {
multicallChunk: 10,
gasLimitPerCall: 5000000,
quoteMinSuccessRate: 0.1,
};
}, (_) => {
return {
gasLimitOverride: 5000000,
multicallChunk: 5,
};
}, (_) => {
return {
gasLimitOverride: 6250000,
multicallChunk: 4,
};
});
break;
case ChainId.POLYGON_MUMBAI:
case ChainId.SEPOLIA:
case ChainId.MAINNET:
case ChainId.POLYGON:
this.onChainQuoteProvider = new OnChainQuoteProvider(chainId, provider, this.multicall2Provider, RETRY_OPTIONS[chainId], (_) => BATCH_PARAMS[chainId], (_) => GAS_ERROR_FAILURE_OVERRIDES[chainId], (_) => SUCCESS_RATE_FAILURE_OVERRIDES[chainId], (_) => BLOCK_NUMBER_CONFIGS[chainId]);
break;
default:
this.onChainQuoteProvider = new OnChainQuoteProvider(chainId, provider, this.multicall2Provider, DEFAULT_RETRY_OPTIONS, (_) => DEFAULT_BATCH_PARAMS, (_) => DEFAULT_GAS_ERROR_FAILURE_OVERRIDES, (_) => DEFAULT_SUCCESS_RATE_FAILURE_OVERRIDES, (_) => DEFAULT_BLOCK_NUMBER_CONFIGS);
break;
}
}
if (tokenValidatorProvider) {
this.tokenValidatorProvider = tokenValidatorProvider;
}
else if (this.chainId === ChainId.MAINNET) {
this.tokenValidatorProvider = new TokenValidatorProvider(this.chainId, this.multicall2Provider, new NodeJSCache(new NodeCache({ stdTTL: 30000, useClones: false })));
}
if (tokenPropertiesProvider) {
this.tokenPropertiesProvider = tokenPropertiesProvider;
}
else {
this.tokenPropertiesProvider = new TokenPropertiesProvider(this.chainId, new NodeJSCache(new NodeCache({ stdTTL: 86400, useClones: false })), new OnChainTokenFeeFetcher(this.chainId, provider));
}
this.v2PoolProvider =
v2PoolProvider !== null && v2PoolProvider !== void 0 ? v2PoolProvider : new CachingV2PoolProvider(chainId, new V2PoolProvider(chainId, this.multicall2Provider, this.tokenPropertiesProvider), new NodeJSCache(new NodeCache({ stdTTL: 60, useClones: false })));
this.v2QuoteProvider = v2QuoteProvider !== null && v2QuoteProvider !== void 0 ? v2QuoteProvider : new V2QuoteProvider();
this.blockedTokenListProvider =
blockedTokenListProvider !== null && blockedTokenListProvider !== void 0 ? blockedTokenListProvider : new CachingTokenListProvider(chainId, UNSUPPORTED_TOKENS, new NodeJSCache(new NodeCache({ stdTTL: 3600, useClones: false })));
this.tokenProvider =
tokenProvider !== null && tokenProvider !== void 0 ? tokenProvider : new CachingTokenProviderWithFallback(chainId, new NodeJSCache(new NodeCache({ stdTTL: 3600, useClones: false })), new CachingTokenListProvider(chainId, DEFAULT_TOKEN_LIST, new NodeJSCache(new NodeCache({ stdTTL: 3600, useClones: false }))), new TokenProvider(chainId, this.multicall2Provider));
this.portionProvider = portionProvider !== null && portionProvider !== void 0 ? portionProvider : new PortionProvider();
const chainName = ID_TO_NETWORK_NAME(chainId);
// ipfs urls in the following format: `https://cloudflare-ipfs.com/ipns/api.uniswap.org/v1/pools/${protocol}/${chainName}.json`;
if (v2SubgraphProvider) {
this.v2SubgraphProvider = v2SubgraphProvider;
}
else {
this.v2SubgraphProvider = new V2SubgraphProviderWithFallBacks([
new CachingV2SubgraphProvider(chainId, new URISubgraphProvider(chainId, `https://cloudflare-ipfs.com/ipns/api.uniswap.org/v1/pools/v2/${chainName}.json`, undefined, 0), new NodeJSCache(new NodeCache({ stdTTL: 300, useClones: false }))),
new StaticV2SubgraphProvider(chainId),
]);
}
if (v3SubgraphProvider) {
this.v3SubgraphProvider = v3SubgraphProvider;
}
else {
this.v3SubgraphProvider = new V3SubgraphProviderWithFallBacks([
new CachingV3SubgraphProvider(chainId, new URISubgraphProvider(chainId, `https://cloudflare-ipfs.com/ipns/api.uniswap.org/v1/pools/v3/${chainName}.json`, undefined, 0), new NodeJSCache(new NodeCache({ stdTTL: 300, useClones: false }))),
new StaticV3SubgraphProvider(chainId, this.v3PoolProvider),
]);
}
this.v4PoolParams =
v4PoolParams !== null && v4PoolParams !== void 0 ? v4PoolParams : getApplicableV4FeesTickspacingsHooks(chainId);
if (v4SubgraphProvider) {
this.v4SubgraphProvider = v4SubgraphProvider;
}
else {
this.v4SubgraphProvider = new V4SubgraphProviderWithFallBacks([
new CachingV4SubgraphProvider(chainId, new URISubgraphProvider(chainId, `https://cloudflare-ipfs.com/ipns/api.uniswap.org/v1/pools/v4/${chainName}.json`, undefined, 0), new NodeJSCache(new NodeCache({ stdTTL: 300, useClones: false }))),
new StaticV4SubgraphProvider(chainId, this.v4PoolProvider, this.v4PoolParams),
]);
}
let gasPriceProviderInstance;
if (JsonRpcProvider.isProvider(this.provider)) {
gasPriceProviderInstance = new OnChainGasPriceProvider(chainId, new EIP1559GasPriceProvider(this.provider), new LegacyGasPriceProvider(this.provider));
}
else {
gasPriceProviderInstance = new ETHGasStationInfoProvider(ETH_GAS_STATION_API_URL);
}
this.gasPriceProvider =
gasPriceProvider !== null && gasPriceProvider !== void 0 ? gasPriceProvider : new CachingGasStationProvider(chainId, gasPriceProviderInstance, new NodeJSCache(new NodeCache({ stdTTL: 7, useClones: false })));
this.v4GasModelFactory =
v4GasModelFactory !== null && v4GasModelFactory !== void 0 ? v4GasModelFactory : new V4HeuristicGasModelFactory(this.provider);
this.v3GasModelFactory =
v3GasModelFactory !== null && v3GasModelFactory !== void 0 ? v3GasModelFactory : new V3HeuristicGasModelFactory(this.provider);
this.v2GasModelFactory =
v2GasModelFactory !== null && v2GasModelFactory !== void 0 ? v2GasModelFactory : new V2HeuristicGasModelFactory(this.provider);
this.mixedRouteGasModelFactory =
mixedRouteGasModelFactory !== null && mixedRouteGasModelFactory !== void 0 ? mixedRouteGasModelFactory : new MixedRouteHeuristicGasModelFactory();
this.swapRouterProvider =
swapRouterProvider !== null && swapRouterProvider !== void 0 ? swapRouterProvider : new SwapRouterProvider(this.multicall2Provider, this.chainId);
if (chainId === ChainId.ARBITRUM_ONE ||
chainId === ChainId.ARBITRUM_GOERLI) {
this.l2GasDataProvider =
arbitrumGasDataProvider !== null && arbitrumGasDataProvider !== void 0 ? arbitrumGasDataProvider : new ArbitrumGasDataProvider(chainId, this.provider);
}
// Initialize the Quoters.
// Quoters are an abstraction encapsulating the business logic of fetching routes and quotes.
this.v2Quoter = new V2Quoter(this.v2SubgraphProvider, this.v2PoolProvider, this.v2QuoteProvider, this.v2GasModelFactory, this.tokenProvider, this.chainId, this.blockedTokenListProvider, this.tokenValidatorProvider, this.l2GasDataProvider);
this.v3Quoter = new V3Quoter(this.v3SubgraphProvider, this.v3PoolProvider, this.onChainQuoteProvider, this.tokenProvider, this.chainId, this.blockedTokenListProvider, this.tokenValidatorProvider);
this.v4Quoter = new V4Quoter(this.v4SubgraphProvider, this.v4PoolProvider, this.onChainQuoteProvider, this.tokenProvider, this.chainId, this.blockedTokenListProvider, this.tokenValidatorProvider);
this.mixedQuoter = new MixedQuoter(this.v4SubgraphProvider, this.v4PoolProvider, this.v3SubgraphProvider, this.v3PoolProvider, this.v2SubgraphProvider, this.v2PoolProvider, this.onChainQuoteProvider, this.tokenProvider, this.chainId, this.blockedTokenListProvider, this.tokenValidatorProvider);
this.v2Supported = v2Supported !== null && v2Supported !== void 0 ? v2Supported : V2_SUPPORTED;
this.v4Supported = v4Supported !== null && v4Supported !== void 0 ? v4Supported : V4_SUPPORTED;
this.mixedSupported = mixedSupported !== null && mixedSupported !== void 0 ? mixedSupported : MIXED_SUPPORTED;
this.cachedRoutesCacheInvalidationFixRolloutPercentage =
cachedRoutesCacheInvalidationFixRolloutPercentage;
}
async routeToRatio(token0Balance, token1Balance, position, swapAndAddConfig, swapAndAddOptions, routingConfig = DEFAULT_ROUTING_CONFIG_BY_CHAIN(this.chainId)) {
if (token1Balance.currency.wrapped.sortsBefore(token0Balance.currency.wrapped)) {
[token0Balance, token1Balance] = [token1Balance, token0Balance];
}
let preSwapOptimalRatio = this.calculateOptimalRatio(position, position.pool.sqrtRatioX96, true);
// set up parameters according to which token will be swapped
let zeroForOne;
if (position.pool.tickCurrent > position.tickUpper) {
zeroForOne = true;
}
else if (position.pool.tickCurrent < position.tickLower) {
zeroForOne = false;
}
else {
zeroForOne = new Fraction(token0Balance.quotient, token1Balance.quotient).greaterThan(preSwapOptimalRatio);
if (!zeroForOne)
preSwapOptimalRatio = preSwapOptimalRatio.invert();
}
const [inputBalance, outputBalance] = zeroForOne
? [token0Balance, token1Balance]
: [token1Balance, token0Balance];
let optimalRatio = preSwapOptimalRatio;
let postSwapTargetPool = position.pool;
let exchangeRate = zeroForOne
? position.pool.token0Price
: position.pool.token1Price;
let swap = null;
let ratioAchieved = false;
let n = 0;
// iterate until we find a swap with a sufficient ratio or return null
while (!ratioAchieved) {
n++;
if (n > swapAndAddConfig.maxIterations) {
log.info('max iterations exceeded');
return {
status: SwapToRatioStatus.NO_ROUTE_FOUND,
error: 'max iterations exceeded',
};
}
const amountToSwap = calculateRatioAmountIn(optimalRatio, exchangeRate, inputBalance, outputBalance);
if (amountToSwap.equalTo(0)) {
log.info(`no swap needed: amountToSwap = 0`);
return {
status: SwapToRatioStatus.NO_SWAP_NEEDED,
};
}
swap = await this.route(amountToSwap, outputBalance.currency, TradeType.EXACT_INPUT, undefined, {
...DEFAULT_ROUTING_CONFIG_BY_CHAIN(this.chainId),
...routingConfig,
/// @dev We do not want to query for mixedRoutes for routeToRatio as they are not supported
/// [Protocol.V3, Protocol.V2] will make sure we only query for V3 and V2
protocols: [Protocol.V3, Protocol.V2],
});
if (!swap) {
log.info('no route found from this.route()');
return {
status: SwapToRatioStatus.NO_ROUTE_FOUND,
error: 'no route found',
};
}
const inputBalanceUpdated = inputBalance.subtract(swap.trade.inputAmount);
const outputBalanceUpdated = outputBalance.add(swap.trade.outputAmount);
const newRatio = inputBalanceUpdated.divide(outputBalanceUpdated);
let targetPoolPriceUpdate;
swap.route.forEach((route) => {
if (route.protocol === Protocol.V3) {
const v3Route = route;
v3Route.route.pools.forEach((pool, i) => {
if (pool.token0.equals(position.pool.token0) &&
pool.token1.equals(position.pool.token1) &&
pool.fee === position.pool.fee) {
targetPoolPriceUpdate = JSBI.BigInt(v3Route.sqrtPriceX96AfterList[i].toString());
optimalRatio = this.calculateOptimalRatio(position, JSBI.BigInt(targetPoolPriceUpdate.toString()), zeroForOne);
}
});
}
});
if (!targetPoolPriceUpdate) {
optimalRatio = preSwapOptimalRatio;
}
ratioAchieved =
newRatio.equalTo(optimalRatio) ||
this.absoluteValue(newRatio.asFraction.divide(optimalRatio).subtract(1)).lessThan(swapAndAddConfig.ratioErrorTolerance);
if (ratioAchieved && targetPoolPriceUpdate) {
postSwapTargetPool = new Pool(position.pool.token0, position.pool.token1, position.pool.fee, targetPoolPriceUpdate, position.pool.liquidity, TickMath.getTickAtSqrtRatio(targetPoolPriceUpdate), position.pool.tickDataProvider);
}
exchangeRate = swap.trade.outputAmount.divide(swap.trade.inputAmount);
log.info({
exchangeRate: exchangeRate.asFraction.toFixed(18),
optimalRatio: optimalRatio.asFraction.toFixed(18),
newRatio: newRatio.asFraction.toFixed(18),
inputBalanceUpdated: inputBalanceUpdated.asFraction.toFixed(18),
outputBalanceUpdated: outputBalanceUpdated.asFraction.toFixed(18),
ratioErrorTolerance: swapAndAddConfig.ratioErrorTolerance.toFixed(18),
iterationN: n.toString(),
}, 'QuoteToRatio Iteration Parameters');
if (exchangeRate.equalTo(0)) {
log.info('exchangeRate to 0');
return {
status: SwapToRatioStatus.NO_ROUTE_FOUND,
error: 'insufficient liquidity to swap to optimal ratio',
};
}
}
if (!swap) {
return {
status: SwapToRatioStatus.NO_ROUTE_FOUND,
error: 'no route found',
};
}
let methodParameters;
if (swapAndAddOptions) {
methodParameters = await this.buildSwapAndAddMethodParameters(swap.trade, swapAndAddOptions, {
initialBalanceTokenIn: inputBalance,
initialBalanceTokenOut: outputBalance,
preLiquidityPosition: position,
});
}
return {
status: SwapToRatioStatus.SUCCESS,
result: { ...swap, methodParameters, optimalRatio, postSwapTargetPool },
};
}
/**
* @inheritdoc IRouter
*/
async route(amount, quoteCurrency, tradeType, swapConfig, partialRoutingConfig = {}) {
var _a, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z;
const originalAmount = amount;
const { currencyIn, currencyOut } = this.determineCurrencyInOutFromTradeType(tradeType, amount, quoteCurrency);
const tokenOutProperties = await this.tokenPropertiesProvider.getTokensProperties([currencyOut], partialRoutingConfig);
const feeTakenOnTransfer = (_c = (_a = tokenOutProperties[getAddressLowerCase(currencyOut)]) === null || _a === void 0 ? void 0 : _a.tokenFeeResult) === null || _c === void 0 ? void 0 : _c.feeTakenOnTransfer;
const externalTransferFailed = (_e = (_d = tokenOutProperties[getAddressLowerCase(currencyOut)]) === null || _d === void 0 ? void 0 : _d.tokenFeeResult) === null || _e === void 0 ? void 0 : _e.externalTransferFailed;
// We want to log the fee on transfer output tokens that we are taking fee or not
// Ideally the trade size (normalized in USD) would be ideal to log here, but we don't have spot price of output tokens here.
// We have to make sure token out is FOT with either buy/sell fee bps > 0
if (((_h = (_g = (_f = tokenOutProperties[getAddressLowerCase(currencyOut)]) === null || _f === void 0 ? void 0 : _f.tokenFeeResult) === null || _g === void 0 ? void 0 : _g.buyFeeBps) === null || _h === void 0 ? void 0 : _h.gt(0)) ||
((_l = (_k = (_j = tokenOutProperties[getAddressLowerCase(currencyOut)]) === null || _j === void 0 ? void 0 : _j.tokenFeeResult) === null || _k === void 0 ? void 0 : _k.sellFeeBps) === null || _l === void 0 ? void 0 : _l.gt(0))) {
if (feeTakenOnTransfer || externalTransferFailed) {
// also to be extra safe, in case of FOT with feeTakenOnTransfer or externalTransferFailed,
// we nullify the fee and flat fee to avoid any potential issues.
// although neither web nor wallet should use the calldata returned from routing/SOR
if ((swapConfig === null || swapConfig === void 0 ? void 0 : swapConfig.type) === SwapType.UNIVERSAL_ROUTER) {
swapConfig.fee = undefined;
swapConfig.flatFee = undefined;
}
metric.putMetric('TokenOutFeeOnTransferNotTakingFee', 1, MetricLoggerUnit.Count);
}
else {
metric.putMetric('TokenOutFeeOnTransferTakingFee', 1, MetricLoggerUnit.Count);
}
}
if (tradeType === TradeType.EXACT_OUTPUT) {
const portionAmount = this.portionProvider.getPortionAmount(amount, tradeType, feeTakenOnTransfer, externalTransferFailed, swapConfig);
if (portionAmount && portionAmount.greaterThan(ZERO)) {
// In case of exact out swap, before we route, we need to make sure that the
// token out amount accounts for flat portion, and token in amount after the best swap route contains the token in equivalent of portion.
// In other words, in case a pool's LP fee bps is lower than the portion bps (0.01%/0.05% for v3), a pool can go insolvency.
// This is because instead of the swapper being responsible for the portion,
// the pool instead gets responsible for the portion.
// The addition below avoids that situation.
amount = amount.add(portionAmount);
}
}
metric.setProperty('chainId', this.chainId);
metric.setProperty('pair', `${currencyIn.symbol}/${currencyOut.symbol}`);
metric.setProperty('tokenIn', getAddress(currencyIn));
metric.setProperty('tokenOut', getAddress(currencyOut));
metric.setProperty('tradeType', tradeType === TradeType.EXACT_INPUT ? 'ExactIn' : 'ExactOut');
metric.putMetric(`QuoteRequestedForChain${this.chainId}`, 1, MetricLoggerUnit.Count);
// Get a block number to specify in all our calls. Ensures data we fetch from chain is
// from the same block.
const blockNumber = (_m = partialRoutingConfig.blockNumber) !== null && _m !== void 0 ? _m : this.getBlockNumberPromise();
const routingConfig = _.merge({
// These settings could be changed by the partialRoutingConfig
useCachedRoutes: true,
writeToCachedRoutes: true,
optimisticCachedRoutes: false,
}, DEFAULT_ROUTING_CONFIG_BY_CHAIN(this.chainId), partialRoutingConfig, { blockNumber });
if (routingConfig.debugRouting) {
log.warn(`Finalized routing config is ${JSON.stringify(routingConfig)}`);
}
const gasPriceWei = await this.getGasPriceWei(await blockNumber, await partialRoutingConfig.blockNumber);
// const gasTokenAccessor = await this.tokenProvider.getTokens([routingConfig.gasToken!]);
const gasToken = routingConfig.gasToken
? (await this.tokenProvider.getTokens([routingConfig.gasToken])).getTokenByAddress(routingConfig.gasToken)
: undefined;
const providerConfig = {
...routingConfig,
blockNumber,
additionalGasOverhead: NATIVE_OVERHEAD(this.chainId, amount.currency, quoteCurrency),
gasToken,
externalTransferFailed,
feeTakenOnTransfer,
};
const { v2GasModel: v2GasModel, v3GasModel: v3GasModel, v4GasModel: v4GasModel, mixedRouteGasModel: mixedRouteGasModel, } = await this.getGasModels(gasPriceWei, amount.currency.wrapped, quoteCurrency.wrapped, providerConfig);
// Create a Set to sanitize the protocols input, a Set of undefined becomes an empty set,
// Then create an Array from the values of that Set.
const protocols = Array.from(new Set(routingConfig.protocols).values());
const cacheMode = (_o = routingConfig.overwriteCacheMode) !== null && _o !== void 0 ? _o : (await ((_p = this.routeCachingProvider) === null || _p === void 0 ? void 0 : _p.getCacheMode(this.chainId, amount, quoteCurrency, tradeType, protocols)));
// Fetch CachedRoutes
let cachedRoutes;
// Decide whether to use cached routes or not - If |enabledAndRequestedProtocolsMatch| is true we are good to use cached routes.
// In order to use cached routes, we need to have all enabled protocols specified in the request.
// By default, all protocols are enabled but for UniversalRouterVersion.V1_2, V4 is not.
// - ref: https://github.com/Uniswap/routing-api/blob/663b607d80d9249f85e7ab0925a611ec3701da2a/lib/util/supportedProtocolVersions.ts#L15
// So we take this into account when deciding whether to use cached routes or not.
// We only want to use cache if all enabled protocols are specified (V2,V3,V4? + MIXED). In any other case, use onchain path.
// - Cache is optimized for global search, not for specific protocol(s) search.
// For legacy systems (SWAP_ROUTER_02) or missing swapConfig, follow UniversalRouterVersion.V1_2 logic.
const availableProtocolsSet = new Set(Object.values(Protocol));
const requestedProtocolsSet = new Set(protocols);
const swapRouter = !swapConfig ||
swapConfig.type === SwapType.SWAP_ROUTER_02 ||
(swapConfig.type === SwapType.UNIVERSAL_ROUTER &&
swapConfig.version === UniversalRouterVersion.V1_2);
if (swapRouter) {
availableProtocolsSet.delete(Protocol.V4);
if (requestedProtocolsSet.has(Protocol.V4)) {
requestedProtocolsSet.delete(Protocol.V4);
}
}
const enabledAndRequestedProtocolsMatch = availableProtocolsSet.size === requestedProtocolsSet.size &&
[...availableProtocolsSet].every((protocol) => requestedProtocolsSet.has(protocol));
// If the requested protocols do not match the enabled protocols, we need to set the hooks options to NO_HOOKS.
if (!requestedProtocolsSet.has(Protocol.V4)) {
routingConfig.hooksOptions = HooksOptions.NO_HOOKS;
}
// If hooksOptions not specified and it's not a swapRouter (i.e. Universal Router it is),
// we should also set it to HOOKS_INCLUSIVE, as this is default behavior even without hooksOptions.
if (!routingConfig.hooksOptions) {
routingConfig.hooksOptions = HooksOptions.HOOKS_INCLUSIVE;
}
log.debug('UniversalRouterVersion_CacheGate_Check', {
availableProtocolsSet: Array.from(availableProtocolsSet),
requestedProtocolsSet: Array.from(requestedProtocolsSet),
enabledAndRequestedProtocolsMatch,
swapConfigType: swapConfig === null || swapConfig === void 0 ? void 0 : swapConfig.type,
swapConfigUniversalRouterVersion: (swapConfig === null || swapConfig === void 0 ? void 0 : swapConfig.type) === SwapType.UNIVERSAL_ROUTER
? swapConfig === null || swapConfig === void 0 ? void 0 : swapConfig.version
: 'N/A',
});
if (routingConfig.useCachedRoutes &&
cacheMode !== CacheMode.Darkmode &&
AlphaRouter.isAllowedToEnterCachedRoutes(routingConfig.intent, routingConfig.hooksOptions, swapRouter)) {
if (enabledAndRequestedProtocolsMatch) {
if (protocols.includes(Protocol.V4) &&
(currencyIn.isNative || currencyOut.isNative)) {
const [wrappedNativeCachedRoutes, nativeCachedRoutes] = await Promise.all([
(_q = this.routeCachingProvider) === null || _q === void 0 ? void 0 : _q.getCachedRoute(this.chainId, CurrencyAmount.fromRawAmount(amount.currency.wrapped, amount.quotient), quoteCurrency.wrapped, tradeType, protocols, await blockNumber, routingConfig.optimisticCachedRoutes),
(_r = this.routeCachingProvider) === null || _r === void 0 ? void 0 : _r.getCachedRoute(this.chainId, amount, quoteCurrency, tradeType, [Protocol.V4], await blockNumber, routingConfig.optimisticCachedRoutes),
]);
if ((wrappedNativeCachedRoutes &&
(wrappedNativeCachedRoutes === null || wrappedNativeCachedRoutes === void 0 ? void 0 : wrappedNativeCachedRoutes.routes.length) > 0) ||
(nativeCachedRoutes && (nativeCachedRoutes === null || nativeCachedRoutes === void 0 ? void 0 : nativeCachedRoutes.routes.length) > 0)) {
cachedRoutes = new CachedRoutes({
routes: [
...((_s = nativeCachedRoutes === null || nativeCachedRoutes === void 0 ? void 0 : nativeCachedRoutes.routes) !== null && _s !== void 0 ? _s : []),
...((_t = wrappedNativeCachedRoutes === null || wrappedNativeCachedRoutes === void 0 ? void 0 : wrappedNativeCachedRoutes.routes) !== null && _t !== void 0 ? _t : []),
],
chainId: this.chainId,
currencyIn: currencyIn,
currencyOut: currencyOut,
protocolsCovered: protocols,
blockNumber: await blockNumber,
tradeType: tradeType,
originalAmount: (_v = (_u = wrappedNativeCachedRoutes === null || wrappedNativeCachedRoutes === void 0 ? void 0 : wrappedNativeCachedRoutes.originalAmount) !== null && _u !== void 0 ? _u : nativeCachedRoutes === null || nativeCachedRoutes === void 0 ? void 0 : nativeCachedRoutes.originalAmount) !== null && _v !== void 0 ? _v : amount.quotient.toString(),
blocksToLive: (_x = (_w = wrappedNativeCachedRoutes === null || wrappedNativeCachedRoutes === void 0 ? void 0 : wrappedNativeCachedRoutes.blocksToLive) !== null && _w !== void 0 ? _w : nativeCachedRoutes === null || nativeCachedRoutes === void 0 ? void 0 : nativeCachedRoutes.blocksToLive) !== null && _x !== void 0 ? _x : DEFAULT_BLOCKS_TO_LIVE[this.chainId],
});
}
}
else {
cachedRoutes = await ((_y = this.routeCachingProvider) === null || _y === void 0 ? void 0 : _y.getCachedRoute(this.chainId, amount, quoteCurrency, tradeType, protocols, await blockNumber, routingConfig.optimisticCachedRoutes));
}
}
}
if (shouldWipeoutCachedRoutes(cachedRoutes, routingConfig)) {
cachedRoutes = undefined;
}
metric.putMetric(routingConfig.useCachedRoutes
? 'GetQuoteUsingCachedRoutes'
: 'GetQuoteNotUsingCachedRoutes', 1, MetricLoggerUnit.Count);
if (cacheMode &&
routingConfig.useCachedRoutes &&
cacheMode !== CacheMode.Darkmode &&
!cachedRoutes) {
metric.putMetric(`GetCachedRoute_miss_${cacheMode}`, 1, MetricLoggerUnit.Count);
log.info({
currencyIn: currencyIn.symbol,
currencyInAddress: getAddress(currencyIn),
currencyOut: currencyOut.symbol,
currencyOutAddress: getAddress(currencyOut),
cacheMode,
amount: amount.toExact(),
chainId: this.chainId,
tradeType: this.tradeTypeStr(tradeType),
}, `GetCachedRoute miss ${cacheMode} for ${this.tokenPairSymbolTradeTypeChainId(currencyIn, currencyOut, tradeType)}`);
}
else if (cachedRoutes && routingConfig.useCachedRoutes) {
metric.putMetric(`GetCachedRoute_hit_${cacheMode}`, 1, MetricLoggerUnit.Count);
log.info({
currencyIn: currencyIn.symbol,
currencyInAddress: getAddress(currencyIn),
currencyOut: currencyOut.symbol,
currencyOutAddress: getAddress(currencyOut),
cacheMode,
amount: amount.toExact(),
chainId: this.chainId,
tradeType: this.tradeTypeStr(tradeType),
}, `GetCachedRoute hit ${cacheMode} for ${this.tokenPairSymbolTradeTypeChainId(currencyIn, currencyOut, tradeType)}`);
}
let swapRouteFromCachePromise = Promise.resolve(null);
if (cachedRoutes) {
swapRouteFromCachePromise = this.getSwapRouteFromCache(currencyIn, currencyOut, cachedRoutes, await blockNumber, amount, quoteCurrency, tradeType, routingConfig, v3GasModel, v4GasModel, mixedRouteGasModel, gasPriceWei, v2GasModel, swapConfig, providerConfig);
}
let swapRouteFromChainPromise = Promise.resolve(null);
if (!cachedRoutes || cacheMode !== CacheMode.Livemode) {
swapRouteFromChainPromise = this.getSwapRouteFromChain(amount, currencyIn, currencyOut, protocols, quoteCurrency, tradeType, routingConfig, v3GasModel, v4GasModel, mixedRouteGasModel, gasPriceWei, v2GasModel, swapConfig, providerConfig);
}
const [swapRouteFromCache, swapRouteFromChain] = await Promise.all([
swapRouteFromCachePromise,
swapRouteFromChainPromise,
]);
let swapRouteRaw;
let hitsCachedRoute = false;
if (cacheMode === CacheMode.Livemode && swapRouteFromCache) {
log.info(`CacheMode is ${cacheMode}, and we are using swapRoute from cache`);
hitsCachedRoute = true;
swapRouteRaw = swapRouteFromCache;
}
else {
log.info(`CacheMode is ${cacheMode}, and we are using materialized swapRoute`);
swapRouteRaw = swapRouteFromChain;
}
if (cacheMode === CacheMode.Tapcompare &&
swapRouteFromCache &&
swapRouteFromChain) {
const quoteDiff = swapRouteFromChain.quote.subtract(swapRouteFromCache.quote);
const quoteGasAdjustedDiff = swapRouteFromChain.quoteGasAdjusted.subtract(swapRouteFromCache.quoteGasAdjusted);
const gasUsedDiff = swapRouteFromChain.estimatedGasUsed.sub(swapRouteFromCache.estimatedGasUsed);
// Only log if quoteDiff is different from 0, or if quoteGasAdjustedDiff and gasUsedDiff are both different from 0
if (!quoteDiff.equalTo(0) ||
!(quoteGasAdjustedDiff.equalTo(0) || gasUsedDiff.eq(0))) {
try {
// Calculates the percentage of the difference with respect to the quoteFromChain (not from cache)
const misquotePercent = quoteGasAdjustedDiff
.divide(swapRouteFromChain.quoteGasAdjusted)
.multiply(100);
metric.putMetric(`TapcompareCachedRoute_quoteGasAdjustedDiffPercent`, Number(misquotePercent.toExact()), MetricLoggerUnit.Percent);
log.warn({
quoteFromChain: swapRouteFromChain.quote.toExact(),
quoteFromCache: swapRouteFromCache.quote.toExact(),
quoteDiff: quoteDiff.toExact(),
quoteGasAdjustedFromChain: swapRouteFromChain.quoteGasAdjusted.toExact(),
quoteGasAdjustedFromCache: swapRouteFromCache.quoteGasAdjusted.toExact(),
quoteGasAdjustedDiff: quoteGasAdjustedDiff.toExact(),
gasUsedFromChain: swapRouteFromChain.estimatedGasUsed.toString(),
gasUsedFromCache: swapRouteFromCache.estimatedGasUsed.toString(),
gasUsedDiff: gasUsedDiff.toString(),
routesFromChain: swapRouteFromChain.routes.toString(),
routesFromCache: swapRouteFromCache.routes.toString(),
amount: amount.toExact(),
originalAmount: cachedRoutes === null || cachedRoutes === void 0 ? void 0 : cachedRoutes.originalAmount,
pair: this.tokenPairSymbolTradeTypeChainId(currencyIn, currencyOut, tradeType),
blockNumber,
}, `Comparing quotes between Chain and Cache for ${this.tokenPairSymbolTradeTypeChainId(currencyIn, currencyOut, tradeType)}`);
}
catch (error) {
// This is in response to the 'division by zero' error
// during https://uniswapteam.slack.com/archives/C059TGEC57W/p1723997015399579
if (error instanceof RangeError &&
error.message.includes('Division by zero')) {
log.error({
quoteGasAdjustedDiff: quoteGasAdjustedDiff.toExact(),
swapRouteFromChainQuoteGasAdjusted: swapRouteFromChain.quoteGasAdjusted.toExact(),
}, 'Error calculating misquote percent');
metric.putMetric(`TapcompareCachedRoute_quoteGasAdjustedDiffPercent_divzero`, 1, MetricLoggerUnit.Count);
}
// Log but don't throw here - this is only for logging.
}
}
}
let newSetCachedRoutesPath = false;
const shouldEnableCachedRoutesCacheInvalidationFix = Math.random() * 100 <
((_z = this.cachedRoutesCacheInvalidationFixRolloutPercentage) !== null && _z !== void 0 ? _z : 0);
// we have to write cached routes right before checking swapRouteRaw is null or not
// because getCachedRoutes in routing-api do not use the blocks-to-live to filter out the expired routes at all
// there's a possibility the cachedRoutes is always populated, but swapRouteFromCache is always null, because we don't update cachedRoutes in this case at all,
// as long as it's within 24 hours sliding window TTL
if (shouldEnableCachedRoutesCacheInvalidationFix) {
// theoretically, when routingConfig.intent === INTENT.CACHING, optimisticCachedRoutes should be false
// so that we can always pass in cachedRoutes?.notExpired(await blockNumber, !routingConfig.optimisticCachedRoutes)
// but just to be safe, we just hardcode true when checking the cached routes expiry for write update
// we decide to not check cached routes expiry in the read path anyway
if (!(cachedRoutes === null || cachedRoutes === void 0 ? void 0 : cachedRoutes.notExpired(await blockNumber, true))) {
// optimisticCachedRoutes === false means at routing-api level, we only want to set cached routes during intent=caching, not intent=quote
// this means during the online quote endpoint path, we should not reset cached routes
if (routingConfig.intent === INTENT.CACHING) {
// due to fire and forget nature, we already take note that we should set new cached routes during the new path
newSetCachedRoutesPath = true;
metric.putMetric(`SetCachedRoute_NewPath`, 1, MetricLoggerUnit.Count);
// there's a chance that swapRouteFromChain might be populated already,
// when there's no cachedroutes in the dynamo DB.
// in that case, we don't try to swap route from chain again
const swapRouteFromChainAgain = swapRouteFromChain !== null && swapRouteFromChain !== void 0 ? swapRouteFromChain :
// we have to intentionally await here, because routing-api lambda has a chance to return the swapRoute/swapRouteWithSimulation
// before the routing-api quote handler can finish running getSwapRouteFromChain (getSwapRouteFromChain is runtime intensive)
(await this.getSwapRouteFromChain(amount, currencyIn, currencyOut, protocols, quoteCurrency, tradeType, routingConfig, v3GasModel, v4GasModel, mixedRouteGasModel, gasPriceWei, v2GasModel, swapConfig, providerConfig));
if (swapRouteFromChainAgain) {
const routesToCache = CachedRoutes.fromRoutesWithValidQuotes(swapRouteFromChainAgain.routes, this.chainId, currencyIn, currencyOut, protocols.sort(), await blockNumber, tradeType, amount.toExact());
await this.setCachedRoutesAndLog(amount, currencyIn, currencyOut, tradeType, 'SetCachedRoute_NewPath', routesToCache, routingConfig.cachedRoutesRouteIds);
}
}
}
}
if (!swapRouteRaw) {
return null;
}
const { quote, quoteGasAdjusted,