quickswap-smart-order-router
Version:
QuickSwap Smart Order Router
686 lines • 75.4 kB
JavaScript
import { BigNumber } from '@ethersproject/bignumber';
import { JsonRpcProvider } from '@ethersproject/providers';
import DEFAULT_TOKEN_LIST from '@uniswap/default-token-list';
import { Protocol, SwapRouter } from '@uniswap/router-sdk';
import { Fraction, TradeType } from '@uniswap/sdk-core';
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 { V3HeuristicGasModelFactory } from '.';
import { CachingGasStationProvider, CachingTokenProviderWithFallback, CachingV2SubgraphProvider, CachingV3PoolProvider, CachingV3SubgraphProvider, EIP1559GasPriceProvider, ETHGasStationInfoProvider, LegacyGasPriceProvider, NodeJSCache, OnChainGasPriceProvider, StaticV2SubgraphProvider, StaticV3SubgraphProvider, SwapRouterProvider, UniswapMulticallProvider, URISubgraphProvider, V2QuoteProvider, V2SubgraphProviderWithFallBacks, V3SubgraphProviderWithFallBacks, } from '../../providers';
import { CachingTokenListProvider, } from '../../providers/caching-token-list-provider';
import { TokenProvider } from '../../providers/token-provider';
import { TokenValidationResult, TokenValidatorProvider, } from '../../providers/token-validator-provider';
import { V2PoolProvider, } from '../../providers/v2/pool-provider';
import { ArbitrumGasDataProvider, OptimismGasDataProvider, } from '../../providers/v3/gas-data-provider';
import { V3PoolProvider, } from '../../providers/v3/pool-provider';
import { V3QuoteProvider, } from '../../providers/v3/quote-provider';
import { ChainId, ID_TO_CHAIN_ID, ID_TO_NETWORK_NAME, V2_SUPPORTED, } from '../../util/chains';
import { log } from '../../util/log';
import { buildSwapMethodParameters, buildTrade, } from '../../util/methodParameters';
import { metric, MetricLoggerUnit } from '../../util/metric';
import { poolToString, routeToString } from '../../util/routes';
import { UNSUPPORTED_TOKENS } from '../../util/unsupported-tokens';
import { SwapToRatioStatus, } from '../router';
import { DEFAULT_ROUTING_CONFIG_BY_CHAIN, ETH_GAS_STATION_API_URL, } from './config';
import { V2RouteWithValidQuote, V3RouteWithValidQuote, } from './entities/route-with-valid-quote';
import { getBestSwapRoute } from './functions/best-swap-route';
import { calculateRatioAmountIn } from './functions/calculate-ratio-amount-in';
import { computeAllV2Routes, computeAllV3Routes, } from './functions/compute-all-routes';
import { getV2CandidatePools, getV3CandidatePools, } from './functions/get-candidate-pools';
import { V2HeuristicGasModelFactory } from './gas-models/v2/v2-heuristic-gas-model';
export class AlphaRouter {
constructor({ chainId, provider, multicall2Provider, v3PoolProvider, v3QuoteProvider, v2PoolProvider, v2QuoteProvider, v2SubgraphProvider, tokenProvider, blockedTokenListProvider, v3SubgraphProvider, gasPriceProvider, v3GasModelFactory, v2GasModelFactory, swapRouterProvider, optimismGasDataProvider, tokenValidatorProvider, arbitrumGasDataProvider, }) {
this.chainId = chainId;
this.provider = provider;
this.multicall2Provider =
multicall2Provider !== null && multicall2Provider !== void 0 ? multicall2Provider : new UniswapMulticallProvider(chainId, provider, 375000);
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 })));
if (v3QuoteProvider) {
this.v3QuoteProvider = v3QuoteProvider;
}
else {
switch (chainId) {
case ChainId.OPTIMISM:
case ChainId.OPTIMISTIC_KOVAN:
this.v3QuoteProvider = new V3QuoteProvider(chainId, provider, this.multicall2Provider, {
retries: 2,
minTimeout: 100,
maxTimeout: 1000,
}, {
multicallChunk: 110,
gasLimitPerCall: 1200000,
quoteMinSuccessRate: 0.1,
}, {
gasLimitOverride: 3000000,
multicallChunk: 45,
}, {
gasLimitOverride: 3000000,
multicallChunk: 45,
}, {
baseBlockOffset: -10,
rollback: {
enabled: true,
attemptsBeforeRollback: 1,
rollbackBlockOffset: -10,
},
});
break;
case ChainId.ARBITRUM_ONE:
case ChainId.ARBITRUM_RINKEBY:
this.v3QuoteProvider = new V3QuoteProvider(chainId, provider, this.multicall2Provider, {
retries: 2,
minTimeout: 100,
maxTimeout: 1000,
}, {
multicallChunk: 10,
gasLimitPerCall: 12000000,
quoteMinSuccessRate: 0.1,
}, {
gasLimitOverride: 30000000,
multicallChunk: 6,
}, {
gasLimitOverride: 30000000,
multicallChunk: 6,
});
break;
default:
this.v3QuoteProvider = new V3QuoteProvider(chainId, provider, this.multicall2Provider, {
retries: 2,
minTimeout: 100,
maxTimeout: 1000,
}, {
multicallChunk: 210,
gasLimitPerCall: 705000,
quoteMinSuccessRate: 0.15,
}, {
gasLimitOverride: 2000000,
multicallChunk: 70,
});
break;
}
}
this.v2PoolProvider =
v2PoolProvider !== null && v2PoolProvider !== void 0 ? v2PoolProvider : new V2PoolProvider(chainId, this.multicall2Provider);
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));
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.gasPriceProvider =
gasPriceProvider !== null && gasPriceProvider !== void 0 ? gasPriceProvider : new CachingGasStationProvider(chainId, this.provider instanceof JsonRpcProvider
? new OnChainGasPriceProvider(chainId, new EIP1559GasPriceProvider(this.provider), new LegacyGasPriceProvider(this.provider))
: new ETHGasStationInfoProvider(ETH_GAS_STATION_API_URL), new NodeJSCache(new NodeCache({ stdTTL: 15, useClones: false })));
this.v3GasModelFactory =
v3GasModelFactory !== null && v3GasModelFactory !== void 0 ? v3GasModelFactory : new V3HeuristicGasModelFactory();
this.v2GasModelFactory =
v2GasModelFactory !== null && v2GasModelFactory !== void 0 ? v2GasModelFactory : new V2HeuristicGasModelFactory();
this.swapRouterProvider =
swapRouterProvider !== null && swapRouterProvider !== void 0 ? swapRouterProvider : new SwapRouterProvider(this.multicall2Provider);
if (chainId == ChainId.OPTIMISM || chainId == ChainId.OPTIMISTIC_KOVAN) {
this.l2GasDataProvider =
optimismGasDataProvider !== null && optimismGasDataProvider !== void 0 ? optimismGasDataProvider : new OptimismGasDataProvider(chainId, this.multicall2Provider);
}
if (chainId == ChainId.ARBITRUM_ONE ||
chainId == ChainId.ARBITRUM_RINKEBY) {
this.l2GasDataProvider =
arbitrumGasDataProvider !== null && arbitrumGasDataProvider !== void 0 ? arbitrumGasDataProvider : new ArbitrumGasDataProvider(chainId, this.provider);
}
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 })));
}
}
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,
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;
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 = (_a = partialRoutingConfig.blockNumber) !== null && _a !== void 0 ? _a : this.getBlockNumberPromise();
const routingConfig = _.merge({}, DEFAULT_ROUTING_CONFIG_BY_CHAIN(this.chainId), partialRoutingConfig, { blockNumber });
const { protocols } = routingConfig;
const currencyIn = tradeType == TradeType.EXACT_INPUT ? amount.currency : quoteCurrency;
const currencyOut = tradeType == TradeType.EXACT_INPUT ? quoteCurrency : amount.currency;
const tokenIn = currencyIn.wrapped;
const tokenOut = currencyOut.wrapped;
// 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);
// Get an estimate of the gas price to use when estimating gas cost of different routes.
const beforeGas = Date.now();
const { gasPriceWei } = await this.gasPriceProvider.getGasPrice();
metric.putMetric('GasPriceLoad', Date.now() - beforeGas, MetricLoggerUnit.Milliseconds);
const quoteToken = quoteCurrency.wrapped;
const quotePromises = [];
const protocolsSet = new Set(protocols !== null && protocols !== void 0 ? protocols : []);
const gasModel = await this.v3GasModelFactory.buildGasModel(this.chainId, gasPriceWei, this.v3PoolProvider, quoteToken, this.l2GasDataProvider);
if ((protocolsSet.size == 0 ||
(protocolsSet.has(Protocol.V2) && protocolsSet.has(Protocol.V3))) &&
V2_SUPPORTED.includes(this.chainId)) {
log.info({ protocols, tradeType }, 'Routing across all protocols');
quotePromises.push(this.getV3Quotes(tokenIn, tokenOut, amounts, percents, quoteToken, gasModel, tradeType, routingConfig));
quotePromises.push(this.getV2Quotes(tokenIn, tokenOut, amounts, percents, quoteToken, gasPriceWei, tradeType, routingConfig));
}
else {
if (protocolsSet.has(Protocol.V3) ||
(protocolsSet.size == 0 && !V2_SUPPORTED.includes(this.chainId))) {
log.info({ protocols, swapType: tradeType }, 'Routing across V3');
quotePromises.push(this.getV3Quotes(tokenIn, tokenOut, amounts, percents, quoteToken, gasModel, tradeType, routingConfig));
}
if (protocolsSet.has(Protocol.V2)) {
log.info({ protocols, swapType: tradeType }, 'Routing across V2');
quotePromises.push(this.getV2Quotes(tokenIn, tokenOut, amounts, percents, quoteToken, gasPriceWei, tradeType, routingConfig));
}
}
const routesWithValidQuotesByProtocol = await Promise.all(quotePromises);
let allRoutesWithValidQuotes = [];
let allCandidatePools = [];
for (const { routesWithValidQuotes, candidatePools, } of routesWithValidQuotesByProtocol) {
allRoutesWithValidQuotes = [
...allRoutesWithValidQuotes,
...routesWithValidQuotes,
];
allCandidatePools = [...allCandidatePools, candidatePools];
}
if (allRoutesWithValidQuotes.length == 0) {
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 beforeBestSwap = Date.now();
const swapRouteRaw = await getBestSwapRoute(amount, percents, allRoutesWithValidQuotes, tradeType, this.chainId, routingConfig, gasModel);
if (!swapRouteRaw) {
return null;
}
const { quote, quoteGasAdjusted, estimatedGasUsed, routes: routeAmounts, estimatedGasUsedQuoteToken, estimatedGasUsedUSD, } = swapRouteRaw;
// Build Trade object that represents the optimal swap.
const trade = 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 = buildSwapMethodParameters(trade, swapConfig);
}
metric.putMetric('FindBestSwapRoute', Date.now() - beforeBestSwap, MetricLoggerUnit.Milliseconds);
metric.putMetric(`QuoteFoundForChain${this.chainId}`, 1, MetricLoggerUnit.Count);
this.emitPoolSelectionMetrics(swapRouteRaw, allCandidatePools);
return {
quote,
quoteGasAdjusted,
estimatedGasUsed,
estimatedGasUsedQuoteToken,
estimatedGasUsedUSD,
gasPriceWei,
route: routeAmounts,
trade,
methodParameters,
blockNumber: BigNumber.from(await blockNumber),
};
}
async applyTokenValidatorToPools(pools, isInvalidFn) {
if (!this.tokenValidatorProvider) {
return pools;
}
log.info(`Running token validator on ${pools.length} pools`);
const tokens = _.flatMap(pools, (pool) => [pool.token0, pool.token1]);
const tokenValidationResults = await this.tokenValidatorProvider.validateTokens(tokens);
const poolsFiltered = _.filter(pools, (pool) => {
const token0Validation = tokenValidationResults.getValidationByToken(pool.token0);
const token1Validation = tokenValidationResults.getValidationByToken(pool.token1);
const token0Invalid = isInvalidFn(pool.token0, token0Validation);
const token1Invalid = isInvalidFn(pool.token1, token1Validation);
if (token0Invalid || token1Invalid) {
log.info(`Dropping pool ${poolToString(pool)} because token is invalid. ${pool.token0.symbol}: ${token0Validation}, ${pool.token1.symbol}: ${token1Validation}`);
}
return !token0Invalid && !token1Invalid;
});
return poolsFiltered;
}
async getV3Quotes(tokenIn, tokenOut, amounts, percents, quoteToken, gasModel, swapType, routingConfig) {
log.info('Starting to get V3 quotes');
// Fetch all the pools that we will consider routing via. There are thousands
// of pools, so we filter them to a set of candidate pools that we expect will
// result in good prices.
const { poolAccessor, candidatePools } = await getV3CandidatePools({
tokenIn,
tokenOut,
tokenProvider: this.tokenProvider,
blockedTokenListProvider: this.blockedTokenListProvider,
poolProvider: this.v3PoolProvider,
routeType: swapType,
subgraphProvider: this.v3SubgraphProvider,
routingConfig,
chainId: this.chainId,
});
const poolsRaw = poolAccessor.getAllPools();
// Drop any pools that contain fee on transfer tokens (not supported by v3) or have issues with being transferred.
const pools = await this.applyTokenValidatorToPools(poolsRaw, (token, tokenValidation) => {
// If there is no available validation result we assume the token is fine.
if (!tokenValidation) {
return false;
}
// Only filters out *intermediate* pools that involve tokens that we detect
// cant be transferred. This prevents us trying to route through tokens that may
// not be transferrable, but allows users to still swap those tokens if they
// specify.
//
if (tokenValidation == TokenValidationResult.STF &&
(token.equals(tokenIn) || token.equals(tokenOut))) {
return false;
}
return (tokenValidation == TokenValidationResult.FOT ||
tokenValidation == TokenValidationResult.STF);
});
// Given all our candidate pools, compute all the possible ways to route from tokenIn to tokenOut.
const { maxSwapsPerPath } = routingConfig;
const routes = computeAllV3Routes(tokenIn, tokenOut, pools, maxSwapsPerPath);
if (routes.length == 0) {
return { routesWithValidQuotes: [], candidatePools };
}
// For all our routes, and all the fractional amounts, fetch quotes on-chain.
const quoteFn = swapType == TradeType.EXACT_INPUT
? this.v3QuoteProvider.getQuotesManyExactIn.bind(this.v3QuoteProvider)
: this.v3QuoteProvider.getQuotesManyExactOut.bind(this.v3QuoteProvider);
const beforeQuotes = Date.now();
log.info(`Getting quotes for V3 for ${routes.length} routes with ${amounts.length} amounts per route.`);
const { routesWithQuotes } = await quoteFn(amounts, routes, {
blockNumber: routingConfig.blockNumber,
});
metric.putMetric('V3QuotesLoad', Date.now() - beforeQuotes, MetricLoggerUnit.Milliseconds);
metric.putMetric('V3QuotesFetched', _(routesWithQuotes)
.map(([, quotes]) => quotes.length)
.sum(), MetricLoggerUnit.Count);
const routesWithValidQuotes = [];
for (const routeWithQuote of routesWithQuotes) {
const [route, quotes] = routeWithQuote;
for (let i = 0; i < quotes.length; i++) {
const percent = percents[i];
const amountQuote = quotes[i];
const { quote, amount, sqrtPriceX96AfterList, initializedTicksCrossedList, gasEstimate, } = amountQuote;
if (!quote ||
!sqrtPriceX96AfterList ||
!initializedTicksCrossedList ||
!gasEstimate) {
log.debug({
route: routeToString(route),
amountQuote,
}, 'Dropping a null V3 quote for route.');
continue;
}
const routeWithValidQuote = new V3RouteWithValidQuote({
route,
rawQuote: quote,
amount,
percent,
sqrtPriceX96AfterList,
initializedTicksCrossedList,
quoterGasEstimate: gasEstimate,
gasModel,
quoteToken,
tradeType: swapType,
v3PoolProvider: this.v3PoolProvider,
});
routesWithValidQuotes.push(routeWithValidQuote);
}
}
return { routesWithValidQuotes, candidatePools };
}
async getV2Quotes(tokenIn, tokenOut, amounts, percents, quoteToken, gasPriceWei, swapType, routingConfig) {
log.info('Starting to get V2 quotes');
// Fetch all the pools that we will consider routing via. There are thousands
// of pools, so we filter them to a set of candidate pools that we expect will
// result in good prices.
const { poolAccessor, candidatePools } = await getV2CandidatePools({
tokenIn,
tokenOut,
tokenProvider: this.tokenProvider,
blockedTokenListProvider: this.blockedTokenListProvider,
poolProvider: this.v2PoolProvider,
routeType: swapType,
subgraphProvider: this.v2SubgraphProvider,
routingConfig,
chainId: this.chainId,
});
const poolsRaw = poolAccessor.getAllPools();
// Drop any pools that contain tokens that can not be transferred according to the token validator.
const pools = await this.applyTokenValidatorToPools(poolsRaw, (token, tokenValidation) => {
// If there is no available validation result we assume the token is fine.
if (!tokenValidation) {
return false;
}
// Only filters out *intermediate* pools that involve tokens that we detect
// cant be transferred. This prevents us trying to route through tokens that may
// not be transferrable, but allows users to still swap those tokens if they
// specify.
if (tokenValidation == TokenValidationResult.STF &&
(token.equals(tokenIn) || token.equals(tokenOut))) {
return false;
}
return tokenValidation == TokenValidationResult.STF;
});
// Given all our candidate pools, compute all the possible ways to route from tokenIn to tokenOut.
const { maxSwapsPerPath } = routingConfig;
const routes = computeAllV2Routes(tokenIn, tokenOut, pools, maxSwapsPerPath);
if (routes.length == 0) {
return { routesWithValidQuotes: [], candidatePools };
}
// For all our routes, and all the fractional amounts, fetch quotes on-chain.
const quoteFn = swapType == TradeType.EXACT_INPUT
? this.v2QuoteProvider.getQuotesManyExactIn.bind(this.v2QuoteProvider)
: this.v2QuoteProvider.getQuotesManyExactOut.bind(this.v2QuoteProvider);
const beforeQuotes = Date.now();
log.info(`Getting quotes for V2 for ${routes.length} routes with ${amounts.length} amounts per route.`);
const { routesWithQuotes } = await quoteFn(amounts, routes);
const gasModel = await this.v2GasModelFactory.buildGasModel(this.chainId, gasPriceWei, this.v2PoolProvider, quoteToken);
metric.putMetric('V2QuotesLoad', Date.now() - beforeQuotes, MetricLoggerUnit.Milliseconds);
metric.putMetric('V2QuotesFetched', _(routesWithQuotes)
.map(([, quotes]) => quotes.length)
.sum(), MetricLoggerUnit.Count);
const routesWithValidQuotes = [];
for (const routeWithQuote of routesWithQuotes) {
const [route, quotes] = routeWithQuote;
for (let i = 0; i < quotes.length; i++) {
const percent = percents[i];
const amountQuote = quotes[i];
const { quote, amount } = amountQuote;
if (!quote) {
log.debug({
route: routeToString(route),
amountQuote,
}, 'Dropping a null V2 quote for route.');
continue;
}
const routeWithValidQuote = new V2RouteWithValidQuote({
route,
rawQuote: quote,
amount,
percent,
gasModel,
quoteToken,
tradeType: swapType,
v2PoolProvider: this.v2PoolProvider,
});
routesWithValidQuotes.push(routeWithValidQuote);
}
}
return { routesWithValidQuotes, candidatePools };
}
// 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 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 SwapRouter.swapAndAddCallParameters(trade, {
recipient,
slippageTolerance,
deadlineOrPreviousBlockhash: deadline,
inputTokenPermit,
}, 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);
}
emitPoolSelectionMetrics(swapRouteRaw, allPoolsBySelection) {
const poolAddressesUsed = new Set();
const { routes: routeAmounts } = swapRouteRaw;
_(routeAmounts)
.flatMap((routeAmount) => {
const { poolAddresses } = routeAmount;
return poolAddresses;
})
.forEach((address) => {
poolAddressesUsed.add(address.toLowerCase());
});
for (const poolsBySelection of allPoolsBySelection) {
const { protocol } = poolsBySelection;
_.forIn(poolsBySelection.selections, (pools, topNSelection) => {
const topNUsed = _.findLastIndex(pools, (pool) => poolAddressesUsed.has(pool.id.toLowerCase())) + 1;
metric.putMetric(_.capitalize(`${protocol}${topNSelection}`), topNUsed, MetricLoggerUnit.Count);
});
}
let hasV3Route = false;
let hasV2Route = false;
for (const routeAmount of routeAmounts) {
if (routeAmount.protocol == Protocol.V3) {
hasV3Route = true;
}
if (routeAmount.protocol == Protocol.V2) {
hasV2Route = true;
}
}
if (hasV3Route && hasV2Route) {
metric.putMetric(`V3AndV2SplitRoute`, 1, MetricLoggerUnit.Count);
metric.putMetric(`V3AndV2SplitRouteForChain${this.chainId}`, 1, MetricLoggerUnit.Count);
}
else if (hasV3Route) {
if (routeAmounts.length > 1) {
metric.putMetric(`V3SplitRoute`, 1, MetricLoggerUnit.Count);
metric.putMetric(`V3SplitRouteForChain${this.chainId}`, 1, MetricLoggerUnit.Count);
}
else {
metric.putMetric(`V3Route`, 1, MetricLoggerUnit.Count);
metric.putMetric(`V3RouteForChain${this.chainId}`, 1, MetricLoggerUnit.Count);
}
}
else if (hasV2Route) {
if (routeAmounts.length > 1) {
metric.putMetric(`V2SplitRoute`, 1, MetricLoggerUnit.Count);
metric.putMetric(`V2SplitRouteForChain${this.chainId}`, 1, MetricLoggerUnit.Count);
}
else {
metric.putMetric(`V2Route`, 1, MetricLoggerUnit.Count);
metric.putMetric(`V2RouteForChain${this.chainId}`, 1, MetricLoggerUnit.Count);
}
}
}
calculateOptimalRatio(position, sqrtRatioX96, zeroForOne) {
const upperSqrtRatioX96 = TickMath.getSqrtRatioAtTick(position.tickUpper);
const lowerSqrtRatioX96 = 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.greaterThan(sqrtRatioX96, upperSqrtRatioX96) ||
JSBI.lessThan(sqrtRatioX96, lowerSqrtRatioX96)) {
return new Fraction(0, 1);
}
const precision = JSBI.BigInt('1' + '0'.repeat(18));
let optimalRatio = new Fraction(SqrtPriceMath.getAmount0Delta(sqrtRatioX96, upperSqrtRatioX96, precision, true), SqrtPriceMath.getAmount1Delta(sqrtRatioX96, lowerSqrtRatioX96, precision, true));
if (!zeroForOne)
optimalRatio = optimalRatio.invert();
return optimalRatio;
}
absoluteValue(fraction) {
const numeratorAbs = JSBI.lessThan(fraction.numerator, JSBI.BigInt(0))
? JSBI.unaryMinus(fraction.numerator)
: fraction.numerator;
const denominatorAbs = JSBI.lessThan(fraction.denominator, JSBI.BigInt(0))
? JSBI.unaryMinus(fraction.denominator)
: fraction.denominator;
return new Fraction(numeratorAbs, denominatorAbs);
}
getBlockNumberPromise() {
return retry(async (_b, attempt) => {
if (attempt > 1) {
log.info(`Get block number attempt ${attempt}`);
}
return this.provider.getBlockNumber();
}, {
retries: 2,
minTimeout: 100,
maxTimeout: 1000,
});
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYWxwaGEtcm91dGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL3JvdXRlcnMvYWxwaGEtcm91dGVyL2FscGhhLXJvdXRlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsU0FBUyxFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFDckQsT0FBTyxFQUFnQixlQUFlLEVBQUUsTUFBTSwwQkFBMEIsQ0FBQztBQUN6RSxPQUFPLGtCQUFrQixNQUFNLDZCQUE2QixDQUFDO0FBQzdELE9BQU8sRUFBRSxRQUFRLEVBQUUsVUFBVSxFQUFTLE1BQU0scUJBQXFCLENBQUM7QUFDbEUsT0FBTyxFQUFZLFFBQVEsRUFBUyxTQUFTLEVBQUUsTUFBTSxtQkFBbUIsQ0FBQztBQUd6RSxPQUFPLEVBRUwsSUFBSSxFQUNKLFFBQVEsRUFDUixhQUFhLEVBQ2IsUUFBUSxHQUNULE1BQU0saUJBQWlCLENBQUM7QUFDekIsT0FBTyxLQUFLLE1BQU0sYUFBYSxDQUFDO0FBQ2hDLE9BQU8sSUFBSSxNQUFNLE1BQU0sQ0FBQztBQUN4QixPQUFPLENBQUMsTUFBTSxRQUFRLENBQUM7QUFDdkIsT0FBTyxTQUFTLE1BQU0sWUFBWSxDQUFDO0FBQ25DLE9BQU8sRUFBRSwwQkFBMEIsRUFBRSxNQUFNLEdBQUcsQ0FBQztBQUMvQyxPQUFPLEVBQ0wseUJBQXlCLEVBQ3pCLGdDQUFnQyxFQUNoQyx5QkFBeUIsRUFDekIscUJBQXFCLEVBQ3JCLHlCQUF5QixFQUN6Qix1QkFBdUIsRUFDdkIseUJBQXlCLEVBSXpCLHNCQUFzQixFQUN0QixXQUFXLEVBQ1gsdUJBQXVCLEVBQ3ZCLHdCQUF3QixFQUN4Qix3QkFBd0IsRUFDeEIsa0JBQWtCLEVBQ2xCLHdCQUF3QixFQUN4QixtQkFBbUIsRUFDbkIsZUFBZSxFQUNmLCtCQUErQixFQUMvQiwrQkFBK0IsR0FDaEMsTUFBTSxpQkFBaUIsQ0FBQztBQUN6QixPQUFPLEVBQ0wsd0JBQXdCLEdBRXpC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