From e429e6479ec4e0b749895a2fd38c12814e02b1d7 Mon Sep 17 00:00:00 2001 From: "Siyu Jiang (See-You John)" <91580504+jsy1218@users.noreply.github.com> Date: Thu, 29 Aug 2024 11:39:52 -0700 Subject: [PATCH] chore: bump sor to 3.46.0 - feat: implement best swap route with v4 routes (#822) Release https://github.com/Uniswap/smart-order-router/pull/682 --- lib/handlers/injector-sor.ts | 22 +++++++++--- lib/handlers/quote/injector.ts | 5 +++ lib/handlers/quote/quote.ts | 42 ++++++++++++++++++++--- lib/handlers/quote/schema/quote-schema.ts | 2 +- lib/handlers/schema.ts | 19 +++++++++- package-lock.json | 14 ++++---- package.json | 2 +- test/mocha/e2e/quote.test.ts | 17 +++++++-- 8 files changed, 101 insertions(+), 22 deletions(-) diff --git a/lib/handlers/injector-sor.ts b/lib/handlers/injector-sor.ts index b651cadaaf..9f994a5193 100644 --- a/lib/handlers/injector-sor.ts +++ b/lib/handlers/injector-sor.ts @@ -28,6 +28,7 @@ import { NodeJSCache, OnChainGasPriceProvider, OnChainQuoteProvider, + PROTOCOL_V4_QUOTER_ADDRESSES, QUOTER_V2_ADDRESSES, setGlobalLogger, Simulator, @@ -104,6 +105,7 @@ const DEFAULT_TOKEN_LIST = 'https://gateway.ipfs.io/ipns/tokens.uniswap.org' export interface RequestInjected extends BaseRInj { chainId: ChainId metric: IMetric + v4PoolProvider: IV4PoolProvider v3PoolProvider: IV3PoolProvider v2PoolProvider: IV2PoolProvider tokenProvider: ITokenProvider @@ -122,6 +124,7 @@ export type ContainerDependencies = { gasPriceProvider: IGasPriceProvider tokenProviderFromTokenList: ITokenProvider blockedTokenListProvider: ITokenListProvider + v4PoolProvider: IV4PoolProvider v3PoolProvider: IV3PoolProvider v2PoolProvider: IV2PoolProvider tokenProvider: ITokenProvider @@ -358,8 +361,12 @@ export abstract class InjectorSOR extends Injector< SUCCESS_RATE_FAILURE_OVERRIDES[chainId], BLOCK_NUMBER_CONFIGS[chainId], // We will only enable shadow sample mixed quoter on Base - (useMixedRouteQuoter: boolean) => - useMixedRouteQuoter ? MIXED_ROUTE_QUOTER_V1_ADDRESSES[chainId] : QUOTER_V2_ADDRESSES[chainId] + (useMixedRouteQuoter: boolean, protocol: Protocol) => + useMixedRouteQuoter + ? MIXED_ROUTE_QUOTER_V1_ADDRESSES[chainId] + : protocol === Protocol.V3 + ? QUOTER_V2_ADDRESSES[chainId] + : PROTOCOL_V4_QUOTER_ADDRESSES[chainId] ) const targetQuoteProvider = new OnChainQuoteProvider( chainId, @@ -375,8 +382,12 @@ export abstract class InjectorSOR extends Injector< GAS_ERROR_FAILURE_OVERRIDES[chainId], SUCCESS_RATE_FAILURE_OVERRIDES[chainId], BLOCK_NUMBER_CONFIGS[chainId], - (useMixedRouteQuoter: boolean) => - useMixedRouteQuoter ? MIXED_ROUTE_QUOTER_V1_ADDRESSES[chainId] : NEW_QUOTER_V2_ADDRESSES[chainId], + (useMixedRouteQuoter: boolean, protocol: Protocol) => + useMixedRouteQuoter + ? MIXED_ROUTE_QUOTER_V1_ADDRESSES[chainId] + : protocol === Protocol.V3 + ? NEW_QUOTER_V2_ADDRESSES[chainId] + : PROTOCOL_V4_QUOTER_ADDRESSES[chainId], (chainId: ChainId, useMixedRouteQuoter: boolean, optimisticCachedRoutes: boolean) => useMixedRouteQuoter ? `ChainId_${chainId}_ShadowMixedQuoter_OptimisticCachedRoutes${optimisticCachedRoutes}_` @@ -447,6 +458,8 @@ export abstract class InjectorSOR extends Injector< ChainId.BLAST, ] + const v4Supported = [ChainId.SEPOLIA] + return { chainId, dependencies: { @@ -478,6 +491,7 @@ export abstract class InjectorSOR extends Injector< tokenValidatorProvider, tokenPropertiesProvider, v2Supported, + v4Supported, }, } }) diff --git a/lib/handlers/quote/injector.ts b/lib/handlers/quote/injector.ts index 96af4a95ed..1ba5c63745 100644 --- a/lib/handlers/quote/injector.ts +++ b/lib/handlers/quote/injector.ts @@ -84,6 +84,8 @@ export class QuoteHandlerInjector extends InjectorSOR< const { provider, + v4PoolProvider, + v4SubgraphProvider, v3PoolProvider, multicallProvider, tokenProvider, @@ -115,6 +117,8 @@ export class QuoteHandlerInjector extends InjectorSOR< router = new AlphaRouter({ chainId, provider, + v4SubgraphProvider, + v4PoolProvider, v3SubgraphProvider, multicall2Provider: multicallProvider, v3PoolProvider, @@ -141,6 +145,7 @@ export class QuoteHandlerInjector extends InjectorSOR< log, metric, router, + v4PoolProvider, v3PoolProvider, v2PoolProvider, tokenProvider, diff --git a/lib/handlers/quote/quote.ts b/lib/handlers/quote/quote.ts index 3333283aee..f170d3e168 100644 --- a/lib/handlers/quote/quote.ts +++ b/lib/handlers/quote/quote.ts @@ -18,7 +18,7 @@ import JSBI from 'jsbi' import _ from 'lodash' import { APIGLambdaHandler, ErrorResponse, HandleRequestParams, Response } from '../handler' import { ContainerInjected, RequestInjected } from '../injector-sor' -import { QuoteResponse, QuoteResponseSchemaJoi, V2PoolInRoute, V3PoolInRoute } from '../schema' +import { QuoteResponse, QuoteResponseSchemaJoi, SupportedPoolInRoute } from '../schema' import { DEFAULT_ROUTING_CONFIG_BY_CHAIN, FEE_ON_TRANSFER_SPECIFIC_CONFIG, @@ -235,6 +235,7 @@ export class QuoteHandler extends APIGLambdaHandler< chainId, tokenProvider, tokenListProvider, + v4PoolProvider: v4PoolProvider, v3PoolProvider: v3PoolProvider, v2PoolProvider: v2PoolProvider, metric, @@ -482,13 +483,13 @@ export class QuoteHandler extends APIGLambdaHandler< metric.putMetric('SimulationNotSupported', 1, MetricLoggerUnit.Count) } - const routeResponse: Array<(V3PoolInRoute | V2PoolInRoute)[]> = [] + const routeResponse: Array = [] for (const subRoute of route) { const { amount, quote, tokenPath } = subRoute const pools = subRoute.protocol == Protocol.V2 ? subRoute.route.pairs : subRoute.route.pools - const curRoute: (V3PoolInRoute | V2PoolInRoute)[] = [] + const curRoute: SupportedPoolInRoute[] = [] for (let i = 0; i < pools.length; i++) { const nextPool = pools[i] const tokenIn = tokenPath[i] @@ -505,8 +506,36 @@ export class QuoteHandler extends APIGLambdaHandler< } if (nextPool instanceof V4Pool) { - // TODO - ROUTE-220: Support V4 Pool - throw new Error(`V4 pools are not supported in quote response deserialization ${JSON.stringify(nextPool)}`) + curRoute.push({ + type: 'v4-pool', + address: v4PoolProvider.getPoolId( + nextPool.token0, + nextPool.token1, + nextPool.fee, + nextPool.tickSpacing, + nextPool.hooks + ).poolId, + tokenIn: { + chainId: tokenIn.chainId, + decimals: tokenIn.decimals.toString(), + address: tokenIn.wrapped.address, + symbol: tokenIn.symbol!, + }, + tokenOut: { + chainId: tokenOut.chainId, + decimals: tokenOut.decimals.toString(), + address: tokenOut.wrapped.address, + symbol: tokenOut.symbol!, + }, + fee: nextPool.fee.toString(), + tickSpacing: nextPool.tickSpacing.toString(), + hooks: nextPool.hooks.toString(), + liquidity: nextPool.liquidity.toString(), + sqrtRatioX96: nextPool.sqrtRatioX96.toString(), + tickCurrent: nextPool.tickCurrent.toString(), + amountIn: edgeAmountIn, + amountOut: edgeAmountOut, + }) } else if (nextPool instanceof V3Pool) { curRoute.push({ type: 'v3-pool', @@ -679,6 +708,9 @@ export class QuoteHandler extends APIGLambdaHandler< case Protocol.V3: protocols.push(Protocol.V3) break + case Protocol.V4: + protocols.push(Protocol.V4) + break case Protocol.MIXED: if (chainId === ChainId.MAINNET || !excludeV2) { protocols.push(Protocol.MIXED) diff --git a/lib/handlers/quote/schema/quote-schema.ts b/lib/handlers/quote/schema/quote-schema.ts index 5ad1aae0b8..32d1872630 100644 --- a/lib/handlers/quote/schema/quote-schema.ts +++ b/lib/handlers/quote/schema/quote-schema.ts @@ -46,7 +46,7 @@ export const QuoteQueryParamsJoi = Joi.object({ minSplits: Joi.number().max(7).optional(), forceCrossProtocol: Joi.boolean().optional(), forceMixedRoutes: Joi.boolean().optional(), - protocols: Joi.stringArray().items(Joi.string().valid('v2', 'v3', 'mixed')).optional(), + protocols: Joi.stringArray().items(Joi.string().valid('v2', 'v3', 'v4', 'mixed')).optional(), simulateFromAddress: Joi.string().alphanum().max(42).optional(), permitSignature: Joi.string().optional(), permitNonce: Joi.string().optional(), diff --git a/lib/handlers/schema.ts b/lib/handlers/schema.ts index fff08163e1..0906aaeaaa 100644 --- a/lib/handlers/schema.ts +++ b/lib/handlers/schema.ts @@ -11,6 +11,23 @@ export type TokenInRoute = { sellFeeBps?: string } +export type SupportedPoolInRoute = V2PoolInRoute | V3PoolInRoute | V4PoolInRoute + +export type V4PoolInRoute = { + type: 'v4-pool' + address: string + tokenIn: TokenInRoute + tokenOut: TokenInRoute + sqrtRatioX96: string + liquidity: string + tickCurrent: string + fee: string + tickSpacing: string + hooks: string + amountIn?: string + amountOut?: string +} + export type V3PoolInRoute = { type: 'v3-pool' address: string @@ -94,7 +111,7 @@ export type QuoteResponse = { simulationStatus: RoutingApiSimulationStatus gasPriceWei: string blockNumber: string - route: Array<(V3PoolInRoute | V2PoolInRoute)[]> + route: Array routeString: string methodParameters?: MethodParameters hitsCachedRoutes?: boolean diff --git a/package-lock.json b/package-lock.json index 897c6e773e..02aa68ed01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "@uniswap/permit2-sdk": "^1.3.0", "@uniswap/router-sdk": "^1.10.0", "@uniswap/sdk-core": "^5.3.0", - "@uniswap/smart-order-router": "3.45.0", + "@uniswap/smart-order-router": "3.46.0", "@uniswap/token-lists": "^1.0.0-beta.33", "@uniswap/universal-router-sdk": "^2.2.4", "@uniswap/v2-sdk": "^4.3.2", @@ -4749,9 +4749,9 @@ } }, "node_modules/@uniswap/smart-order-router": { - "version": "3.45.0", - "resolved": "https://registry.npmjs.org/@uniswap/smart-order-router/-/smart-order-router-3.45.0.tgz", - "integrity": "sha512-ljc1pq4TSYC3Zz391nwedfg5XNxAh8XWjMp+wawI6YUdtEepyFnj8qYcN0FKogmnsFyVdnVgSV29XABZBATjWg==", + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/@uniswap/smart-order-router/-/smart-order-router-3.46.0.tgz", + "integrity": "sha512-mXCAmYSHkQhT2kusHuSRdwDtz2gsig5G2tS0AsCGx0X1sxfp9QP76ztYXWUJCf6NQgc8zuCT7fAiBxngRKfcMA==", "dependencies": { "@eth-optimism/sdk": "^3.2.2", "@types/brotli": "^1.3.4", @@ -28422,9 +28422,9 @@ } }, "@uniswap/smart-order-router": { - "version": "3.45.0", - "resolved": "https://registry.npmjs.org/@uniswap/smart-order-router/-/smart-order-router-3.45.0.tgz", - "integrity": "sha512-ljc1pq4TSYC3Zz391nwedfg5XNxAh8XWjMp+wawI6YUdtEepyFnj8qYcN0FKogmnsFyVdnVgSV29XABZBATjWg==", + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/@uniswap/smart-order-router/-/smart-order-router-3.46.0.tgz", + "integrity": "sha512-mXCAmYSHkQhT2kusHuSRdwDtz2gsig5G2tS0AsCGx0X1sxfp9QP76ztYXWUJCf6NQgc8zuCT7fAiBxngRKfcMA==", "requires": { "@eth-optimism/sdk": "^3.2.2", "@types/brotli": "^1.3.4", diff --git a/package.json b/package.json index 2329b70f1e..eb0447517f 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "@uniswap/router-sdk": "^1.10.0", "@uniswap/sdk-core": "^5.3.0", "@types/semver": "^7.5.8", - "@uniswap/smart-order-router": "3.45.0", + "@uniswap/smart-order-router": "3.46.0", "@uniswap/token-lists": "^1.0.0-beta.33", "@uniswap/universal-router-sdk": "^2.2.4", "@uniswap/v2-sdk": "^4.3.2", diff --git a/test/mocha/e2e/quote.test.ts b/test/mocha/e2e/quote.test.ts index accbf11b4b..2d7b41eabd 100644 --- a/test/mocha/e2e/quote.test.ts +++ b/test/mocha/e2e/quote.test.ts @@ -20,6 +20,8 @@ import { USDC_NATIVE_OPTIMISM, USDC_NATIVE_POLYGON, USDT_MAINNET, + V4_SEPOLIA_TEST_OP, + V4_SEPOLIA_TEST_USDC, WBTC_MAINNET, } from '@uniswap/smart-order-router' import { @@ -2482,6 +2484,7 @@ describe('quote', function () { [ChainId.MAINNET]: () => USDC_ON(1), [ChainId.GOERLI]: () => USDC_ON(ChainId.GOERLI), [ChainId.SEPOLIA]: () => USDC_ON(ChainId.SEPOLIA), + [ChainId.SEPOLIA]: () => V4_SEPOLIA_TEST_OP, [ChainId.OPTIMISM]: () => USDC_ON(ChainId.OPTIMISM), [ChainId.OPTIMISM]: () => USDC_NATIVE_OPTIMISM, [ChainId.OPTIMISM_GOERLI]: () => USDC_ON(ChainId.OPTIMISM_GOERLI), @@ -2514,6 +2517,7 @@ describe('quote', function () { [ChainId.MAINNET]: () => DAI_ON(1), [ChainId.GOERLI]: () => DAI_ON(ChainId.GOERLI), [ChainId.SEPOLIA]: () => DAI_ON(ChainId.SEPOLIA), + [ChainId.SEPOLIA]: () => V4_SEPOLIA_TEST_USDC, [ChainId.OPTIMISM]: () => DAI_ON(ChainId.OPTIMISM), [ChainId.OPTIMISM_GOERLI]: () => DAI_ON(ChainId.OPTIMISM_GOERLI), [ChainId.OPTIMISM_SEPOLIA]: () => USDC_ON(ChainId.OPTIMISM_SEPOLIA), @@ -2563,6 +2567,11 @@ describe('quote', function () { const wrappedNative = WNATIVE_ON(chain) it(`${wrappedNative.symbol} -> erc20`, async () => { + if (chain === ChainId.SEPOLIA && erc1.equals(V4_SEPOLIA_TEST_OP)) { + // there's no WETH/USDC v4 pool on Sepolia + return + } + // Current WETH/USDB pool (https://blastscan.io/address/0xf52b4b69123cbcf07798ae8265642793b2e8990c) has low WETH amount const amount = chain === ChainId.BLAST ? (type === 'exactOut' ? '0.002' : '0.01') : '1' @@ -2620,8 +2629,8 @@ describe('quote', function () { } }) - it(`erc20 -> erc20`, async () => { - if (chain === ChainId.SEPOLIA) { + it(`${erc1.symbol} -> ${erc2.symbol}`, async () => { + if (chain === ChainId.SEPOLIA && !erc1.equals(V4_SEPOLIA_TEST_OP)) { // Sepolia doesn't have sufficient liquidity on DAI pools yet return } @@ -2635,6 +2644,7 @@ describe('quote', function () { tokenOutAddress: erc2.address, tokenOutChainId: chain, amount: await getAmountFromToken(type, erc1, erc2, amount), + protocols: 'v2,v3,v4,mixed', type, } @@ -2689,7 +2699,7 @@ describe('quote', function () { } }) it(`has quoteGasAdjusted values`, async () => { - if (chain === ChainId.SEPOLIA) { + if (chain === ChainId.SEPOLIA && !erc1.equals(V4_SEPOLIA_TEST_OP)) { // Sepolia doesn't have sufficient liquidity on DAI pools yet return } @@ -2703,6 +2713,7 @@ describe('quote', function () { tokenOutAddress: erc2.address, tokenOutChainId: chain, amount: await getAmountFromToken(type, erc1, erc2, amount), + protocols: 'v2,v3,v4,mixed', type, }