Commit b74fb817 authored by Zach Pomerantz's avatar Zach Pomerantz Committed by GitHub

feat: optimize AlphaRouter usage (#4596)

* feat: add RouterPreference.PRICE

* docs: add reference to quote params

* fix: cache routers between usages

* fix: tune price inquiries

* fix: note price queries tuning

* fix: clean up params - nix mixed

* fix: defer PRICE tuning
parent a7ec5a64
import { renderHook } from '@testing-library/react'
import { CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { DAI, USDC_MAINNET } from 'constants/tokens'
import { RouterPreference } from 'state/routing/slice'
import { TradeState } from 'state/routing/types'
import { useClientSideRouter } from 'state/user/hooks'
import { useRoutingAPITrade } from '../state/routing/useRoutingAPITrade'
import useAutoRouterSupported from './useAutoRouterSupported'
......@@ -25,6 +27,7 @@ const mockUseAutoRouterSupported = useAutoRouterSupported as jest.MockedFunction
const mockUseIsWindowVisible = useIsWindowVisible as jest.MockedFunction<typeof useIsWindowVisible>
const mockUseRoutingAPITrade = useRoutingAPITrade as jest.MockedFunction<typeof useRoutingAPITrade>
const mockUseClientSideRouter = useClientSideRouter as jest.MockedFunction<typeof useClientSideRouter>
const mockUseClientSideV3Trade = useClientSideV3Trade as jest.MockedFunction<typeof useClientSideV3Trade>
// helpers to set mock expectations
......@@ -42,6 +45,7 @@ beforeEach(() => {
mockUseIsWindowVisible.mockReturnValue(true)
mockUseAutoRouterSupported.mockReturnValue(true)
mockUseClientSideRouter.mockReturnValue([true, () => undefined])
})
describe('#useBestV3Trade ExactIn', () => {
......@@ -52,7 +56,7 @@ describe('#useBestV3Trade ExactIn', () => {
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_INPUT, USDCAmount, DAI))
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, DAI)
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, DAI, RouterPreference.CLIENT)
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, USDCAmount, DAI)
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
})
......@@ -64,7 +68,7 @@ describe('#useBestV3Trade ExactIn', () => {
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_INPUT, USDCAmount, DAI))
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, DAI)
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, DAI, RouterPreference.CLIENT)
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, USDCAmount, DAI)
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
})
......@@ -128,7 +132,12 @@ describe('#useBestV3Trade ExactOut', () => {
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, USDC_MAINNET)
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(
TradeType.EXACT_OUTPUT,
undefined,
USDC_MAINNET,
RouterPreference.CLIENT
)
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET)
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
})
......@@ -140,7 +149,12 @@ describe('#useBestV3Trade ExactOut', () => {
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, USDC_MAINNET)
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(
TradeType.EXACT_OUTPUT,
undefined,
USDC_MAINNET,
RouterPreference.CLIENT
)
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET)
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
})
......
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { useMemo } from 'react'
import { RouterPreference } from 'state/routing/slice'
import { InterfaceTrade, TradeState } from 'state/routing/types'
import { useRoutingAPITrade } from 'state/routing/useRoutingAPITrade'
import { useClientSideRouter } from 'state/user/hooks'
import useAutoRouterSupported from './useAutoRouterSupported'
import { useClientSideV3Trade } from './useClientSideV3Trade'
......@@ -30,10 +32,12 @@ export function useBestTrade(
200
)
const [clientSideRouter] = useClientSideRouter()
const routingAPITrade = useRoutingAPITrade(
tradeType,
autoRouterSupported && isWindowVisible ? debouncedAmount : undefined,
debouncedOtherCurrency
debouncedOtherCurrency,
clientSideRouter ? RouterPreference.CLIENT : RouterPreference.API
)
const isLoading = routingAPITrade.state === TradeState.LOADING
......
......@@ -2,7 +2,8 @@ import { Currency, CurrencyAmount, Price, Token, TradeType } from '@uniswap/sdk-
import { useWeb3React } from '@web3-react/core'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { useMemo, useRef } from 'react'
import { RouterPreference, useRoutingAPITrade } from 'state/routing/useRoutingAPITrade'
import { RouterPreference } from 'state/routing/slice'
import { useRoutingAPITrade } from 'state/routing/useRoutingAPITrade'
import { SupportedChainId } from '../constants/chains'
import { CUSD_CELO, DAI_OPTIMISM, USDC_ARBITRUM, USDC_MAINNET, USDC_POLYGON } from '../constants/tokens'
......@@ -27,7 +28,7 @@ export default function useStablecoinPrice(currency?: Currency): Price<Currency,
const amountOut = chainId ? STABLECOIN_AMOUNT_OUT[chainId] : undefined
const stablecoin = amountOut?.currency
const { trade } = useRoutingAPITrade(TradeType.EXACT_OUTPUT, amountOut, currency, RouterPreference.CLIENT)
const { trade } = useRoutingAPITrade(TradeType.EXACT_OUTPUT, amountOut, currency, RouterPreference.PRICE)
const price = useMemo(() => {
if (!currency || !stablecoin) {
return undefined
......
import { BigintIsh, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core'
// This file is lazy-loaded, so the import of smart-order-router is intentional.
// eslint-disable-next-line no-restricted-imports
import { AlphaRouter, AlphaRouterConfig, AlphaRouterParams, ChainId } from '@uniswap/smart-order-router'
import { AlphaRouter, AlphaRouterConfig, ChainId } from '@uniswap/smart-order-router'
import { SupportedChainId } from 'constants/chains'
import JSBI from 'jsbi'
import { GetQuoteResult } from 'state/routing/types'
......@@ -29,11 +29,9 @@ async function getQuote(
tokenOut: { address: string; chainId: number; decimals: number; symbol?: string }
amount: BigintIsh
},
routerParams: AlphaRouterParams,
routerConfig: Partial<AlphaRouterConfig>
router: AlphaRouter,
config: Partial<AlphaRouterConfig>
): Promise<{ data: GetQuoteResult; error?: unknown }> {
const router = new AlphaRouter(routerParams)
const currencyIn = new Token(tokenIn.chainId, tokenIn.address, tokenIn.decimals, tokenIn.symbol)
const currencyOut = new Token(tokenOut.chainId, tokenOut.address, tokenOut.decimals, tokenOut.symbol)
......@@ -46,7 +44,7 @@ async function getQuote(
quoteCurrency,
type === 'exactIn' ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT,
/*swapConfig=*/ undefined,
routerConfig
config
)
if (!swapRoute) throw new Error('Failed to generate client side quote')
......@@ -80,8 +78,8 @@ export async function getClientSideQuote(
amount,
type,
}: QuoteArguments,
routerParams: AlphaRouterParams,
routerConfig: Partial<AlphaRouterConfig>
router: AlphaRouter,
config: Partial<AlphaRouterConfig>
) {
return getQuote(
{
......@@ -100,7 +98,7 @@ export async function getClientSideQuote(
},
amount,
},
routerParams,
routerConfig
router,
config
)
}
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { useMemo } from 'react'
import { RouterPreference } from 'state/routing/slice'
/**
* Returns query arguments for the Routing API query or undefined if the
......@@ -11,13 +12,13 @@ export function useRoutingAPIArguments({
tokenOut,
amount,
tradeType,
useClientSideRouter,
routerPreference,
}: {
tokenIn: Currency | undefined
tokenOut: Currency | undefined
amount: CurrencyAmount<Currency> | undefined
tradeType: TradeType
useClientSideRouter: boolean
routerPreference: RouterPreference
}) {
return useMemo(
() =>
......@@ -33,9 +34,9 @@ export function useRoutingAPIArguments({
tokenOutChainId: tokenOut.wrapped.chainId,
tokenOutDecimals: tokenOut.wrapped.decimals,
tokenOutSymbol: tokenOut.wrapped.symbol,
useClientSideRouter,
routerPreference,
type: (tradeType === TradeType.EXACT_INPUT ? 'exactIn' : 'exactOut') as 'exactIn' | 'exactOut',
},
[amount, tokenIn, tokenOut, tradeType, useClientSideRouter]
[amount, routerPreference, tokenIn, tokenOut, tradeType]
)
}
import { BaseProvider, JsonRpcProvider } from '@ethersproject/providers'
import { JsonRpcProvider } from '@ethersproject/providers'
import { createApi, fetchBaseQuery, FetchBaseQueryError } from '@reduxjs/toolkit/query/react'
import { Protocol } from '@uniswap/router-sdk'
import { ChainId } from '@uniswap/smart-order-router'
import { AlphaRouter, ChainId } from '@uniswap/smart-order-router'
import { RPC_URLS } from 'constants/networks'
import { getClientSideQuote, toSupportedChainId } from 'lib/hooks/routing/clientSideSmartOrderRouter'
import ms from 'ms.macro'
......@@ -9,28 +9,60 @@ import qs from 'qs'
import { GetQuoteResult } from './types'
const routerProviders = new Map<ChainId, BaseProvider>()
function getRouterProvider(chainId: ChainId): BaseProvider {
const provider = routerProviders.get(chainId)
if (provider) return provider
export enum RouterPreference {
API = 'api',
CLIENT = 'client',
PRICE = 'price',
}
const routers = new Map<ChainId, AlphaRouter>()
function getRouter(chainId: ChainId): AlphaRouter {
const router = routers.get(chainId)
if (router) return router
const supportedChainId = toSupportedChainId(chainId)
if (supportedChainId) {
const provider = new JsonRpcProvider(RPC_URLS[supportedChainId])
routerProviders.set(chainId, provider)
return provider
const router = new AlphaRouter({ chainId, provider })
routers.set(chainId, router)
return router
}
throw new Error(`Router does not support this chain (chainId: ${chainId}).`)
}
const protocols: Protocol[] = [Protocol.V2, Protocol.V3, Protocol.MIXED]
const DEFAULT_QUERY_PARAMS = {
protocols: protocols.map((p) => p.toLowerCase()).join(','),
// example other params
// forceCrossProtocol: 'true',
// minSplits: '5',
// routing API quote params: https://github.com/Uniswap/routing-api/blob/main/lib/handlers/quote/schema/quote-schema.ts
const API_QUERY_PARAMS = {
protocols: 'v2,v3,mixed',
}
const CLIENT_PARAMS = {
protocols: [Protocol.V2, Protocol.V3, Protocol.MIXED],
}
// Price queries are tuned down to minimize the required RPCs to respond to them.
// TODO(zzmp): This will be used after testing router caching.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const PRICE_PARAMS = {
protocols: [Protocol.V2, Protocol.V3],
v2PoolSelection: {
topN: 2,
topNDirectSwaps: 1,
topNTokenInOut: 2,
topNSecondHop: 1,
topNWithEachBaseToken: 2,
topNWithBaseToken: 2,
},
v3PoolSelection: {
topN: 2,
topNDirectSwaps: 1,
topNTokenInOut: 2,
topNSecondHop: 1,
topNWithEachBaseToken: 2,
topNWithBaseToken: 2,
},
maxSwapsPerPath: 2,
minSplits: 1,
maxSplits: 1,
distributionPercent: 100,
}
export const routingApi = createApi({
......@@ -51,24 +83,20 @@ export const routingApi = createApi({
tokenOutDecimals: number
tokenOutSymbol?: string
amount: string
useClientSideRouter: boolean // included in key to invalidate on change
routerPreference: RouterPreference
type: 'exactIn' | 'exactOut'
}
>({
async queryFn(args, _api, _extraOptions, fetch) {
const { tokenInAddress, tokenInChainId, tokenOutAddress, tokenOutChainId, amount, useClientSideRouter, type } =
const { tokenInAddress, tokenInChainId, tokenOutAddress, tokenOutChainId, amount, routerPreference, type } =
args
let result
try {
if (useClientSideRouter) {
const chainId = args.tokenInChainId
const params = { chainId, provider: getRouterProvider(chainId) }
result = await getClientSideQuote(args, params, { protocols })
} else {
if (routerPreference === RouterPreference.API) {
const query = qs.stringify({
...DEFAULT_QUERY_PARAMS,
...API_QUERY_PARAMS,
tokenInAddress,
tokenInChainId,
tokenOutAddress,
......@@ -77,6 +105,15 @@ export const routingApi = createApi({
type,
})
result = await fetch(`quote?${query}`)
} else {
const router = getRouter(args.tokenInChainId)
result = await getClientSideQuote(
args,
router,
// TODO(zzmp): Use PRICE_PARAMS for RouterPreference.PRICE.
// This change is intentionally being deferred to first see what effect router caching has.
CLIENT_PARAMS
)
}
return { data: result.data as GetQuoteResult }
......
......@@ -7,17 +7,11 @@ import { useRoutingAPIArguments } from 'lib/hooks/routing/useRoutingAPIArguments
import useIsValidBlock from 'lib/hooks/useIsValidBlock'
import ms from 'ms.macro'
import { useMemo } from 'react'
import { useGetQuoteQuery } from 'state/routing/slice'
import { useClientSideRouter } from 'state/user/hooks'
import { RouterPreference, useGetQuoteQuery } from 'state/routing/slice'
import { GetQuoteResult, InterfaceTrade, TradeState } from './types'
import { computeRoutes, transformRoutesToTrade } from './utils'
export enum RouterPreference {
CLIENT = 'client',
API = 'api',
}
/**
* Returns the best trade by invoking the routing api or the smart order router on the client
* @param tradeType whether the swap is an exact in/out
......@@ -26,9 +20,9 @@ export enum RouterPreference {
*/
export function useRoutingAPITrade<TTradeType extends TradeType>(
tradeType: TTradeType,
amountSpecified?: CurrencyAmount<Currency>,
otherCurrency?: Currency,
routerPreference?: RouterPreference
amountSpecified: CurrencyAmount<Currency> | undefined,
otherCurrency: Currency | undefined,
routerPreference: RouterPreference
): {
state: TradeState
trade: InterfaceTrade<Currency, Currency, TTradeType> | undefined
......@@ -41,17 +35,12 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
[amountSpecified, otherCurrency, tradeType]
)
const [clientSideRouterStoredPreference] = useClientSideRouter()
const clientSideRouter = routerPreference
? routerPreference === RouterPreference.CLIENT
: clientSideRouterStoredPreference
const queryArgs = useRoutingAPIArguments({
tokenIn: currencyIn,
tokenOut: currencyOut,
amount: amountSpecified,
tradeType,
useClientSideRouter: clientSideRouter,
routerPreference,
})
const { isLoading, isError, data, currentData } = useGetQuoteQuery(queryArgs ?? skipToken, {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment