@uniswap/smart-order-router
Version:
Uniswap Smart Order Router
681 lines • 71.6 kB
JavaScript
import { BigNumber } from '@ethersproject/bignumber';
import { encodeMixedRouteToPath, MixedRouteSDK, Protocol, } from '@uniswap/router-sdk';
import { ChainId } from '@uniswap/sdk-core';
import { encodeRouteToPath as encodeV3RouteToPath } from '@uniswap/v3-sdk';
import { encodeRouteToPath as encodeV4RouteToPath, Pool as V4Pool, } from '@uniswap/v4-sdk';
import retry from 'async-retry';
import _ from 'lodash';
import stats from 'stats-lite';
import { V2Route, } from '../routers/router';
import { IMixedRouteQuoterV1__factory } from '../types/other/factories/IMixedRouteQuoterV1__factory';
import { MixedRouteQuoterV2__factory } from '../types/other/factories/MixedRouteQuoterV2__factory';
import { V4Quoter__factory } from '../types/other/factories/V4Quoter__factory';
import { IQuoterV2__factory } from '../types/v3/factories/IQuoterV2__factory';
import { getAddress, ID_TO_NETWORK_NAME, metric, MetricLoggerUnit, MIXED_ROUTE_QUOTER_V1_ADDRESSES, MIXED_ROUTE_QUOTER_V2_ADDRESSES, NEW_QUOTER_V2_ADDRESSES, PROTOCOL_V4_QUOTER_ADDRESSES, } from '../util';
import { log } from '../util/log';
import { DEFAULT_BLOCK_NUMBER_CONFIGS, DEFAULT_SUCCESS_RATE_FAILURE_OVERRIDES, } from '../util/onchainQuoteProviderConfigs';
import { routeToString } from '../util/routes';
export class BlockConflictError extends Error {
constructor() {
super(...arguments);
this.name = 'BlockConflictError';
}
}
export class SuccessRateError extends Error {
constructor() {
super(...arguments);
this.name = 'SuccessRateError';
}
}
export class ProviderBlockHeaderError extends Error {
constructor() {
super(...arguments);
this.name = 'ProviderBlockHeaderError';
}
}
export class ProviderTimeoutError extends Error {
constructor() {
super(...arguments);
this.name = 'ProviderTimeoutError';
}
}
/**
* This error typically means that the gas used by the multicall has
* exceeded the total call gas limit set by the node provider.
*
* This can be resolved by modifying BatchParams to request fewer
* quotes per call, or to set a lower gas limit per quote.
*
* @export
* @class ProviderGasError
*/
export class ProviderGasError extends Error {
constructor() {
super(...arguments);
this.name = 'ProviderGasError';
}
}
const DEFAULT_BATCH_RETRIES = 2;
/**
* Computes on chain quotes for swaps. For pure V3 routes, quotes are computed on-chain using
* the 'QuoterV2' smart contract. For exactIn mixed and V2 routes, quotes are computed using the 'MixedRouteQuoterV1' contract
* This is because computing quotes off-chain would require fetching all the tick data for each pool, which is a lot of data.
*
* To minimize the number of requests for quotes we use a Multicall contract. Generally
* the number of quotes to fetch exceeds the maximum we can fit in a single multicall
* while staying under gas limits, so we also batch these quotes across multiple multicalls.
*
* The biggest challenge with the quote provider is dealing with various gas limits.
* Each provider sets a limit on the amount of gas a call can consume (on Infura this
* is approximately 10x the block max size), so we must ensure each multicall does not
* exceed this limit. Additionally, each quote on V3 can consume a large number of gas if
* the pool lacks liquidity and the swap would cause all the ticks to be traversed.
*
* To ensure we don't exceed the node's call limit, we limit the gas used by each quote to
* a specific value, and we limit the number of quotes in each multicall request. Users of this
* class should set BatchParams such that multicallChunk * gasLimitPerCall is less than their node
* providers total gas limit per call.
*
* @export
* @class OnChainQuoteProvider
*/
export class OnChainQuoteProvider {
/**
* Creates an instance of OnChainQuoteProvider.
*
* @param chainId The chain to get quotes for.
* @param provider The web 3 provider.
* @param multicall2Provider The multicall provider to use to get the quotes on-chain.
* Only supports the Uniswap Multicall contract as it needs the gas limitting functionality.
* @param retryOptions The retry options for each call to the multicall.
* @param batchParams The parameters for each batched call to the multicall.
* @param gasErrorFailureOverride The gas and chunk parameters to use when retrying a batch that failed due to out of gas.
* @param successRateFailureOverrides The parameters for retries when we fail to get quotes.
* @param blockNumberConfig Parameters for adjusting which block we get quotes from, and how to handle block header not found errors.
* @param [quoterAddressOverride] Overrides the address of the quoter contract to use.
* @param metricsPrefix metrics prefix to differentiate between different instances of the quote provider.
*/
constructor(chainId, provider,
// Only supports Uniswap Multicall as it needs the gas limitting functionality.
multicall2Provider,
// retryOptions, batchParams, and gasErrorFailureOverride are always override in alpha-router
// so below default values are always not going to be picked up in prod.
// So we will not extract out below default values into constants.
retryOptions = {
retries: DEFAULT_BATCH_RETRIES,
minTimeout: 25,
maxTimeout: 250,
}, batchParams = (_optimisticCachedRoutes, _protocol) => {
return {
multicallChunk: 150,
gasLimitPerCall: 1000000,
quoteMinSuccessRate: 0.2,
};
}, gasErrorFailureOverride = (_protocol) => {
return {
gasLimitOverride: 1500000,
multicallChunk: 100,
};
},
// successRateFailureOverrides and blockNumberConfig are not always override in alpha-router.
// So we will extract out below default values into constants.
// In alpha-router default case, we will also define the constants with same values as below.
successRateFailureOverrides = (_protocol) => {
return DEFAULT_SUCCESS_RATE_FAILURE_OVERRIDES;
}, blockNumberConfig = (_protocol) => {
return DEFAULT_BLOCK_NUMBER_CONFIGS;
}, quoterAddressOverride, metricsPrefix = (chainId, useMixedRouteQuoter, mixedRouteContainsV4Pool, protocol, optimisticCachedRoutes) => useMixedRouteQuoter
? `ChainId_${chainId}_${protocol}RouteQuoter${mixedRouteContainsV4Pool ? 'V2' : 'V1'}_OptimisticCachedRoutes${optimisticCachedRoutes}_`
: `ChainId_${chainId}_${protocol}Quoter_OptimisticCachedRoutes${optimisticCachedRoutes}_`) {
this.chainId = chainId;
this.provider = provider;
this.multicall2Provider = multicall2Provider;
this.retryOptions = retryOptions;
this.batchParams = batchParams;
this.gasErrorFailureOverride = gasErrorFailureOverride;
this.successRateFailureOverrides = successRateFailureOverrides;
this.blockNumberConfig = blockNumberConfig;
this.quoterAddressOverride = quoterAddressOverride;
this.metricsPrefix = metricsPrefix;
}
getQuoterAddress(useMixedRouteQuoter, mixedRouteContainsV4Pool, protocol) {
if (this.quoterAddressOverride) {
const quoterAddress = this.quoterAddressOverride(useMixedRouteQuoter, mixedRouteContainsV4Pool, protocol);
if (!quoterAddress) {
throw new Error(`No address for the quoter contract on chain id: ${this.chainId} ${useMixedRouteQuoter} ${mixedRouteContainsV4Pool} ${protocol}`);
}
return quoterAddress;
}
const quoterAddress = useMixedRouteQuoter
? mixedRouteContainsV4Pool
? MIXED_ROUTE_QUOTER_V2_ADDRESSES[this.chainId]
: MIXED_ROUTE_QUOTER_V1_ADDRESSES[this.chainId]
: protocol === Protocol.V3
? NEW_QUOTER_V2_ADDRESSES[this.chainId]
: PROTOCOL_V4_QUOTER_ADDRESSES[this.chainId];
if (!quoterAddress) {
throw new Error(`No address for the quoter contract on chain id: ${this.chainId}`);
}
return quoterAddress;
}
async getQuotesManyExactIn(amountIns, routes, providerConfig) {
return this.getQuotesManyData(amountIns, routes, 'quoteExactInput', providerConfig);
}
async getQuotesManyExactOut(amountOuts, routes, providerConfig) {
return this.getQuotesManyData(amountOuts, routes, 'quoteExactOutput', providerConfig);
}
encodeRouteToPath(route, functionName) {
switch (route.protocol) {
case Protocol.V3:
return encodeV3RouteToPath(route, functionName == 'quoteExactOutput' // For exactOut must be true to ensure the routes are reversed.
);
case Protocol.V4:
return encodeV4RouteToPath(route, functionName == 'quoteExactOutput');
// We don't have onchain V2 quoter, but we do have a mixed quoter that can quote against v2 routes onchain
// Hence in case of V2 or mixed, we explicitly encode into mixed routes.
case Protocol.V2:
case Protocol.MIXED:
// we need to retain the fake pool data for the mixed route
return encodeMixedRouteToPath(route instanceof V2Route
? new MixedRouteSDK(route.pairs, route.input, route.output, true)
: route);
default:
throw new Error(`Unsupported protocol for the route: ${JSON.stringify(route)}`);
}
}
getContractInterface(useMixedRouteQuoter, mixedRouteContainsV4Pool, protocol) {
if (useMixedRouteQuoter) {
if (mixedRouteContainsV4Pool) {
return MixedRouteQuoterV2__factory.createInterface();
}
else {
return IMixedRouteQuoterV1__factory.createInterface();
}
}
switch (protocol) {
case Protocol.V3:
return IQuoterV2__factory.createInterface();
case Protocol.V4:
return V4Quoter__factory.createInterface();
default:
throw new Error(`Unsupported protocol: ${protocol}`);
}
}
async consolidateResults(protocol, useMixedRouteQuoter, mixedRouteContainsV4Pool, functionName, inputs, providerConfig, gasLimitOverride) {
if ((protocol === Protocol.MIXED && mixedRouteContainsV4Pool) ||
protocol === Protocol.V4) {
const mixedQuote = await this.multicall2Provider.callSameFunctionOnContractWithMultipleParams({
address: this.getQuoterAddress(useMixedRouteQuoter, mixedRouteContainsV4Pool, protocol),
contractInterface: this.getContractInterface(useMixedRouteQuoter, mixedRouteContainsV4Pool, protocol),
functionName,
functionParams: inputs,
providerConfig,
additionalConfig: {
gasLimitPerCallOverride: gasLimitOverride,
},
});
return {
blockNumber: mixedQuote.blockNumber,
approxGasUsedPerSuccessCall: mixedQuote.approxGasUsedPerSuccessCall,
results: mixedQuote.results.map((result) => {
if (result.success) {
switch (functionName) {
case 'quoteExactInput':
case 'quoteExactOutput':
return {
success: true,
result: [
result.result[0],
Array(inputs.length),
Array(inputs.length),
result.result[1],
],
};
default:
throw new Error(`Unsupported function name: ${functionName}`);
}
}
else {
return result;
}
}),
};
}
else {
return await this.multicall2Provider.callSameFunctionOnContractWithMultipleParams({
address: this.getQuoterAddress(useMixedRouteQuoter, mixedRouteContainsV4Pool, protocol),
contractInterface: this.getContractInterface(useMixedRouteQuoter, mixedRouteContainsV4Pool, protocol),
functionName,
functionParams: inputs,
providerConfig,
additionalConfig: {
gasLimitPerCallOverride: gasLimitOverride,
},
});
}
}
async getQuotesManyData(amounts, routes, functionName, _providerConfig) {
var _a, _b;
const useMixedRouteQuoter = routes.some((route) => route.protocol === Protocol.V2) ||
routes.some((route) => route.protocol === Protocol.MIXED);
const useV4RouteQuoter = routes.some((route) => route.protocol === Protocol.V4) &&
!useMixedRouteQuoter;
const mixedRouteContainsV4Pool = useMixedRouteQuoter
? routes.some((route) => route.protocol === Protocol.MIXED &&
route.pools.some((pool) => pool instanceof V4Pool))
: false;
const protocol = useMixedRouteQuoter
? Protocol.MIXED
: useV4RouteQuoter
? Protocol.V4
: Protocol.V3;
const optimisticCachedRoutes = (_a = _providerConfig === null || _providerConfig === void 0 ? void 0 : _providerConfig.optimisticCachedRoutes) !== null && _a !== void 0 ? _a : false;
/// Validate that there are no incorrect routes / function combinations
this.validateRoutes(routes, functionName, useMixedRouteQuoter);
let multicallChunk = this.batchParams(optimisticCachedRoutes, protocol).multicallChunk;
let gasLimitOverride = this.batchParams(optimisticCachedRoutes, protocol).gasLimitPerCall;
const { baseBlockOffset, rollback } = this.blockNumberConfig(protocol);
// Apply the base block offset if provided
const originalBlockNumber = await this.provider.getBlockNumber();
const providerConfig = {
..._providerConfig,
blockNumber: (_b = _providerConfig === null || _providerConfig === void 0 ? void 0 : _providerConfig.blockNumber) !== null && _b !== void 0 ? _b : originalBlockNumber + baseBlockOffset,
};
const inputs = _(routes)
.flatMap((route) => {
const encodedRoute = this.encodeRouteToPath(route, functionName);
const routeInputs = amounts.map((amount) => {
switch (route.protocol) {
case Protocol.V4:
return [
{
exactCurrency: getAddress(amount.currency),
path: encodedRoute,
exactAmount: amount.quotient.toString(),
},
];
case Protocol.MIXED:
if (mixedRouteContainsV4Pool) {
return [
encodedRoute,
{
nonEncodableData: route.pools.map((_) => {
return {
hookData: '0x',
};
}),
},
amount.quotient.toString(),
];
}
else {
return [encodedRoute, amount.quotient.toString()];
}
default:
return [
encodedRoute,
`0x${amount.quotient.toString(16)}`,
];
}
});
return routeInputs;
})
.value();
const normalizedChunk = Math.ceil(inputs.length / Math.ceil(inputs.length / multicallChunk));
const inputsChunked = _.chunk(inputs, normalizedChunk);
let quoteStates = _.map(inputsChunked, (inputChunk) => {
return {
status: 'pending',
inputs: inputChunk,
};
});
log.info(`About to get ${inputs.length} quotes in chunks of ${normalizedChunk} [${_.map(inputsChunked, (i) => i.length).join(',')}] ${gasLimitOverride
? `with a gas limit override of ${gasLimitOverride}`
: ''} and block number: ${await providerConfig.blockNumber} [Original before offset: ${originalBlockNumber}].`);
metric.putMetric(`${this.metricsPrefix(this.chainId, useMixedRouteQuoter, mixedRouteContainsV4Pool, protocol, optimisticCachedRoutes)}QuoteBatchSize`, inputs.length, MetricLoggerUnit.Count);
metric.putMetric(`${this.metricsPrefix(this.chainId, useMixedRouteQuoter, mixedRouteContainsV4Pool, protocol, optimisticCachedRoutes)}QuoteBatchSize_${ID_TO_NETWORK_NAME(this.chainId)}`, inputs.length, MetricLoggerUnit.Count);
const startTime = Date.now();
let haveRetriedForSuccessRate = false;
let haveRetriedForBlockHeader = false;
let blockHeaderRetryAttemptNumber = 0;
let haveIncrementedBlockHeaderFailureCounter = false;
let blockHeaderRolledBack = false;
let haveRetriedForBlockConflictError = false;
let haveRetriedForOutOfGas = false;
let haveRetriedForTimeout = false;
let haveRetriedForUnknownReason = false;
let finalAttemptNumber = 1;
const expectedCallsMade = quoteStates.length;
let totalCallsMade = 0;
const { results: quoteResults, blockNumber, approxGasUsedPerSuccessCall, } = await retry(async (_bail, attemptNumber) => {
haveIncrementedBlockHeaderFailureCounter = false;
finalAttemptNumber = attemptNumber;
const [success, failed, pending] = this.partitionQuotes(quoteStates);
log.info(`Starting attempt: ${attemptNumber}.
Currently ${success.length} success, ${failed.length} failed, ${pending.length} pending.
Gas limit override: ${gasLimitOverride} Block number override: ${providerConfig.blockNumber}.`);
quoteStates = await Promise.all(_.map(quoteStates, async (quoteState, idx) => {
if (quoteState.status == 'success') {
return quoteState;
}
// QuoteChunk is pending or failed, so we try again
const { inputs } = quoteState;
try {
totalCallsMade = totalCallsMade + 1;
const results = await this.consolidateResults(protocol, useMixedRouteQuoter, mixedRouteContainsV4Pool, functionName, inputs, providerConfig, gasLimitOverride);
const successRateError = this.validateSuccessRate(results.results, haveRetriedForSuccessRate, useMixedRouteQuoter, mixedRouteContainsV4Pool, protocol, optimisticCachedRoutes);
if (successRateError) {
return {
status: 'failed',
inputs,
reason: successRateError,
results,
};
}
return {
status: 'success',
inputs,
results,
};
}
catch (err) {
// Error from providers have huge messages that include all the calldata and fill the logs.
// Catch them and rethrow with shorter message.
if (err.message.includes('header not found')) {
return {
status: 'failed',
inputs,
reason: new ProviderBlockHeaderError(err.message.slice(0, 500)),
};
}
if (err.message.includes('timeout')) {
return {
status: 'failed',
inputs,
reason: new ProviderTimeoutError(`Req ${idx}/${quoteStates.length}. Request had ${inputs.length} inputs. ${err.message.slice(0, 500)}`),
};
}
if (err.message.includes('out of gas')) {
return {
status: 'failed',
inputs,
reason: new ProviderGasError(err.message.slice(0, 500)),
};
}
return {
status: 'failed',
inputs,
reason: new Error(`Unknown error from provider: ${err.message.slice(0, 500)}`),
};
}
}));
const [successfulQuoteStates, failedQuoteStates, pendingQuoteStates] = this.partitionQuotes(quoteStates);
if (pendingQuoteStates.length > 0) {
throw new Error('Pending quote after waiting for all promises.');
}
let retryAll = false;
const blockNumberError = this.validateBlockNumbers(successfulQuoteStates, inputsChunked.length, gasLimitOverride);
// If there is a block number conflict we retry all the quotes.
if (blockNumberError) {
retryAll = true;
}
const reasonForFailureStr = _.map(failedQuoteStates, (failedQuoteState) => failedQuoteState.reason.name).join(', ');
if (failedQuoteStates.length > 0) {
log.info(`On attempt ${attemptNumber}: ${failedQuoteStates.length}/${quoteStates.length} quotes failed. Reasons: ${reasonForFailureStr}`);
for (const failedQuoteState of failedQuoteStates) {
const { reason: error } = failedQuoteState;
log.info({ error }, `[QuoteFetchError] Attempt ${attemptNumber}. ${error.message}`);
if (error instanceof BlockConflictError) {
if (!haveRetriedForBlockConflictError) {
metric.putMetric(`${this.metricsPrefix(this.chainId, useMixedRouteQuoter, mixedRouteContainsV4Pool, protocol, optimisticCachedRoutes)}QuoteBlockConflictErrorRetry`, 1, MetricLoggerUnit.Count);
haveRetriedForBlockConflictError = true;
}
retryAll = true;
}
else if (error instanceof ProviderBlockHeaderError) {
if (!haveRetriedForBlockHeader) {
metric.putMetric(`${this.metricsPrefix(this.chainId, useMixedRouteQuoter, mixedRouteContainsV4Pool, protocol, optimisticCachedRoutes)}QuoteBlockHeaderNotFoundRetry`, 1, MetricLoggerUnit.Count);
haveRetriedForBlockHeader = true;
}
// Ensure that if multiple calls fail due to block header in the current pending batch,
// we only count once.
if (!haveIncrementedBlockHeaderFailureCounter) {
blockHeaderRetryAttemptNumber =
blockHeaderRetryAttemptNumber + 1;
haveIncrementedBlockHeaderFailureCounter = true;
}
if (rollback.enabled) {
const { rollbackBlockOffset, attemptsBeforeRollback } = rollback;
if (blockHeaderRetryAttemptNumber >= attemptsBeforeRollback &&
!blockHeaderRolledBack) {
log.info(`Attempt ${attemptNumber}. Have failed due to block header ${blockHeaderRetryAttemptNumber - 1} times. Rolling back block number by ${rollbackBlockOffset} for next retry`);
providerConfig.blockNumber = providerConfig.blockNumber
? (await providerConfig.blockNumber) + rollbackBlockOffset
: (await this.provider.getBlockNumber()) +
rollbackBlockOffset;
retryAll = true;
blockHeaderRolledBack = true;
}
}
}
else if (error instanceof ProviderTimeoutError) {
if (!haveRetriedForTimeout) {
metric.putMetric(`${this.metricsPrefix(this.chainId, useMixedRouteQuoter, mixedRouteContainsV4Pool, protocol, optimisticCachedRoutes)}QuoteTimeoutRetry`, 1, MetricLoggerUnit.Count);
haveRetriedForTimeout = true;
}
}
else if (error instanceof ProviderGasError) {
if (!haveRetriedForOutOfGas) {
metric.putMetric(`${this.metricsPrefix(this.chainId, useMixedRouteQuoter, mixedRouteContainsV4Pool, protocol, optimisticCachedRoutes)}QuoteOutOfGasExceptionRetry`, 1, MetricLoggerUnit.Count);
haveRetriedForOutOfGas = true;
}
gasLimitOverride =
this.gasErrorFailureOverride(protocol).gasLimitOverride;
multicallChunk =
this.gasErrorFailureOverride(protocol).multicallChunk;
retryAll = true;
}
else if (error instanceof SuccessRateError) {
if (!haveRetriedForSuccessRate) {
metric.putMetric(`${this.metricsPrefix(this.chainId, useMixedRouteQuoter, mixedRouteContainsV4Pool, protocol, optimisticCachedRoutes)}QuoteSuccessRateRetry`, 1, MetricLoggerUnit.Count);
haveRetriedForSuccessRate = true;
// Low success rate can indicate too little gas given to each call.
gasLimitOverride =
this.successRateFailureOverrides(protocol).gasLimitOverride;
multicallChunk =
this.successRateFailureOverrides(protocol).multicallChunk;
retryAll = true;
}
}
else {
if (!haveRetriedForUnknownReason) {
metric.putMetric(`${this.metricsPrefix(this.chainId, useMixedRouteQuoter, mixedRouteContainsV4Pool, protocol, optimisticCachedRoutes)}QuoteUnknownReasonRetry`, 1, MetricLoggerUnit.Count);
haveRetriedForUnknownReason = true;
}
}
}
}
if (retryAll) {
log.info(`Attempt ${attemptNumber}. Resetting all requests to pending for next attempt.`);
const normalizedChunk = Math.ceil(inputs.length / Math.ceil(inputs.length / multicallChunk));
const inputsChunked = _.chunk(inputs, normalizedChunk);
quoteStates = _.map(inputsChunked, (inputChunk) => {
return {
status: 'pending',
inputs: inputChunk,
};
});
}
if (failedQuoteStates.length > 0) {
// TODO: Work with Arbitrum to find a solution for making large multicalls with gas limits that always
// successfully.
//
// On Arbitrum we can not set a gas limit for every call in the multicall and guarantee that
// we will not run out of gas on the node. This is because they have a different way of accounting
// for gas, that seperates storage and compute gas costs, and we can not cover both in a single limit.
//
// To work around this and avoid throwing errors when really we just couldn't get a quote, we catch this
// case and return 0 quotes found.
if ((this.chainId == ChainId.ARBITRUM_ONE ||
this.chainId == ChainId.ARBITRUM_GOERLI) &&
_.every(failedQuoteStates, (failedQuoteState) => failedQuoteState.reason instanceof ProviderGasError) &&
attemptNumber == this.retryOptions.retries) {
log.error(`Failed to get quotes on Arbitrum due to provider gas error issue. Overriding error to return 0 quotes.`);
return {
results: [],
blockNumber: BigNumber.from(0),
approxGasUsedPerSuccessCall: 0,
};
}
throw new Error(`Failed to get ${failedQuoteStates.length} quotes. Reasons: ${reasonForFailureStr}`);
}
const callResults = _.map(successfulQuoteStates, (quoteState) => quoteState.results);
return {
results: _.flatMap(callResults, (result) => result.results),
blockNumber: BigNumber.from(callResults[0].blockNumber),
approxGasUsedPerSuccessCall: stats.percentile(_.map(callResults, (result) => result.approxGasUsedPerSuccessCall), 100),
};
}, {
retries: DEFAULT_BATCH_RETRIES,
...this.retryOptions,
});
const routesQuotes = this.processQuoteResults(quoteResults, routes, amounts, BigNumber.from(gasLimitOverride));
const endTime = Date.now();
metric.putMetric(`${this.metricsPrefix(this.chainId, useMixedRouteQuoter, mixedRouteContainsV4Pool, protocol, optimisticCachedRoutes)}QuoteLatency`, endTime - startTime, MetricLoggerUnit.Milliseconds);
metric.putMetric(`${this.metricsPrefix(this.chainId, useMixedRouteQuoter, mixedRouteContainsV4Pool, protocol, optimisticCachedRoutes)}QuoteApproxGasUsedPerSuccessfulCall`, approxGasUsedPerSuccessCall, MetricLoggerUnit.Count);
metric.putMetric(`${this.metricsPrefix(this.chainId, useMixedRouteQuoter, mixedRouteContainsV4Pool, protocol, optimisticCachedRoutes)}QuoteNumRetryLoops`, finalAttemptNumber - 1, MetricLoggerUnit.Count);
metric.putMetric(`${this.metricsPrefix(this.chainId, useMixedRouteQuoter, mixedRouteContainsV4Pool, protocol, optimisticCachedRoutes)}QuoteTotalCallsToProvider`, totalCallsMade, MetricLoggerUnit.Count);
metric.putMetric(`${this.metricsPrefix(this.chainId, useMixedRouteQuoter, mixedRouteContainsV4Pool, protocol, optimisticCachedRoutes)}QuoteExpectedCallsToProvider`, expectedCallsMade, MetricLoggerUnit.Count);
metric.putMetric(`${this.metricsPrefix(this.chainId, useMixedRouteQuoter, mixedRouteContainsV4Pool, protocol, optimisticCachedRoutes)}QuoteNumRetriedCalls`, totalCallsMade - expectedCallsMade, MetricLoggerUnit.Count);
const [successfulQuotes, failedQuotes] = _(routesQuotes)
.flatMap((routeWithQuotes) => routeWithQuotes[1])
.partition((quote) => quote.quote != null)
.value();
log.info(`Got ${successfulQuotes.length} successful quotes, ${failedQuotes.length} failed quotes. Took ${finalAttemptNumber - 1} attempt loops. Total calls made to provider: ${totalCallsMade}. Have retried for timeout: ${haveRetriedForTimeout}`);
// Log total routes
metric.putMetric(`${this.metricsPrefix(this.chainId, useMixedRouteQuoter, mixedRouteContainsV4Pool, protocol, optimisticCachedRoutes)}RoutesLength`, routesQuotes.length, MetricLoggerUnit.Count);
// Log total quotes
metric.putMetric(`${this.metricsPrefix(this.chainId, useMixedRouteQuoter, mixedRouteContainsV4Pool, protocol, optimisticCachedRoutes)}RoutesQuotesLength`, successfulQuotes.length + failedQuotes.length, MetricLoggerUnit.Count);
// log successful quotes
metric.putMetric(`${this.metricsPrefix(this.chainId, useMixedRouteQuoter, mixedRouteContainsV4Pool, protocol, optimisticCachedRoutes)}RoutesSuccessfulQuotesLength`, successfulQuotes.length, MetricLoggerUnit.Count);
// log failed quotes
metric.putMetric(`${this.metricsPrefix(this.chainId, useMixedRouteQuoter, mixedRouteContainsV4Pool, protocol, optimisticCachedRoutes)}RoutesFailedQuotesLength`, failedQuotes.length, MetricLoggerUnit.Count);
return {
routesWithQuotes: routesQuotes,
blockNumber,
};
}
partitionQuotes(quoteStates) {
const successfulQuoteStates = _.filter(quoteStates, (quoteState) => quoteState.status == 'success');
const failedQuoteStates = _.filter(quoteStates, (quoteState) => quoteState.status == 'failed');
const pendingQuoteStates = _.filter(quoteStates, (quoteState) => quoteState.status == 'pending');
return [successfulQuoteStates, failedQuoteStates, pendingQuoteStates];
}
processQuoteResults(quoteResults, routes, amounts, gasLimit) {
const routesQuotes = [];
const quotesResultsByRoute = _.chunk(quoteResults, amounts.length);
const debugFailedQuotes = [];
for (let i = 0; i < quotesResultsByRoute.length; i++) {
const route = routes[i];
const quoteResults = quotesResultsByRoute[i];
const quotes = _.map(quoteResults, (quoteResult, index) => {
var _a;
const amount = amounts[index];
if (!quoteResult.success) {
const percent = (100 / amounts.length) * (index + 1);
const amountStr = amount.toFixed(Math.min(amount.currency.decimals, 2));
const routeStr = routeToString(route);
debugFailedQuotes.push({
route: routeStr,
percent,
amount: amountStr,
});
return {
amount,
quote: null,
sqrtPriceX96AfterList: null,
gasEstimate: (_a = quoteResult.gasUsed) !== null && _a !== void 0 ? _a : null,
gasLimit: gasLimit,
initializedTicksCrossedList: null,
};
}
return {
amount,
quote: quoteResult.result[0],
sqrtPriceX96AfterList: quoteResult.result[1],
initializedTicksCrossedList: quoteResult.result[2],
gasEstimate: quoteResult.result[3],
gasLimit: gasLimit,
};
});
routesQuotes.push([route, quotes]);
}
// For routes and amounts that we failed to get a quote for, group them by route
// and batch them together before logging to minimize number of logs.
const debugChunk = 80;
_.forEach(_.chunk(debugFailedQuotes, debugChunk), (quotes, idx) => {
const failedQuotesByRoute = _.groupBy(quotes, (q) => q.route);
const failedFlat = _.mapValues(failedQuotesByRoute, (f) => _(f)
.map((f) => `${f.percent}%[${f.amount}]`)
.join(','));
log.info({
failedQuotes: _.map(failedFlat, (amounts, routeStr) => `${routeStr} : ${amounts}`),
}, `Failed on chain quotes for routes Part ${idx}/${Math.ceil(debugFailedQuotes.length / debugChunk)}`);
});
return routesQuotes;
}
validateBlockNumbers(successfulQuoteStates, totalCalls, gasLimitOverride) {
if (successfulQuoteStates.length <= 1) {
return null;
}
const results = _.map(successfulQuoteStates, (quoteState) => quoteState.results);
const blockNumbers = _.map(results, (result) => result.blockNumber);
const uniqBlocks = _(blockNumbers)
.map((blockNumber) => blockNumber.toNumber())
.uniq()
.value();
if (uniqBlocks.length == 1) {
return null;
}
/* if (
uniqBlocks.length == 2 &&
Math.abs(uniqBlocks[0]! - uniqBlocks[1]!) <= 1
) {
return null;
} */
return new BlockConflictError(`Quotes returned from different blocks. ${uniqBlocks}. ${totalCalls} calls were made with gas limit ${gasLimitOverride}`);
}
validateSuccessRate(allResults, haveRetriedForSuccessRate, useMixedRouteQuoter, mixedRouteContainsV4Pool, protocol, optimisticCachedRoutes) {
const numResults = allResults.length;
const numSuccessResults = allResults.filter((result) => result.success).length;
const successRate = (1.0 * numSuccessResults) / numResults;
const { quoteMinSuccessRate } = this.batchParams(optimisticCachedRoutes, protocol);
if (successRate < quoteMinSuccessRate) {
if (haveRetriedForSuccessRate) {
log.info(`Quote success rate still below threshold despite retry. Continuing. ${quoteMinSuccessRate}: ${successRate}`);
metric.putMetric(`${this.metricsPrefix(this.chainId, useMixedRouteQuoter, mixedRouteContainsV4Pool, protocol, optimisticCachedRoutes)}QuoteRetriedSuccessRateLow`, successRate, MetricLoggerUnit.Percent);
return;
}
metric.putMetric(`${this.metricsPrefix(this.chainId, useMixedRouteQuoter, mixedRouteContainsV4Pool, protocol, optimisticCachedRoutes)}QuoteSuccessRateLow`, successRate, MetricLoggerUnit.Percent);
return new SuccessRateError(`Quote success rate below threshold of ${quoteMinSuccessRate}: ${successRate}`);
}
}
/**
* Throw an error for incorrect routes / function combinations
* @param routes Any combination of V3, V2, and Mixed routes.
* @param functionName
* @param useMixedRouteQuoter true if there are ANY V2Routes or MixedRoutes in the routes parameter
*/
validateRoutes(routes, functionName, useMixedRouteQuoter) {
/// We do not send any V3Routes to new qutoer becuase it is not deployed on chains besides mainnet
if (routes.some((route) => route.protocol === Protocol.V3) &&
useMixedRouteQuoter) {
throw new Error(`Cannot use mixed route quoter with V3 routes`);
}
/// We cannot call quoteExactOutput with V2 or Mixed routes
if (functionName === 'quoteExactOutput' && useMixedRouteQuoter) {
throw new Error('Cannot call quoteExactOutput with V2 or Mixed routes');
}
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib24tY2hhaW4tcXVvdGUtcHJvdmlkZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvcHJvdmlkZXJzL29uLWNoYWluLXF1b3RlLXByb3ZpZGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUNBLE9BQU8sRUFBRSxTQUFTLEVBQWdCLE1BQU0sMEJBQTBCLENBQUM7QUFHbkUsT0FBTyxFQUNMLHNCQUFzQixFQUN0QixhQUFhLEVBQ2IsUUFBUSxHQUNULE1BQU0scUJBQXFCLENBQUM7QUFDN0IsT0FBTyxFQUFFLE9BQU8sRUFBRSxNQUFNLG1CQUFtQixDQUFDO0FBQzVDLE9BQU8sRUFBRSxpQkFBaUIsSUFBSSxtQkFBbUIsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBQzNFLE9BQU8sRUFDTCxpQkFBaUIsSUFBSSxtQkFBbUIsRUFDeEMsSUFBSSxJQUFJLE1BQU0sR0FDZixNQUFNLGlCQUFpQixDQUFDO0FBQ3pCLE9BQU8sS0FBa0MsTUFBTSxhQUFhLENBQUM7QUFDN0QsT0FBTyxDQUFDLE1BQU0sUUFBUSxDQUFDO0FBQ3ZCLE9BQU8sS0FBSyxNQUFNLFlBQVksQ0FBQztBQUUvQixPQUFPLEVBR0wsT0FBTyxHQUdSLE1BQU0sbUJBQW1CLENBQUM7QUFDM0IsT0FBTyxFQUFFLDRCQUE0QixFQUFFLE1BQU0sdURBQXVELENBQUM7QUFDckcsT0FBTyxFQUFFLDJCQUEyQixFQUFFLE1BQU0sc0RBQXNELENBQUM7QUFDbkcsT0FBTyxFQUFFLGlCQUFpQixFQUFFLE1BQU0sNENBQTRDLENBQUM7QUFDL0UsT0FBTyxFQUFFLGtCQUFrQixFQUFFLE1BQU0sMENBQTBDLENBQUM7QUFDOUUsT0FBTyxFQUNMLFVBQVUsRUFDVixrQkFBa0IsRUFDbEIsTUFBTSxFQUNOLGdCQUFnQixFQUNoQiwrQkFBK0IsRUFDL0IsK0JBQStCLEVBQy9CLHVCQUF1QixFQUN2Qiw0QkFBNEIsR0FDN0IsTUFBTSxTQUFTLENBQUM7QUFFakIsT0FBTyxFQUFF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