UNPKG

@maxosllc/smart-order-router

Version:
681 lines 71.6 kB
import { BigNumber } from '@ethersproject/bignumber'; import { encodeMixedRouteToPath, MixedRouteSDK, Protocol, } from '@uniswap/router-sdk'; import { ChainId } from '../../src/util/chains'; 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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib24tY2hhaW4tcXVvdGUtcHJvdmlkZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvcHJvdmlkZXJzL29uLWNoYWluLXF1b3RlLXByb3ZpZGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUNBLE9BQU8sRUFBRSxTQUFTLEVBQWdCLE1BQU0sMEJBQTBCLENBQUM7QUFHbkUsT0FBTyxFQUNMLHNCQUFzQixFQUN0QixhQUFhLEVBQ2IsUUFBUSxHQUNULE1BQU0scUJBQXFCLENBQUM7QUFDN0IsT0FBTyxFQUFFLE9BQU8sRUFBRSxNQUFNLHVCQUF1QixDQUFDO0FBQ2hELE9BQU8sRUFBRSxpQkFBaUIsSUFBSSxtQkFBbUIsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBQzNFLE9BQU8sRUFDTCxpQkFBaUIsSUFBSSxtQkFBbUIsRUFDeEMsSUFBSSxJQUFJLE1BQU0sR0FDZixNQUFNLGlCQUFpQixDQUFDO0FBQ3pCLE9BQU8sS0FBa0MsTUFBTSxhQUFhLENBQUM7QUFDN0QsT0FBTyxDQUFDLE1BQU0sUUFBUSxDQUFDO0FBQ3ZCLE9BQU8sS0FBSyxNQUFNLFlBQVksQ0FBQztBQUUvQixPQUFPLEVBR0wsT0FBTyxHQUdSLE1BQU0sbUJBQW1CLENBQUM7QUFDM0IsT0FBTyxFQUFFLDRCQUE0QixFQUFFLE1BQU0sdURBQXVELENBQUM7QUFDckcsT0FBTyxFQUFFLDJCQUEyQixFQUFFLE1BQU0sc0RBQXNELENBQUM7QUFDbkcsT0FBTyxFQUFFLGlCQUFpQixFQUFFLE1BQU0sNENBQTRDLENBQUM7QUFDL0UsT0FBTyxFQUFFLGtCQUFrQixFQUFFLE1BQU0sMENBQTBDLENBQUM7QUFDOUUsT0FBTyxFQUNMLFVBQVUsRUFDVixrQkFBa0IsRUFDbEIsTUFBTSxFQUNOLGdCQUFnQixFQUNoQiwrQkFBK0IsRUFDL0IsK0JBQStCLEVBQy9CLHVCQUF1QixFQUN2Qiw0QkFBNEIsR0FDN0IsTUFBTSxTQUFTLENBQUM7QUFFakIsT0FBTyxFQUFF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