diff --git a/package.json b/package.json index fd3e58c64..8753ecc5a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@uniswap/smart-order-router", - "version": "4.17.14", + "version": "4.18.0", "description": "Uniswap Smart Order Router", "main": "build/main/index.js", "typings": "build/main/index.d.ts", diff --git a/src/routers/alpha-router/alpha-router.ts b/src/routers/alpha-router/alpha-router.ts index c0b63c465..fd7a24781 100644 --- a/src/routers/alpha-router/alpha-router.ts +++ b/src/routers/alpha-router/alpha-router.ts @@ -1432,10 +1432,46 @@ export class AlphaRouter // Fetch CachedRoutes let cachedRoutes: CachedRoutes | undefined; + // Decide whether to use cached routes or not - If |enabledAndRequestedProtocolsMatch| is true we are good to use cached routes. + // In order to use cached routes, we need to have all enabled protocols specified in the request. + // By default, all protocols are enabled but for UniversalRouterVersion.V1_2, V4 is not. + // - ref: https://github.com/Uniswap/routing-api/blob/663b607d80d9249f85e7ab0925a611ec3701da2a/lib/util/supportedProtocolVersions.ts#L15 + // So we take this into account when deciding whether to use cached routes or not. + // We only want to use cache if all enabled protocols are specified (V2,V3,V4? + MIXED). In any other case, use onchain path. + // - Cache is optimized for global search, not for specific protocol(s) search. + // For legacy systems (SWAP_ROUTER_02) or missing swapConfig, follow UniversalRouterVersion.V1_2 logic. + const availableProtocolsSet = new Set(Object.values(Protocol)); + const requestedProtocolsSet = new Set(protocols); + if ( + !swapConfig || + swapConfig.type === SwapType.SWAP_ROUTER_02 || + (swapConfig.type === SwapType.UNIVERSAL_ROUTER && + swapConfig.version === UniversalRouterVersion.V1_2) + ) { + availableProtocolsSet.delete(Protocol.V4); + if (requestedProtocolsSet.has(Protocol.V4)) { + requestedProtocolsSet.delete(Protocol.V4); + } + } + const enabledAndRequestedProtocolsMatch = + availableProtocolsSet.size === requestedProtocolsSet.size && + [...availableProtocolsSet].every((protocol) => + requestedProtocolsSet.has(protocol) + ); + + log.debug('UniversalRouterVersion_CacheGate_Check', { + availableProtocolsSet: Array.from(availableProtocolsSet), + requestedProtocolsSet: Array.from(requestedProtocolsSet), + enabledAndRequestedProtocolsMatch, + swapConfigType: swapConfig?.type, + swapConfigUniversalRouterVersion: + swapConfig?.type === SwapType.UNIVERSAL_ROUTER + ? swapConfig?.version + : 'N/A', + }); + if (routingConfig.useCachedRoutes && cacheMode !== CacheMode.Darkmode) { - // Only use cache if 0 or more than 1 protocol is specified. - // - Cache is optimized for global search, not for specific protocol search - if (protocols.length != 1) { + if (enabledAndRequestedProtocolsMatch) { if ( protocols.includes(Protocol.V4) && (currencyIn.isNative || currencyOut.isNative) @@ -2319,10 +2355,7 @@ export class AlphaRouter Promise.resolve(undefined); // we are explicitly requiring people to specify v4 for now - if ( - (v4SupportedInChain && (v4ProtocolSpecified || noProtocolsSpecified)) || - (shouldQueryMixedProtocol && mixedProtocolAllowed) - ) { + if (v4SupportedInChain && (v4ProtocolSpecified || noProtocolsSpecified)) { // if (v4ProtocolSpecified || noProtocolsSpecified) { v4CandidatePoolsPromise = getV4CandidatePools({ currencyIn: currencyIn, @@ -2348,11 +2381,7 @@ export class AlphaRouter let v3CandidatePoolsPromise: Promise = Promise.resolve(undefined); if (!fotInDirectSwap) { - if ( - v3ProtocolSpecified || - noProtocolsSpecified || - (shouldQueryMixedProtocol && mixedProtocolAllowed) - ) { + if (v3ProtocolSpecified || noProtocolsSpecified) { const tokenIn = currencyIn.wrapped; const tokenOut = currencyOut.wrapped; @@ -2379,10 +2408,7 @@ export class AlphaRouter let v2CandidatePoolsPromise: Promise = Promise.resolve(undefined); - if ( - (v2SupportedInChain && (v2ProtocolSpecified || noProtocolsSpecified)) || - (shouldQueryMixedProtocol && mixedProtocolAllowed) - ) { + if (v2SupportedInChain && (v2ProtocolSpecified || noProtocolsSpecified)) { const tokenIn = currencyIn.wrapped; const tokenOut = currencyOut.wrapped; @@ -2539,7 +2565,12 @@ export class AlphaRouter // 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) { + // Also make sure there are at least 2 protocols provided besides MIXED, before entering mixed quoter + if ( + shouldQueryMixedProtocol && + mixedProtocolAllowed && + protocols.filter((protocol) => protocol !== Protocol.MIXED).length >= 2 + ) { log.info({ protocols, tradeType }, 'Routing across MixedRoutes'); metric.putMetric( @@ -2580,9 +2611,9 @@ export class AlphaRouter percents, quoteCurrency.wrapped, [ - v4CandidatePools!, - v3CandidatePools!, - v2CandidatePools!, + v4CandidatePools, + v3CandidatePools, + v2CandidatePools, crossLiquidityPools, ], tradeType, diff --git a/src/routers/alpha-router/functions/get-candidate-pools.ts b/src/routers/alpha-router/functions/get-candidate-pools.ts index 74ed9f5cf..05b663f84 100644 --- a/src/routers/alpha-router/functions/get-candidate-pools.ts +++ b/src/routers/alpha-router/functions/get-candidate-pools.ts @@ -168,9 +168,9 @@ export type V2GetCandidatePoolsParams = { }; export type MixedRouteGetCandidatePoolsParams = { - v4CandidatePools: V4CandidatePools; - v3CandidatePools: V3CandidatePools; - v2CandidatePools: V2CandidatePools; + v4CandidatePools: V4CandidatePools | undefined; + v3CandidatePools: V3CandidatePools | undefined; + v2CandidatePools: V2CandidatePools | undefined; crossLiquidityPools: CrossLiquidityCandidatePools; routingConfig: AlphaRouterConfig; tokenProvider: ITokenProvider; @@ -1929,11 +1929,66 @@ export async function getMixedRouteCandidatePools({ chainId, }: MixedRouteGetCandidatePoolsParams): Promise { const beforeSubgraphPools = Date.now(); - const [ - { subgraphPools: V4subgraphPools, candidatePools: V4candidatePools }, - { subgraphPools: V3subgraphPools, candidatePools: V3candidatePools }, - { subgraphPools: V2subgraphPools, candidatePools: V2candidatePools }, - ] = [v4CandidatePools, v3CandidatePools, v2CandidatePools]; + const [v4Results, v3Results, v2Results] = [ + v4CandidatePools, + v3CandidatePools, + v2CandidatePools, + ]; + + // Create empty defaults for undefined results + const { + subgraphPools: V4subgraphPools = [], + candidatePools: V4candidatePools = { + protocol: Protocol.V4, + selections: { + topByBaseWithTokenIn: [], + topByBaseWithTokenOut: [], + topByDirectSwapPool: [], + topByEthQuoteTokenPool: [], + topByTVL: [], + topByTVLUsingTokenIn: [], + topByTVLUsingTokenOut: [], + topByTVLUsingTokenInSecondHops: [], + topByTVLUsingTokenOutSecondHops: [], + }, + }, + } = v4Results || {}; + + const { + subgraphPools: V3subgraphPools = [], + candidatePools: V3candidatePools = { + protocol: Protocol.V3, + selections: { + topByBaseWithTokenIn: [], + topByBaseWithTokenOut: [], + topByDirectSwapPool: [], + topByEthQuoteTokenPool: [], + topByTVL: [], + topByTVLUsingTokenIn: [], + topByTVLUsingTokenOut: [], + topByTVLUsingTokenInSecondHops: [], + topByTVLUsingTokenOutSecondHops: [], + }, + }, + } = v3Results || {}; + + const { + subgraphPools: V2subgraphPools = [], + candidatePools: V2candidatePools = { + protocol: Protocol.V2, + selections: { + topByBaseWithTokenIn: [], + topByBaseWithTokenOut: [], + topByDirectSwapPool: [], + topByEthQuoteTokenPool: [], + topByTVL: [], + topByTVLUsingTokenIn: [], + topByTVLUsingTokenOut: [], + topByTVLUsingTokenInSecondHops: [], + topByTVLUsingTokenOutSecondHops: [], + }, + }, + } = v2Results || {}; // Injects the liquidity pools found by the getMixedCrossLiquidityCandidatePools function V2subgraphPools.push(...crossLiquidityPools.v2Pools); diff --git a/src/routers/alpha-router/quoters/base-quoter.ts b/src/routers/alpha-router/quoters/base-quoter.ts index 022b758d0..c836809f0 100644 --- a/src/routers/alpha-router/quoters/base-quoter.ts +++ b/src/routers/alpha-router/quoters/base-quoter.ts @@ -44,9 +44,9 @@ export abstract class BaseQuoter< CandidatePools extends | SupportedCandidatePools | [ - V4CandidatePools, - V3CandidatePools, - V2CandidatePools, + V4CandidatePools | undefined, + V3CandidatePools | undefined, + V2CandidatePools | undefined, CrossLiquidityCandidatePools ], Route extends SupportedRoutes, diff --git a/src/routers/alpha-router/quoters/mixed-quoter.ts b/src/routers/alpha-router/quoters/mixed-quoter.ts index 15beab4ae..b6876ca28 100644 --- a/src/routers/alpha-router/quoters/mixed-quoter.ts +++ b/src/routers/alpha-router/quoters/mixed-quoter.ts @@ -41,9 +41,9 @@ import { GetQuotesResult, GetRoutesResult } from './model'; export class MixedQuoter extends BaseQuoter< [ - V4CandidatePools, - V3CandidatePools, - V2CandidatePools, + V4CandidatePools | undefined, + V3CandidatePools | undefined, + V2CandidatePools | undefined, CrossLiquidityCandidatePools ], MixedRoute, @@ -90,9 +90,9 @@ export class MixedQuoter extends BaseQuoter< currencyIn: Currency, currencyOut: Currency, v4v3v2candidatePools: [ - V4CandidatePools, - V3CandidatePools, - V2CandidatePools, + V4CandidatePools | undefined, + V3CandidatePools | undefined, + V2CandidatePools | undefined, CrossLiquidityCandidatePools ], tradeType: TradeType, diff --git a/test/integ/routers/alpha-router/alpha-router.integration.test.ts b/test/integ/routers/alpha-router/alpha-router.integration.test.ts index 1819f3d95..ad024d5ef 100644 --- a/test/integ/routers/alpha-router/alpha-router.integration.test.ts +++ b/test/integ/routers/alpha-router/alpha-router.integration.test.ts @@ -3324,7 +3324,7 @@ describe('alpha router integration', () => { }, { ...ROUTING_CONFIG, - protocols: [Protocol.MIXED], + protocols: [Protocol.V3, Protocol.V2, Protocol.MIXED], } ); expect(swap).toBeDefined(); diff --git a/test/unit/routers/alpha-router/alpha-router.test.ts b/test/unit/routers/alpha-router/alpha-router.test.ts index e35741d91..83b95fc4f 100644 --- a/test/unit/routers/alpha-router/alpha-router.test.ts +++ b/test/unit/routers/alpha-router/alpha-router.test.ts @@ -1,13 +1,8 @@ import { BigNumber } from '@ethersproject/bignumber'; import { BaseProvider } from '@ethersproject/providers'; import { Protocol, SwapRouter } from '@uniswap/router-sdk'; -import { - ChainId, - Ether, - Fraction, - Percent, - TradeType -} from '@uniswap/sdk-core'; +import { ChainId, Fraction, Percent, TradeType } from '@uniswap/sdk-core'; +import { UniversalRouterVersion } from '@uniswap/universal-router-sdk'; import { Pair } from '@uniswap/v2-sdk'; import { encodeSqrtRatioX96, Pool as V3Pool, Position } from '@uniswap/v3-sdk'; import { Pool as V4Pool } from '@uniswap/v4-sdk'; @@ -74,6 +69,9 @@ import { import { V2HeuristicGasModelFactory } from '../../../../src/routers/alpha-router/gas-models/v2/v2-heuristic-gas-model'; +import { + V4HeuristicGasModelFactory +} from '../../../../src/routers/alpha-router/gas-models/v4/v4-heuristic-gas-model'; import { buildMockTokenAccessor, buildMockV2PoolAccessor, @@ -84,14 +82,16 @@ import { DAI_USDT, DAI_USDT_LOW, DAI_USDT_MEDIUM, - DAI_USDT_V4_LOW, ETH_USDT_V4_LOW, + DAI_USDT_V4_LOW, + ETH_USDT_V4_LOW, MOCK_ZERO_DEC_TOKEN, mockBlock, mockBlockBN, mockGasPriceWeiBN, pairToV2SubgraphPool, poolToV3SubgraphPool, - poolToV4SubgraphPool, UNI_ETH_V4_MEDIUM, + poolToV4SubgraphPool, + UNI_ETH_V4_MEDIUM, USDC_DAI, USDC_DAI_LOW, USDC_DAI_MEDIUM, @@ -111,10 +111,6 @@ import { import { InMemoryRouteCachingProvider } from '../../providers/caching/route/test-util/inmemory-route-caching-provider'; -import { UniversalRouterVersion } from '@uniswap/universal-router-sdk'; -import { - V4HeuristicGasModelFactory -} from '../../../../src/routers/alpha-router/gas-models/v4/v4-heuristic-gas-model'; const helper = require('../../../../src/routers/alpha-router/functions/calculate-ratio-amount-in'); @@ -151,6 +147,8 @@ describe('alpha router', () => { let alphaRouter: AlphaRouter; + const allProtocols = [Protocol.V2, Protocol.V3, Protocol.V4, Protocol.MIXED]; + const ROUTING_CONFIG: AlphaRouterConfig = { v4PoolSelection: { topN: 0, @@ -516,188 +514,6 @@ describe('alpha router', () => { }); describe('exact in', () => { - test('succeeds to route across all protocols when no protocols specified', async () => { - // Mock the quote providers so that for each protocol, one route and one - // amount less than 100% of the input gives a huge quote. - // Ensures a split route. - mockV2QuoteProvider.getQuotesManyExactIn.callsFake( - async (amountIns: CurrencyAmount[], routes: V2Route[]) => { - const routesWithQuotes = _.map(routes, (r, routeIdx) => { - const amountQuotes = _.map(amountIns, (amountIn, idx) => { - const quote = - idx == 1 && routeIdx == 1 - ? BigNumber.from(amountIn.quotient.toString()).mul(10) - : BigNumber.from(amountIn.quotient.toString()); - return { - amount: amountIn, - quote, - } as V2AmountQuote; - }); - return [r, amountQuotes]; - }); - - return { - routesWithQuotes: routesWithQuotes, - } as { routesWithQuotes: V2RouteWithQuotes[] }; - } - ); - - mockOnChainQuoteProvider.getQuotesManyExactIn.callsFake( - async ( - amountIns: CurrencyAmount[], - routes: SupportedRoutes[], - _providerConfig?: ProviderConfig - ) => { - const routesWithQuotes = _.map(routes, (r, routeIdx) => { - const amountQuotes = _.map(amountIns, (amountIn, idx) => { - const quote = - idx == 1 && routeIdx == 1 - ? BigNumber.from(amountIn.quotient.toString()).mul(10) - : BigNumber.from(amountIn.quotient.toString()); - return { - amount: amountIn, - quote, - sqrtPriceX96AfterList: [ - BigNumber.from(1), - BigNumber.from(1), - BigNumber.from(1), - ], - initializedTicksCrossedList: [1], - gasEstimate: BigNumber.from(10000), - } as AmountQuote; - }); - return [r, amountQuotes]; - }); - - return { - routesWithQuotes: routesWithQuotes, - blockNumber: mockBlockBN, - } as { - routesWithQuotes: RouteWithQuotes[]; - blockNumber: BigNumber; - }; - } - ); - - const amount = CurrencyAmount.fromRawAmount(USDC, 10000); - - const swap = await alphaRouter.route( - amount, - WRAPPED_NATIVE_CURRENCY[1], - TradeType.EXACT_INPUT, - undefined, - { ...ROUTING_CONFIG } - ); - expect(swap).toBeDefined(); - - expect(mockFallbackTenderlySimulator.simulate.called).toBeFalsy(); - expect(mockProvider.getBlockNumber.called).toBeTruthy(); - expect(mockGasPriceProvider.getGasPrice.called).toBeTruthy(); - expect( - mockV3GasModelFactory.buildGasModel.calledWith({ - chainId: 1, - gasPriceWei: mockGasPriceWeiBN, - pools: sinon.match.any, - amountToken: amount.currency, - quoteToken: WRAPPED_NATIVE_CURRENCY[1], - v2poolProvider: sinon.match.any, - l2GasDataProvider: undefined, - providerConfig: sinon.match({ - blockNumber: sinon.match.instanceOf(Promise) - }) - }) - ).toBeTruthy(); - expect( - mockV2GasModelFactory.buildGasModel.calledWith({ - chainId: 1, - gasPriceWei: mockGasPriceWeiBN, - poolProvider: sinon.match.any, - token: WRAPPED_NATIVE_CURRENCY[1], - l2GasDataProvider: sinon.match.any, - providerConfig: sinon.match.any, - }) - ).toBeTruthy(); - expect( - mockMixedRouteGasModelFactory.buildGasModel.calledWith({ - chainId: 1, - gasPriceWei: mockGasPriceWeiBN, - pools: sinon.match.any, /// v3 pool provider - v2poolProvider: sinon.match.any, - amountToken: amount.currency, - quoteToken: WRAPPED_NATIVE_CURRENCY[1], - providerConfig: sinon.match.any - }) - ).toBeTruthy(); - - sinon.assert.calledWith( - mockOnChainQuoteProvider.getQuotesManyExactIn, - sinon.match((value) => { - return value instanceof Array && value.length == 4; - }), - sinon.match.array, - sinon.match({ blockNumber: sinon.match.defined }) - ); - /// V3, then mixedRoutes - sinon.assert.callCount(mockOnChainQuoteProvider.getQuotesManyExactIn, 2); - sinon.assert.calledWith( - mockV2QuoteProvider.getQuotesManyExactIn, - sinon.match((value) => { - return value instanceof Array && value.length == 4; - }), - sinon.match.array - ); - sinon.assert.notCalled(mockOnChainQuoteProvider.getQuotesManyExactOut); - - for (const r of swap!.route) { - expect(r.route.input.equals(USDC)).toBeTruthy(); - expect( - r.route.output.equals(WRAPPED_NATIVE_CURRENCY[1].wrapped) - ).toBeTruthy(); - } - - expect( - swap!.quote.currency.equals(WRAPPED_NATIVE_CURRENCY[1]) - ).toBeTruthy(); - expect( - swap!.quoteGasAdjusted.currency.equals(WRAPPED_NATIVE_CURRENCY[1]) - ).toBeTruthy(); - expect(swap!.quote.greaterThan(swap!.quoteGasAdjusted)).toBeTruthy(); - expect(swap!.estimatedGasUsed.toString()).toEqual('20000'); - expect( - swap!.estimatedGasUsedQuoteToken.currency.equals( - WRAPPED_NATIVE_CURRENCY[1] - ) - ).toBeTruthy(); - expect( - swap!.estimatedGasUsedUSD.currency.equals(USDC) || - swap!.estimatedGasUsedUSD.currency.equals(USDT) || - swap!.estimatedGasUsedUSD.currency.equals(DAI) - ).toBeTruthy(); - expect(swap!.gasPriceWei.toString()).toEqual( - mockGasPriceWeiBN.toString() - ); - expect(swap!.route).toHaveLength(2); - - expect( - _.filter(swap!.route, (r) => r.protocol == Protocol.V3) - ).toHaveLength(1); - expect( - _.filter(swap!.route, (r) => r.protocol == Protocol.V2) - ).toHaveLength(1); - - expect( - _(swap!.route) - .map((r) => r.percent) - .sum() - ).toEqual(100); - - expect(sumFn(_.map(swap!.route, (r) => r.amount)).equalTo(amount)); - - expect(swap!.trade).toBeDefined(); - expect(swap!.methodParameters).not.toBeDefined(); - expect(swap!.blockNumber.toString()).toEqual(mockBlockBN.toString()); - }); - test('find a favorable mixedRoute while routing across V2,V3,Mixed protocols', async () => { mockV2QuoteProvider.getQuotesManyExactIn.callsFake( async (amountIns: CurrencyAmount[], routes: V2Route[]) => { @@ -1112,60 +928,6 @@ describe('alpha router', () => { expect(swapTo).toBeDefined(); }); - test('succeeds to route on mixed only', async () => { - const amount = CurrencyAmount.fromRawAmount(USDC, 10000); - const swap = await alphaRouter.route( - amount, - Ether.onChain(ChainId.MAINNET), - TradeType.EXACT_INPUT, - undefined, - { ...ROUTING_CONFIG, protocols: [Protocol.MIXED] } - ) - expect(swap).toBeDefined(); - - expect(mockFallbackTenderlySimulator.simulate.called).toBeFalsy(); - expect(mockProvider.getBlockNumber.called).toBeTruthy(); - expect(mockGasPriceProvider.getGasPrice.called).toBeTruthy(); - - sinon.assert.calledWith( - mockOnChainQuoteProvider.getQuotesManyExactIn, - sinon.match((value) => { - return value instanceof Array && value.length == 4; - }), - sinon.match.array, - sinon.match({ blockNumber: sinon.match.defined }) - ); - /// Should not be calling onChainQuoteProvider for mixedRoutes - sinon.assert.callCount(mockOnChainQuoteProvider.getQuotesManyExactIn, 1); - - expect( - swap!.quote.currency.equals(WRAPPED_NATIVE_CURRENCY[ChainId.MAINNET]) - ).toBeTruthy(); - expect( - swap!.quoteGasAdjusted.currency.equals(WRAPPED_NATIVE_CURRENCY[ChainId.MAINNET]) - ).toBeTruthy(); - - expect(swap!.quote.greaterThan(swap!.quoteGasAdjusted)).toBeTruthy(); - expect(swap!.estimatedGasUsed.toString()).toEqual('10000'); - expect( - swap!.estimatedGasUsedQuoteToken.currency.equals( - WRAPPED_NATIVE_CURRENCY[ChainId.MAINNET] - ) - ).toBeTruthy(); - expect( - swap!.estimatedGasUsedUSD.currency.equals(USDC) || - swap!.estimatedGasUsedUSD.currency.equals(USDT) || - swap!.estimatedGasUsedUSD.currency.equals(DAI) - ).toBeTruthy(); - expect(swap!.gasPriceWei.toString()).toEqual( - mockGasPriceWeiBN.toString() - ); - expect(swap!.route).toHaveLength(1); - expect(swap!.trade).toBeDefined(); - expect(swap!.methodParameters).not.toBeDefined(); - expect(swap!.blockNumber.toString()).toEqual(mockBlockBN.toString()); - }); - test('succeeds to route on v3 only', async () => { const amount = CurrencyAmount.fromRawAmount(USDC, 10000); const swap = await alphaRouter.route( @@ -1308,100 +1070,6 @@ describe('alpha router', () => { expect(swap!.blockNumber.toString()).toEqual(mockBlockBN.toString()); }); - test('succeeds to route on mixed only', async () => { - const amount = CurrencyAmount.fromRawAmount(USDC, 10000); - const swap = await alphaRouter.route( - amount, - WRAPPED_NATIVE_CURRENCY[1], - TradeType.EXACT_INPUT, - undefined, - { ...ROUTING_CONFIG, protocols: [Protocol.MIXED] } - ); - expect(swap).toBeDefined(); - - expect(mockFallbackTenderlySimulator.simulate.called).toBeFalsy(); - expect(mockProvider.getBlockNumber.called).toBeTruthy(); - expect(mockGasPriceProvider.getGasPrice.called).toBeTruthy(); - expect( - mockMixedRouteGasModelFactory.buildGasModel.calledWith({ - chainId: 1, - gasPriceWei: mockGasPriceWeiBN, - pools: sinon.match.any, - v2poolProvider: sinon.match.any, - amountToken: amount.currency, - quoteToken: WRAPPED_NATIVE_CURRENCY[1], - providerConfig: sinon.match({ - blockNumber: sinon.match.instanceOf(Promise) - }) - }) - ).toBeTruthy(); - - sinon.assert.calledWith( - mockOnChainQuoteProvider.getQuotesManyExactIn, - sinon.match((value) => { - return value instanceof Array && value.length == 4; - }), - sinon.match.array, - sinon.match({ blockNumber: sinon.match.defined }) - ); - /// Should not be calling onChainQuoteProvider for v3Routes - sinon.assert.callCount(mockOnChainQuoteProvider.getQuotesManyExactIn, 1); - sinon.assert.notCalled(mockOnChainQuoteProvider.getQuotesManyExactOut); - - expect( - swap!.quote.currency.equals(WRAPPED_NATIVE_CURRENCY[1]) - ).toBeTruthy(); - expect( - swap!.quoteGasAdjusted.currency.equals(WRAPPED_NATIVE_CURRENCY[1]) - ).toBeTruthy(); - - for (const r of swap!.route) { - expect(r.protocol).toEqual(Protocol.MIXED); - expect(r.route.input.equals(USDC)).toBeTruthy(); - expect( - r.route.output.equals(WRAPPED_NATIVE_CURRENCY[1].wrapped) - ).toBeTruthy(); - } - - expect(swap!.quote.greaterThan(swap!.quoteGasAdjusted)).toBeTruthy(); - expect(swap!.estimatedGasUsed.toString()).toEqual('10000'); - expect( - swap!.estimatedGasUsedQuoteToken.currency.equals( - WRAPPED_NATIVE_CURRENCY[1] - ) - ).toBeTruthy(); - expect( - swap!.estimatedGasUsedUSD.currency.equals(USDC) || - swap!.estimatedGasUsedUSD.currency.equals(USDT) || - swap!.estimatedGasUsedUSD.currency.equals(DAI) - ).toBeTruthy(); - expect(swap!.gasPriceWei.toString()).toEqual( - mockGasPriceWeiBN.toString() - ); - expect(swap!.route).toHaveLength(1); - expect(swap!.trade).toBeDefined(); - expect(swap!.methodParameters).not.toBeDefined(); - expect(swap!.blockNumber.toString()).toEqual(mockBlockBN.toString()); - }); - - test('finds a route with no protocols specified and forceMixedRoutes is true', async () => { - const swap = await alphaRouter.route( - CurrencyAmount.fromRawAmount(USDC, 10000), - WRAPPED_NATIVE_CURRENCY[1], - TradeType.EXACT_INPUT, - undefined, - { - ...ROUTING_CONFIG, - forceMixedRoutes: true, - } - ); - expect(swap).toBeDefined(); - expect(mockFallbackTenderlySimulator.simulate.called).toBeFalsy(); - expect( - swap!.route.every((route) => route.protocol === Protocol.MIXED) - ).toBeTruthy(); - }); - test('finds a route with V2,V3,Mixed protocols specified and forceMixedRoutes is true', async () => { const swap = await alphaRouter.route( CurrencyAmount.fromRawAmount(USDC, 10000), @@ -1722,83 +1390,6 @@ describe('alpha router', () => { expect(swap!.blockNumber.eq(mockBlockBN)).toBeTruthy(); }); - test('succeeds to route and generates calldata on mixed only', async () => { - const swapParams = { - type: SwapType.UNIVERSAL_ROUTER, - version: UniversalRouterVersion.V1_2, - deadline: Math.floor(Date.now() / 1000) + 1000000, - recipient: '0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B', - slippageTolerance: new Percent(500, 10_000), - }; - - const amount = CurrencyAmount.fromRawAmount(USDC, 10000); - - const swap = await alphaRouter.route( - amount, - WRAPPED_NATIVE_CURRENCY[1], - TradeType.EXACT_INPUT, - swapParams, - { ...ROUTING_CONFIG, protocols: [Protocol.MIXED] } - ); - expect(swap).toBeDefined(); - - expect(mockFallbackTenderlySimulator.simulate.called).toBeFalsy(); - expect(mockProvider.getBlockNumber.called).toBeTruthy(); - expect(mockGasPriceProvider.getGasPrice.called).toBeTruthy(); - expect( - mockMixedRouteGasModelFactory.buildGasModel.calledWith({ - chainId: 1, - gasPriceWei: mockGasPriceWeiBN, - pools: sinon.match.any, - v2poolProvider: sinon.match.any, - amountToken: amount.currency, - quoteToken: WRAPPED_NATIVE_CURRENCY[1], - providerConfig: sinon.match({ - blockNumber: sinon.match.instanceOf(Promise) - }) - }) - ).toBeTruthy(); - - expect( - mockOnChainQuoteProvider.getQuotesManyExactOut.notCalled - ).toBeTruthy(); - - expect( - swap!.quote.currency.equals(WRAPPED_NATIVE_CURRENCY[1]) - ).toBeTruthy(); - expect( - swap!.quoteGasAdjusted.currency.equals(WRAPPED_NATIVE_CURRENCY[1]) - ).toBeTruthy(); - - for (const r of swap!.route) { - expect(r.protocol).toEqual(Protocol.MIXED); - expect(r.route.input.equals(USDC)).toBeTruthy(); - expect( - r.route.output.equals(WRAPPED_NATIVE_CURRENCY[1].wrapped) - ).toBeTruthy(); - } - - expect(swap!.quote.greaterThan(swap!.quoteGasAdjusted)).toBeTruthy(); - expect(swap!.estimatedGasUsed.toString()).toEqual('10000'); - expect( - swap!.estimatedGasUsedQuoteToken.currency.equals( - WRAPPED_NATIVE_CURRENCY[1] - ) - ).toBeTruthy(); - expect( - swap!.estimatedGasUsedUSD.currency.equals(USDC) || - swap!.estimatedGasUsedUSD.currency.equals(USDT) || - swap!.estimatedGasUsedUSD.currency.equals(DAI) - ).toBeTruthy(); - expect(swap!.gasPriceWei.toString()).toEqual( - mockGasPriceWeiBN.toString() - ); - expect(swap!.route).toHaveLength(1); - expect(swap!.trade).toBeDefined(); - expect(swap!.methodParameters).toBeDefined(); - expect(swap!.blockNumber.eq(mockBlockBN)).toBeTruthy(); - }); - test('succeeds to route and generate calldata and simulates', async () => { const swapParams = { type: SwapType.UNIVERSAL_ROUTER, @@ -1890,7 +1481,10 @@ describe('alpha router', () => { MOCK_ZERO_DEC_TOKEN, TradeType.EXACT_INPUT, undefined, - { ...ROUTING_CONFIG } + { + ...ROUTING_CONFIG, + protocols: allProtocols + } ); expect(swap).toBeDefined(); @@ -1902,7 +1496,10 @@ describe('alpha router', () => { MOCK_ZERO_DEC_TOKEN, TradeType.EXACT_INPUT, undefined, - { ...ROUTING_CONFIG } + { + ...ROUTING_CONFIG, + protocols: allProtocols + } ); expect(swap2).toBeDefined(); @@ -1930,7 +1527,10 @@ describe('alpha router', () => { MOCK_ZERO_DEC_TOKEN, TradeType.EXACT_INPUT, undefined, - { ...ROUTING_CONFIG } + { + ...ROUTING_CONFIG, + protocols: allProtocols, + } ); expect(swap).toBeDefined(); @@ -1944,7 +1544,10 @@ describe('alpha router', () => { MOCK_ZERO_DEC_TOKEN, TradeType.EXACT_INPUT, undefined, - { ...ROUTING_CONFIG } + { + ...ROUTING_CONFIG, + protocols: allProtocols + } ); expect(swap2).toBeDefined(); @@ -1956,13 +1559,143 @@ describe('alpha router', () => { MOCK_ZERO_DEC_TOKEN, TradeType.EXACT_INPUT, undefined, - { ...ROUTING_CONFIG } + { + ...ROUTING_CONFIG, + protocols: allProtocols + } ); expect(swap3).toBeDefined(); expect(inMemoryRouteCachingProvider.internalGetCacheRouteCalls).toEqual(3); expect(inMemoryRouteCachingProvider.internalSetCacheRouteCalls).toEqual(2); }); + + describe('UniversalRouter version caching', () => { + const swapParams = { + type: SwapType.UNIVERSAL_ROUTER, + deadline: Math.floor(Date.now() / 1000) + 1000000, + recipient: '0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B', + slippageTolerance: new Percent(500, 10_000), + }; + + test('with V1.2 - hits cache when requested protocols match available protocols (excluding V4)', async () => { + const v1_2Params = { + ...swapParams, + version: UniversalRouterVersion.V1_2 + }; + + // First call to populate cache + const swap = await alphaRouter.route( + CurrencyAmount.fromRawAmount(USDC, 10000), + MOCK_ZERO_DEC_TOKEN, + TradeType.EXACT_INPUT, + v1_2Params, + { + ...ROUTING_CONFIG, + protocols: [Protocol.V2, Protocol.V3, Protocol.MIXED], // Excluding V4 + } + ); + expect(swap).toBeDefined(); + expect(inMemoryRouteCachingProvider.internalGetCacheRouteCalls).toEqual(1); + expect(inMemoryRouteCachingProvider.internalSetCacheRouteCalls).toEqual(1); + + // Second call should hit cache + const swap2 = await alphaRouter.route( + CurrencyAmount.fromRawAmount(USDC, 10000), + MOCK_ZERO_DEC_TOKEN, + TradeType.EXACT_INPUT, + v1_2Params, + { + ...ROUTING_CONFIG, + protocols: [Protocol.V2, Protocol.V3, Protocol.MIXED], // Excluding V4 + } + ); + expect(swap2).toBeDefined(); + expect(inMemoryRouteCachingProvider.internalGetCacheRouteCalls).toEqual(2); + expect(inMemoryRouteCachingProvider.internalSetCacheRouteCalls).toEqual(1); + }); + + test('with V1.2 - skips cache when V2 is missing from requested protocols', async () => { + const v1_2Params = { + ...swapParams, + version: UniversalRouterVersion.V1_2 + }; + + const swap = await alphaRouter.route( + CurrencyAmount.fromRawAmount(USDC, 10000), + MOCK_ZERO_DEC_TOKEN, + TradeType.EXACT_INPUT, + v1_2Params, + { + ...ROUTING_CONFIG, + protocols: [Protocol.V3, Protocol.MIXED], // Missing V2, which is required + } + ); + expect(swap).toBeDefined(); + // Should skip cache since V2 is missing but required for V1.2 + expect(inMemoryRouteCachingProvider.internalGetCacheRouteCalls).toEqual(0); + expect(inMemoryRouteCachingProvider.internalSetCacheRouteCalls).toEqual(1); + }); + + test('with V2.0 - hits cache when all protocols are requested', async () => { + const v2_0Params = { + ...swapParams, + version: UniversalRouterVersion.V2_0 + }; + + // First call to populate cache + const swap = await alphaRouter.route( + CurrencyAmount.fromRawAmount(USDC, 10000), + MOCK_ZERO_DEC_TOKEN, + TradeType.EXACT_INPUT, + v2_0Params, + { + ...ROUTING_CONFIG, + protocols: [Protocol.V2, Protocol.V3, Protocol.V4, Protocol.MIXED], + } + ); + expect(swap).toBeDefined(); + expect(inMemoryRouteCachingProvider.internalGetCacheRouteCalls).toEqual(1); + expect(inMemoryRouteCachingProvider.internalSetCacheRouteCalls).toEqual(1); + + // Second call should hit cache + const swap2 = await alphaRouter.route( + CurrencyAmount.fromRawAmount(USDC, 10000), + MOCK_ZERO_DEC_TOKEN, + TradeType.EXACT_INPUT, + v2_0Params, + { + ...ROUTING_CONFIG, + protocols: [Protocol.V2, Protocol.V3, Protocol.V4, Protocol.MIXED], + } + ); + expect(swap2).toBeDefined(); + expect(inMemoryRouteCachingProvider.internalGetCacheRouteCalls).toEqual(2); + expect(inMemoryRouteCachingProvider.internalSetCacheRouteCalls).toEqual(1); + }); + + test('with V2.0 - skips cache when subset of protocols requested', async () => { + const v2_0Params = { + ...swapParams, + version: UniversalRouterVersion.V2_0 + }; + + const swap = await alphaRouter.route( + CurrencyAmount.fromRawAmount(USDC, 10000), + MOCK_ZERO_DEC_TOKEN, + TradeType.EXACT_INPUT, + v2_0Params, + { + ...ROUTING_CONFIG, + protocols: [Protocol.V2, Protocol.V3], // Only subset of available protocols + } + ); + expect(swap).toBeDefined(); + // Should skip cache since not all available protocols are requested + expect(inMemoryRouteCachingProvider.internalGetCacheRouteCalls).toEqual(0); + expect(inMemoryRouteCachingProvider.internalSetCacheRouteCalls).toEqual(1); + }); + }); }); describe('token in is fee-on-transfer token on sell', () => {