@xspswap/smart-order-router
Version:
XSwap Protocol V3 Smart Order Router
823 lines • 96.5 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 '@x-swap-protocol/router-sdk';
import { ChainId, Fraction, SUPPORTED_CHAINS, SWAP_ROUTER_02_ADDRESSES, TradeType, } from '@x-swap-protocol/sdk-core';
import { Pool, Position, SqrtPriceMath, TickMath, UniV3Factory, } from '@x-swap-protocol/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, EIP1559GasPriceProvider, ETHGasStationInfoProvider, LegacyGasPriceProvider, NodeJSCache, OnChainGasPriceProvider, OnChainQuoteProvider, StaticV2SubgraphProvider, StaticV3SubgraphProvider, SwapRouterProvider, UniswapMulticallProvider, V2QuoteProvider, V2SubgraphProvider, V2SubgraphProviderWithFallBacks, V3SubgraphProvider, V3SubgraphProviderWithFallBacks, } from '../../providers';
import { CachingTokenListProvider, } from '../../providers/caching-token-list-provider';
import { TokenProvider } from '../../providers/token-provider';
import { TokenValidatorProvider, } from '../../providers/token-validator-provider';
import { V2PoolProvider, } from '../../providers/v2/pool-provider';
import { FATHOM_FACTORY_ADDRESS, FATHOM_INIT_CODE_HASH, } from '../../providers/v2-fathom/constants';
import { StaticV2FathomSubgraphProvider } from '../../providers/v2-fathom/static-subgraph-provider';
import { V2SubgraphProvider as FathomV2SubgraphProvider } from '../../providers/v2-fathom/subgraph-provider';
import { V3PoolProvider, } from '../../providers/v3/pool-provider';
import { StaticV3UniSubgraphProvider } from '../../providers/v3/static-subgraph-provider-v3-uni';
import { Erc20__factory } from '../../types/other/factories/Erc20__factory';
import { isV2, isV3 } from '../../util';
import { CurrencyAmount } from '../../util/amounts';
import { ID_TO_CHAIN_ID, V2_SUPPORTED } from '../../util/chains';
import { log } from '../../util/log';
import { buildSwapMethodParameters, buildTrade, } from '../../util/methodParameters';
import { metric, MetricLoggerUnit } from '../../util/metric';
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 { getBestSwapRoute } from './functions/best-swap-route';
import { calculateRatioAmountIn } from './functions/calculate-ratio-amount-in';
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 { MixedQuoter, V2Quoter, V3Quoter } from './quoters';
export class MapWithLowerCaseKey extends Map {
set(key, value) {
return super.set(key.toLowerCase(), value);
}
}
export class AlphaRouter {
constructor({ chainId, provider, multicall2Provider, v3PoolProvider, v3UniPoolProvider, onChainQuoteProvider, v2PoolProvider, v2FathomPoolProvider, v2QuoteProvider, v2SubgraphProvider, v2FathomSubgraphProvider, tokenProvider, blockedTokenListProvider, v3UniSubgraphProvider, v3SubgraphProvider, gasPriceProvider, v3GasModelFactory, v2GasModelFactory, mixedRouteGasModelFactory, swapRouterProvider, tokenValidatorProvider, routeCachingProvider, }) {
this.chainId = chainId;
this.provider = provider;
this.multicall2Provider =
multicall2Provider !== null && multicall2Provider !== void 0 ? multicall2Provider : new UniswapMulticallProvider(chainId, provider, 100000);
this.v3PoolProvider =
v3PoolProvider !== null && v3PoolProvider !== void 0 ? v3PoolProvider : new CachingV3PoolProvider(Protocol.V3, this.chainId, new V3PoolProvider(ID_TO_CHAIN_ID(chainId), this.multicall2Provider), new NodeJSCache(new NodeCache({ stdTTL: 360, useClones: false })));
this.v3UniPoolProvider =
v3UniPoolProvider !== null && v3UniPoolProvider !== void 0 ? v3UniPoolProvider : new CachingV3PoolProvider(Protocol.UNI_V3, this.chainId, new V3PoolProvider(ID_TO_CHAIN_ID(chainId), this.multicall2Provider, UniV3Factory), new NodeJSCache(new NodeCache({ stdTTL: 360, useClones: false })));
// this.simulator = simulator;
this.routeCachingProvider = routeCachingProvider;
if (onChainQuoteProvider) {
this.onChainQuoteProvider = onChainQuoteProvider;
}
else {
switch (chainId) {
// case ChainId.ARBITRUM_ONE:
// this.onChainQuoteProvider = new OnChainQuoteProvider(
// chainId,
// provider,
// this.multicall2Provider,
// {
// retries: 2,
// minTimeout: 100,
// maxTimeout: 1000,
// },
// {
// multicallChunk: 10,
// gasLimitPerCall: 12_000_000,
// quoteMinSuccessRate: 0.1,
// },
// {
// gasLimitOverride: 30_000_000,
// multicallChunk: 6,
// },
// {
// gasLimitOverride: 30_000_000,
// multicallChunk: 6,
// }
// );
// break;
default:
this.onChainQuoteProvider = new OnChainQuoteProvider(chainId, provider, this.multicall2Provider, {
retries: 2,
minTimeout: 100,
maxTimeout: 5000,
}, {
multicallChunk: 150,
gasLimitPerCall: 300000,
quoteMinSuccessRate: 0.15,
}, {
gasLimitOverride: 70000,
multicallChunk: 100,
});
break;
}
}
const retryOptions = {
retries: 2,
minTimeout: 50,
maxTimeout: 500,
};
this.v2PoolProvider =
v2PoolProvider !== null && v2PoolProvider !== void 0 ? v2PoolProvider : new CachingV2PoolProvider(Protocol.V2, chainId, new V2PoolProvider(chainId, this.multicall2Provider, retryOptions), new NodeJSCache(new NodeCache({ stdTTL: 60, useClones: false })));
this.v2QuoteProvider = v2QuoteProvider !== null && v2QuoteProvider !== void 0 ? v2QuoteProvider : new V2QuoteProvider();
this.v2FathomPoolProvider =
v2FathomPoolProvider !== null && v2FathomPoolProvider !== void 0 ? v2FathomPoolProvider : new CachingV2PoolProvider(Protocol.FATHOM, chainId, new V2PoolProvider(chainId, this.multicall2Provider, retryOptions, {
address: FATHOM_FACTORY_ADDRESS,
initCode: FATHOM_INIT_CODE_HASH,
}), new NodeJSCache(new NodeCache({ stdTTL: 60, useClones: false })));
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 xswapV2SubgraphUrl = process.env.XSWAP_V2_SUBGRAPH_URL;
const fathomV2SubgraphUrl = process.env.FATHOM_V2_SUBGRAPH_URL;
const xswapV3SubgraphUrl = process.env.XSWAP_V3_SUBGRAPH_URL;
const uniV3SubgraphUrl = process.env.UNI_V3_SUBGRAPH_URL;
if (v2SubgraphProvider) {
this.v2SubgraphProvider = v2SubgraphProvider;
}
else {
const v2Fallbacks = [];
if (xswapV2SubgraphUrl) {
v2Fallbacks.push(new CachingV2SubgraphProvider(chainId, new V2SubgraphProvider(chainId, xswapV2SubgraphUrl), new NodeJSCache(new NodeCache({ stdTTL: 60, useClones: false }))));
}
v2Fallbacks.push(new StaticV2SubgraphProvider(chainId));
this.v2SubgraphProvider = new V2SubgraphProviderWithFallBacks(v2Fallbacks);
}
if (v2FathomSubgraphProvider) {
this.v2FathomSubgraphProvider = v2FathomSubgraphProvider;
}
else {
const v2FathomFallbacks = [];
if (fathomV2SubgraphUrl) {
v2FathomFallbacks.push(new CachingV2SubgraphProvider(chainId, new FathomV2SubgraphProvider(chainId, fathomV2SubgraphUrl), new NodeJSCache(new NodeCache({ stdTTL: 60, useClones: false }))));
}
v2FathomFallbacks.push(new StaticV2FathomSubgraphProvider(chainId));
this.v2FathomSubgraphProvider = new V2SubgraphProviderWithFallBacks(v2FathomFallbacks);
}
if (v3SubgraphProvider) {
this.v3SubgraphProvider = v3SubgraphProvider;
}
else {
const v3Fallbacks = [];
if (xswapV3SubgraphUrl) {
v3Fallbacks.push(new CachingV3SubgraphProvider(chainId, new V3SubgraphProvider(chainId, xswapV3SubgraphUrl), new NodeJSCache(new NodeCache({ stdTTL: 60, useClones: false }))));
}
v3Fallbacks.push(new StaticV3SubgraphProvider(chainId, this.v3PoolProvider));
this.v3SubgraphProvider = new V3SubgraphProviderWithFallBacks(v3Fallbacks);
}
if (v3UniSubgraphProvider) {
this.v3UniSubgraphProvider = v3UniSubgraphProvider;
}
else {
const v3UniFallbacks = [];
if (uniV3SubgraphUrl) {
v3UniFallbacks.push(new CachingV3SubgraphProvider(chainId, new V3SubgraphProvider(chainId, uniV3SubgraphUrl), new NodeJSCache(new NodeCache({ stdTTL: 60, useClones: false }))));
}
v3UniFallbacks.push(new StaticV3UniSubgraphProvider(chainId, this.v3UniPoolProvider));
this.v3UniSubgraphProvider = new V3SubgraphProviderWithFallBacks(v3UniFallbacks);
}
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: 15, useClones: false })));
this.v3GasModelFactory =
v3GasModelFactory !== null && v3GasModelFactory !== void 0 ? v3GasModelFactory : new V3HeuristicGasModelFactory();
this.v2GasModelFactory =
v2GasModelFactory !== null && v2GasModelFactory !== void 0 ? v2GasModelFactory : new V2HeuristicGasModelFactory();
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 (tokenValidatorProvider) {
this.tokenValidatorProvider = tokenValidatorProvider;
}
else if (this.chainId === ChainId.XDC) {
this.tokenValidatorProvider = new TokenValidatorProvider(this.chainId, this.multicall2Provider, new NodeJSCache(new NodeCache({ stdTTL: 30000, useClones: false })));
}
// Initialize the Quoters.
// Quoters are an abstraction encapsulating the business logic of fetching routes and quotes.
this.v2QuoterFathom = new V2Quoter(this.v2FathomSubgraphProvider, this.v2FathomPoolProvider, this.v2QuoteProvider, this.v2GasModelFactory, this.tokenProvider, this.chainId, Protocol.FATHOM, this.blockedTokenListProvider, this.tokenValidatorProvider);
this.v2QuoterXSwap = new V2Quoter(this.v2SubgraphProvider, this.v2PoolProvider, this.v2QuoteProvider, this.v2GasModelFactory, this.tokenProvider, this.chainId, Protocol.V2, this.blockedTokenListProvider, this.tokenValidatorProvider);
this.v3QuoterUni = new V3Quoter(this.v3UniSubgraphProvider, this.v3UniPoolProvider, this.onChainQuoteProvider, this.tokenProvider, this.chainId, Protocol.UNI_V3, this.blockedTokenListProvider, this.tokenValidatorProvider);
this.v3QuoterXSwap = new V3Quoter(this.v3SubgraphProvider, this.v3PoolProvider, this.onChainQuoteProvider, this.tokenProvider, this.chainId, Protocol.V3, this.blockedTokenListProvider, this.tokenValidatorProvider);
this.mixedQuoter = new MixedQuoter(this.v3SubgraphProvider, this.v3PoolProvider, this.v3UniPoolProvider, this.v2SubgraphProvider, this.v2PoolProvider, this.onChainQuoteProvider, this.tokenProvider, this.chainId, this.blockedTokenListProvider, this.tokenValidatorProvider);
}
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,
Protocol.FATHOM,
Protocol.UNI_V3,
// Protocol.MIXED,
],
});
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 (isV3(route.protocol)) {
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, position.pool.factory);
}
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;
const { currencyIn, currencyOut } = this.determineCurrencyInOutFromTradeType(tradeType, amount, quoteCurrency);
const tokenIn = currencyIn.wrapped;
const tokenOut = currencyOut.wrapped;
metric.setProperty('chainId', this.chainId);
metric.setProperty('pair', `${tokenIn.symbol}/${tokenOut.symbol}`);
metric.setProperty('tokenIn', tokenIn.address);
metric.setProperty('tokenOut', tokenOut.address);
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 = (_a = partialRoutingConfig.blockNumber) !== null && _a !== void 0 ? _a : this.getBlockNumberPromise();
const routingConfig = _.merge({}, DEFAULT_ROUTING_CONFIG_BY_CHAIN(this.chainId), partialRoutingConfig, { blockNumber });
const gasPriceWei = await this.getGasPriceWei();
const quoteToken = quoteCurrency.wrapped;
const [v3GasModel, mixedRouteGasModel] = await this.getGasModels(gasPriceWei, amount.currency.wrapped, quoteToken);
// Create a Set to sanitize the protocols input, a Set of undefined becomes an empty set,
// Then create an Array from the values of that Set.
const protocols = Array.from(new Set(routingConfig.protocols).values());
const cacheMode = await ((_c = this.routeCachingProvider) === null || _c === void 0 ? void 0 : _c.getCacheMode(this.chainId, amount, quoteToken, tradeType, protocols));
// Fetch CachedRoutes
let cachedRoutes;
if (cacheMode !== CacheMode.Darkmode) {
cachedRoutes = await ((_d = this.routeCachingProvider) === null || _d === void 0 ? void 0 : _d.getCachedRoute(this.chainId, amount, quoteToken, tradeType, protocols, await blockNumber));
}
if (cacheMode && cacheMode !== CacheMode.Darkmode && !cachedRoutes) {
metric.putMetric(`GetCachedRoute_miss_${cacheMode}`, 1, MetricLoggerUnit.Count);
log.info({
tokenIn: tokenIn.symbol,
tokenInAddress: tokenIn.address,
tokenOut: tokenOut.symbol,
tokenOutAddress: tokenOut.address,
cacheMode,
amount: amount.toExact(),
chainId: this.chainId,
tradeType: this.tradeTypeStr(tradeType),
}, `GetCachedRoute miss ${cacheMode} for ${this.tokenPairSymbolTradeTypeChainId(tokenIn, tokenOut, tradeType)}`);
}
else if (cachedRoutes) {
metric.putMetric(`GetCachedRoute_hit_${cacheMode}`, 1, MetricLoggerUnit.Count);
log.info({
tokenIn: tokenIn.symbol,
tokenInAddress: tokenIn.address,
tokenOut: tokenOut.symbol,
tokenOutAddress: tokenOut.address,
cacheMode,
amount: amount.toExact(),
chainId: this.chainId,
tradeType: this.tradeTypeStr(tradeType),
}, `GetCachedRoute hit ${cacheMode} for ${this.tokenPairSymbolTradeTypeChainId(tokenIn, tokenOut, tradeType)}`);
}
let swapRouteFromCachePromise = Promise.resolve(null);
if (cachedRoutes) {
swapRouteFromCachePromise = this.getSwapRouteFromCache(cachedRoutes, await blockNumber, amount, quoteToken, tradeType, routingConfig, v3GasModel, mixedRouteGasModel, gasPriceWei);
}
let swapRouteFromChainPromise = Promise.resolve(null);
if (!cachedRoutes || cacheMode !== CacheMode.Livemode) {
swapRouteFromChainPromise = this.getSwapRouteFromChain(amount, tokenIn, tokenOut, protocols, quoteToken, tradeType, routingConfig, v3GasModel, mixedRouteGasModel, gasPriceWei);
}
const [swapRouteFromCache, swapRouteFromChain] = await Promise.all([
swapRouteFromCachePromise,
swapRouteFromChainPromise,
]);
let swapRouteRaw;
if (cacheMode === CacheMode.Livemode && swapRouteFromCache) {
log.info(`CacheMode is ${cacheMode}, and we are using swapRoute from cache`);
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);
log.info({
quoteFromChain: swapRouteFromChain.quote.toExact(),
quoteFromCache: swapRouteFromCache.quote.toExact(),
quoteDiff: quoteDiff.toExact(),
quoteGasAdjustedFromChain: swapRouteFromChain.quoteGasAdjusted.toExact(),
quoteGasAdjustedFromCache: swapRouteFromCache.quoteGasAdjusted.toExact(),
quoteGasAdjustedDiff: quoteGasAdjustedDiff.toExact(),
gasUsedFromChain: swapRouteFromChain.estimatedGasUsed.toString(),
gasUsedFromCache: swapRouteFromCache.estimatedGasUsed.toString(),
gasUsedDiff: gasUsedDiff.toString(),
routesFromChain: swapRouteFromChain.routes.toString(),
routesFromCache: swapRouteFromCache.routes.toString(),
amount: amount.toExact(),
pair: this.tokenPairSymbolTradeTypeChainId(tokenIn, tokenOut, tradeType),
}, `Comparing quotes between Chain and Cache for ${this.tokenPairSymbolTradeTypeChainId(tokenIn, tokenOut, tradeType)}`);
}
if (!swapRouteRaw) {
return null;
}
const { quote, quoteGasAdjusted, estimatedGasUsed, routes: routeAmounts, estimatedGasUsedQuoteToken, estimatedGasUsedUSD, } = swapRouteRaw;
if (this.routeCachingProvider &&
cacheMode !== CacheMode.Darkmode &&
swapRouteFromChain) {
// Generate the object to be cached
const routesToCache = CachedRoutes.fromRoutesWithValidQuotes(swapRouteFromChain.routes, this.chainId, tokenIn, tokenOut, protocols.sort(), // sort it for consistency in the order of the protocols.
await blockNumber, tradeType);
if (routesToCache) {
const tokenPairSymbolTradeTypeChainId = this.tokenPairSymbolTradeTypeChainId(tokenIn, tokenOut, tradeType);
// Attempt to insert the entry in cache. This is fire and forget promise.
// The catch method will prevent any exception from blocking the normal code execution.
this.routeCachingProvider
.setCachedRoute(routesToCache, amount)
.then((success) => {
const status = success ? 'success' : 'rejected';
metric.putMetric(`SetCachedRoute_${tokenPairSymbolTradeTypeChainId}_${status}`, 1, MetricLoggerUnit.Count);
})
.catch((reason) => {
log.info({
reason: reason,
tokenPair: tokenPairSymbolTradeTypeChainId,
}, `SetCachedRoute failure`);
metric.putMetric(`SetCachedRoute_${tokenPairSymbolTradeTypeChainId}_failure`, 1, MetricLoggerUnit.Count);
});
}
}
metric.putMetric(`QuoteFoundForChain${this.chainId}`, 1, MetricLoggerUnit.Count);
// 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, this.chainId);
}
const swapRoute = {
quote,
quoteGasAdjusted,
estimatedGasUsed,
estimatedGasUsedQuoteToken,
estimatedGasUsedUSD,
gasPriceWei,
route: routeAmounts,
trade,
methodParameters,
blockNumber: BigNumber.from(await blockNumber),
};
if (swapConfig &&
swapConfig.simulate &&
methodParameters &&
methodParameters.calldata) {
if (!this.simulator) {
throw new Error('Simulator not initialized!');
}
log.info({ swapConfig, methodParameters }, 'Starting simulation');
const fromAddress = swapConfig.simulate.fromAddress;
const beforeSimulate = Date.now();
const swapRouteWithSimulation = await this.simulator.simulate(fromAddress, swapConfig, swapRoute, amount,
// Quote will be in WETH even if quoteCurrency is ETH
// So we init a new CurrencyAmount object here
CurrencyAmount.fromRawAmount(quoteCurrency, quote.quotient.toString()));
metric.putMetric('SimulateTransaction', Date.now() - beforeSimulate, MetricLoggerUnit.Milliseconds);
return swapRouteWithSimulation;
}
return swapRoute;
}
async getSwapRouteFromCache(cachedRoutes, blockNumber, amount, quoteToken, tradeType, routingConfig, v3GasModel, mixedRouteGasModel, gasPriceWei) {
log.info({
protocols: cachedRoutes.protocolsCovered,
tradeType: cachedRoutes.tradeType,
cachedBlockNumber: cachedRoutes.blockNumber,
quoteBlockNumber: blockNumber,
}, 'Routing across CachedRoute');
const quotePromises = [];
const v3Routes = cachedRoutes.routes.filter((route) => isV3(route.protocol));
const v2Routes = cachedRoutes.routes.filter((route) => isV2(route.protocol));
const mixedRoutes = cachedRoutes.routes.filter((route) => route.protocol === Protocol.MIXED);
const percents = [];
if (v3Routes) {
const v3RoutesFromCache = v3Routes.map((cachedRoute) => cachedRoute.route);
const v3PercentsFromCache = v3Routes.map((cachedRoute) => cachedRoute.percent);
percents.push(...v3PercentsFromCache);
const v3Amounts = v3PercentsFromCache.map((percent) => amount.multiply(new Fraction(percent, 100)));
quotePromises.push(this.v3QuoterXSwap.getQuotes(v3RoutesFromCache, v3Amounts, v3PercentsFromCache, quoteToken, tradeType, routingConfig, undefined, v3GasModel));
}
if (v2Routes) {
const v2RoutesFromCache = v2Routes.map((cachedRoute) => cachedRoute.route);
const v2PercentsFromCache = v2Routes.map((cachedRoute) => cachedRoute.percent);
percents.push(...v2PercentsFromCache);
const v2Amounts = v2PercentsFromCache.map((percent) => amount.multiply(new Fraction(percent, 100)));
quotePromises.push(this.v2QuoterFathom.getQuotes(v2RoutesFromCache, v2Amounts, v2PercentsFromCache, quoteToken, tradeType, routingConfig, undefined, undefined, gasPriceWei));
}
if (mixedRoutes) {
const mixedRoutesFromCache = mixedRoutes.map((cachedRoute) => cachedRoute.route);
const mixedPercentsFromCache = mixedRoutes.map((cachedRoute) => cachedRoute.percent);
percents.push(...mixedPercentsFromCache);
const mixedAmounts = mixedPercentsFromCache.map((percent) => amount.multiply(new Fraction(percent, 100)));
quotePromises.push(this.mixedQuoter.getQuotes(mixedRoutesFromCache, mixedAmounts, mixedPercentsFromCache, quoteToken, tradeType, routingConfig, undefined, mixedRouteGasModel));
}
const getQuotesResults = await Promise.all(quotePromises);
const allRoutesWithValidQuotes = _.flatMap(getQuotesResults, (quoteResult) => quoteResult.routesWithValidQuotes);
return getBestSwapRoute(amount, percents, allRoutesWithValidQuotes, tradeType, this.chainId, routingConfig, v3GasModel);
}
async getSwapRouteFromChain(amount, tokenIn, tokenOut, protocols, quoteToken, tradeType, routingConfig, v3GasModel, mixedRouteGasModel, gasPriceWei) {
// Generate our distribution of amounts, i.e. fractions of the input amount.
// We will get quotes for fractions of the input amount for different routes, then
// combine to generate split routes.
const [percents, amounts] = this.getAmountDistribution(amount, routingConfig);
const noProtocolsSpecified = protocols.length === 0;
const v3ProtocolSpecified = protocols.includes(Protocol.V3);
const v3UniProtocolSpecified = protocols.includes(Protocol.UNI_V3);
const v2XSwapProtocolSpecified = protocols.includes(Protocol.V2);
const v2FathomProtocolSpecified = protocols.includes(Protocol.FATHOM);
const v2SupportedInChain = V2_SUPPORTED.includes(this.chainId);
const shouldQueryMixedProtocol = protocols.includes(Protocol.MIXED) ||
(noProtocolsSpecified && v2SupportedInChain);
const mixedProtocolAllowed = SUPPORTED_CHAINS.includes(this.chainId) &&
tradeType === TradeType.EXACT_INPUT;
const quotePromises = [];
// Maybe Quote V3 - if V3 or V3-UNI is specified, or no protocol is specified
if (v3ProtocolSpecified || noProtocolsSpecified) {
log.info({ protocols, tradeType }, 'Routing across V3 XSwap');
quotePromises.push(this.v3QuoterXSwap.getRoutesThenQuotes(tokenIn, tokenOut, amounts, percents, quoteToken, tradeType, routingConfig, v3GasModel));
}
if (v3UniProtocolSpecified) {
log.info({ protocols, tradeType }, 'Routing across V3 Uni');
quotePromises.push(this.v3QuoterUni.getRoutesThenQuotes(tokenIn, tokenOut, amounts, percents, quoteToken, tradeType, routingConfig, v3GasModel));
}
if (v2FathomProtocolSpecified) {
log.info({ protocols, tradeType }, 'Routing across V2 Fathom');
quotePromises.push(this.v2QuoterFathom.getRoutesThenQuotes(tokenIn, tokenOut, amounts, percents, quoteToken, tradeType, routingConfig, undefined, gasPriceWei));
}
// Maybe Quote V2 - if V2 is specified, or no protocol is specified AND v2 is supported in this chain
if (v2SupportedInChain &&
(v2XSwapProtocolSpecified || noProtocolsSpecified)) {
log.info({ protocols, tradeType }, 'Routing across V2');
quotePromises.push(this.v2QuoterXSwap.getRoutesThenQuotes(tokenIn, tokenOut, amounts, percents, quoteToken, tradeType, routingConfig, undefined, gasPriceWei));
}
// Maybe Quote mixed routes
// if MixedProtocol is specified or no protocol is specified and v2 is supported AND tradeType is ExactIn
// AND is Mainnet or Gorli
if (shouldQueryMixedProtocol && mixedProtocolAllowed) {
log.info({ protocols, tradeType }, 'Routing across MixedRoutes');
quotePromises.push(this.mixedQuoter.getRoutesThenQuotes(tokenIn, tokenOut, amounts, percents, quoteToken, tradeType, routingConfig, mixedRouteGasModel));
}
const getQuotesResults = await Promise.all(quotePromises);
const allRoutesWithValidQuotes = [];
const allCandidatePools = [];
getQuotesResults.forEach((getQuoteResult) => {
allRoutesWithValidQuotes.push(...getQuoteResult.routesWithValidQuotes);
// if (getQuoteResult.candidatePools) {
// allCandidatePools.push(getQuoteResult.candidatePools);
// }
});
if (allRoutesWithValidQuotes.length === 0) {
log.info({ allRoutesWithValidQuotes }, 'Received no valid quotes');
return null;
}
// Given all the quotes for all the amounts for all the routes, find the best combination.
const bestSwapRoute = await getBestSwapRoute(amount, percents, allRoutesWithValidQuotes, tradeType, this.chainId, routingConfig, v3GasModel);
if (bestSwapRoute) {
this.emitPoolSelectionMetrics(bestSwapRoute, allCandidatePools);
}
return bestSwapRoute;
}
tradeTypeStr(tradeType) {
return tradeType === TradeType.EXACT_INPUT ? 'ExactIn' : 'ExactOut';
}
tokenPairSymbolTradeTypeChainId(tokenIn, tokenOut, tradeType) {
return `${tokenIn.symbol}/${tokenOut.symbol}/${this.tradeTypeStr(tradeType)}/${this.chainId}`;
}
determineCurrencyInOutFromTradeType(tradeType, amount, quoteCurrency) {
if (tradeType === TradeType.EXACT_INPUT) {
return {
currencyIn: amount.currency,
currencyOut: quoteCurrency,
};
}
else {
return {
currencyIn: quoteCurrency,
currencyOut: amount.currency,
};
}
}
async getGasPriceWei() {
// Track how long it takes to resolve this async call.
const beforeGasTimestamp = Date.now();
// Get an estimate of the gas price to use when estimating gas cost of different routes.
const { gasPriceWei } = await this.gasPriceProvider.getGasPrice();
metric.putMetric('GasPriceLoad', Date.now() - beforeGasTimestamp, MetricLoggerUnit.Milliseconds);
return gasPriceWei;
}
async getGasModels(gasPriceWei, amountToken, quoteToken) {
const beforeGasModel = Date.now();
const v3GasModelPromise = this.v3GasModelFactory.buildGasModel({
chainId: this.chainId,
gasPriceWei,
v3poolProvider: this.v3PoolProvider,
amountToken,
quoteToken,
v2poolProvider: this.v2PoolProvider,
// l2GasDataProvider: this.l2GasDataProvider,
});
const mixedRouteGasModelPromise = this.mixedRouteGasModelFactory.buildGasModel({
chainId: this.chainId,
gasPriceWei,
v3poolProvider: this.v3PoolProvider,
amountToken,
quoteToken,
v2poolProvider: this.v2PoolProvider,
});
const [v3GasModel, mixedRouteGasModel] = await Promise.all([
v3GasModelPromise,
mixedRouteGasModelPromise,
]);
metric.putMetric('GasModelCreation', Date.now() - beforeGasModel, MetricLoggerUnit.Milliseconds);
return [v3GasModel, mixedRouteGasModel];
}
// Note multiplications here can result in a loss of precision in the amounts (e.g. taking 50% of 101)
// This is reconcilled at the end of the algorithm by adding any lost precision to one of
// the splits in the route.
getAmountDistribution(amount, routingConfig) {
const { distributionPercent } = routingConfig;
const percents = [];
const amounts = [];
for (let i = 1; i <= 100 / distributionPercent; i++) {
percents.push(i * distributionPercent);
amounts.push(amount.multiply(new 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),
to: SWAP_ROUTER_02_ADDRESSES(this.chainId),
};
}
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;
let hasMixedRoute = false;
for (const routeAmount of routeAmounts) {
if (isV3(routeAmount.protocol)) {
hasV3Route = true;
}
if (isV2(routeAmount.protocol)) {
hasV2Route = true;
}
if (routeAmount.protocol === Protocol.MIXED) {
hasMixedRoute = true;
}
}
if (hasMixedRoute && (hasV3Route || hasV2Route)) {
if (hasV3Route && hasV2Route) {
metric.putMetric(`MixedAndV3AndV2SplitRoute`, 1, MetricLoggerUnit.Count);
metric.putMetric(`MixedAndV3AndV2SplitRouteForChain${this.chainId}`, 1, MetricLoggerUnit.Count);
}
else if (hasV3Route) {
metric.putMetric(`MixedAndV3SplitRoute`, 1, MetricLoggerUnit.Count);
metric.putMetric(`MixedAndV3SplitRouteForChain${this.chainId}`, 1, MetricLoggerUnit.Count);
}
else if (hasV2Route) {
metric.putMetric(`MixedAndV2SplitRoute`, 1, MetricLoggerUnit.Count);
metric.putMetric(`MixedAndV2SplitRouteForChain${this.chainId}`, 1, MetricLoggerUnit.Count);
}
}
else if (hasV3Route && hasV2Route) {
metric.putMetric(`V3AndV2SplitRoute`, 1, MetricLoggerUnit.Count);
metric.putMetric(`V3AndV2SplitRouteForChain${this.chainId}`, 1, MetricLoggerUnit.Count);
}
else if (hasMixedRoute) {
if (routeAmounts.length > 1) {
metric.putMetric(`MixedSplitRoute`, 1, MetricLoggerUnit.Count);
metric.putMetric(`MixedSplitRouteForChain${this.chainId}`, 1, MetricLoggerUnit.Count);
}
else {
metric.putMetric(`MixedRoute`, 1, MetricLoggerUnit.Count);
metric.putMetric(`MixedRouteForChain${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;
}
async userHasSufficientBalance(fromAddress, tradeType, amount, quote) {
try {
const neededBalance = tradeType === TradeType.EXACT_INPUT ? amount : quote;
let balance;
if (neededBalance.currency.isNative) {
balance = await this.provider.getBalance(fromAddress);
}
else {
const tokenContract = Erc20__factory.connect(neededBalance.currency.address, this.provider);
balance = await tokenContract.balanceOf(fromAddress);
}
return balance.gte(BigNumber.from(neededBalance.quotient.toString()));
}
catch (e) {
log.error(e, 'Error while checking user balance');
return false;
}
}
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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYWxwaGEtcm91dGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL3JvdXRlcnMvYWxwaGEtcm91dGVyL2FscGhhLXJvdXRlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsU0FBUyxFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFDckQsT0FBTyxFQUFnQixlQUFlLEVBQUUsTUFBTSwwQkFBMEIsQ0FBQztBQUN6RSxPQUFPLGtCQUFrQixNQUFNLDZCQUE2QixDQUFDO0FBRTdELE9BQU8sRUFBRSxRQUFRLEVBQUUsVUFBVSxFQUFTLE1BQU0sNkJBQTZCLENBQUM7QUFDMUUsT0FBTyxFQUNMLE9BQU8sRUFFUCxRQUFRLEVBQ1IsZ0JBQWdCLEVBQ2hCLHdCQUF3QixFQUV4QixTQUFTLEdBQ1YsTUFBTSwyQkFBMkIsQ0FBQztBQUNuQyxPQUFPLEVBQ0wsSUFBSSxFQUNKLFFBQVEsRUFDUixhQUFhLEVBQ2IsUUFBUSxFQUNSLFlBQVksR0FDYixNQUFNLHlCQUF5QixDQUFDO0FBQ2pDLE9BQU8sS0FBSyxNQUFNLGFBQWEsQ0FBQztBQUNoQyxPQUFPLElBQUksTUFBTSxNQUFNLENBQUM7QUFDeEIsT0FBTyxDQUFDLE1BQU0sUUFBUSxDQUFDO0FBQ3ZCLE9BQU8sU0FBUyxNQUFNLFlBQVksQ0FBQztBQUVuQyxPQUFPLEVBQ0wsWUFBWSxFQUNaLFNBQVMsRUFDVCx5QkFBeUIsRUFDekIsZ0NBQWdDLEVBQ2hDLHFCQUFxQixFQUNyQix5QkFBeUIsRUFDekIscUJBQXFCLEVBQ3JCLHlCQUF5QixFQUN6Qix1QkFBdUIsRUFDdkIseUJBQXlCLEVBTXpCLHNCQUFzQixFQUN0QixXQUFXLEVBQ1gsdUJBQXVCLEVBQ3ZCLG9CQUFvQixFQUVwQix3QkFBd0IsRUFDeEIsd0JBQXdCLEVBQ3hCLGtCQUFrQixFQUNsQix3QkFBd0IsRUFDeEIsZUFBZSxFQUNmLGtCQUFrQixFQUNsQiwrQkFBK0IsRUFDL0Isa0JBQWtCLEVBQ2xCLCtCQUErQixHQUNoQyxNQUFNLGlCQUFpQixDQUFDO0FBQ3pCLE9BQU8sRUFDTCx3QkFBd0IsR0FFekIsTUFBTSw2Q0FBNkMsQ0FBQztBQUtyRCxPQUFPLEVBQWtCLGFBQWEsRUFBRSxNQUFNLGdDQUFnQyxDQUFDO0FBQy9FLE9BQU8sRUFFTCxzQkFBc0IsR0FDdkIsTUFBTSwwQ0FBMEMsQ0FBQztBQUNsRCxPQUFPLEVBRUwsY0FBYyxHQUNmLE1BQU0sa0NBQWtDLENBQUM7QUFDMUMsT0FBTyxFQUNMLHNCQUFzQixFQUN0QixxQkFBcUIsR0FDdEIsTUFBTSxxQ0FBcUMsQ0FBQztBQUM3QyxPQUFPLEVBQUUsOEJBQThCLEVBQUUsTUFBTSxvREFBb0QsQ0FBQztBQUNwRyxPQUFPLEVBQUUsa0JBQWtCLElBQUksd0JBQXdCLEVBQUUsTUFBTSw2Q0FBNkMsQ0FBQztBQU03RyxPQUFPLEVBRUwsY0FBYyxHQUNmLE1BQU0sa0NBQWtDLENBQUM7QUFDMUMsT0FBTyxFQUFFLDJCQUEyQixFQUFFLE1BQU0sb0RBQW9ELENBQUM7QUFFakcsT0FBTyxFQUFFLGNBQWMsRUFBRSxNQUFNLDRDQUE0QyxDQUFDO0FBQzVFLE9BQU8sRUFBRSxJQUFJLEVBQUUsSUFBSSxFQUFFLE1BQU0sWUFBWSxDQUFDO0FBQ3hDLE9BQU8sRUFBRSxjQUFjLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQztBQUNwRCxPQUF