Commit 8954aa79 authored by Tina's avatar Tina Committed by GitHub

feat: Use routing-api v2 behind feature flag (#6594)

* add unified-routing-api slice

* rename legacy -> legacyAPI

* deduplicate client params and ura params

* move shared functions into utils and rename comments for unified routing API

* use feature flag

* remove eslint ignore since the function is now being used

* add typing to args

* rename ura -> routing-api v2

* update trace name and comment

* rename variables

* lint
parent 379437b7
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
import { DetailsV2Variant, useDetailsV2Flag } from 'featureFlags/flags/nftDetails'
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
import { UnifiedRouterVariant, useUnifiedRoutingAPIFlag } from 'featureFlags/flags/unifiedRouter'
import { UnifiedRouterVariant, useRoutingAPIV2Flag } from 'featureFlags/flags/unifiedRouter'
import { useUpdateAtom } from 'jotai/utils'
import { Children, PropsWithChildren, ReactElement, ReactNode, useCallback, useState } from 'react'
import { X } from 'react-feather'
......@@ -210,7 +210,7 @@ export default function FeatureFlagModal() {
/>
<FeatureFlagOption
variant={UnifiedRouterVariant}
value={useUnifiedRoutingAPIFlag()}
value={useRoutingAPIV2Flag()}
featureFlag={FeatureFlag.uraEnabled}
label="Enable the Unified Routing API"
/>
......
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useUnifiedRoutingAPIFlag(): BaseVariant {
export function useRoutingAPIV2Flag(): BaseVariant {
return useBaseFlag(FeatureFlag.uraEnabled)
}
// eslint-disable-next-line import/no-unused-modules
export function useUnifiedRoutingAPIEnabled(): boolean {
return useUnifiedRoutingAPIFlag() === BaseVariant.Enabled
export function useRoutingAPIV2Enabled(): boolean {
return useRoutingAPIV2Flag() === BaseVariant.Enabled
}
export { BaseVariant as UnifiedRouterVariant }
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { useMemo } from 'react'
import { INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference } from 'state/routing/slice'
import { GetQuoteArgs, INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference } from 'state/routing/slice'
import { currencyAddressForSwapQuote } from 'state/routing/utils'
/**
......@@ -20,7 +20,7 @@ export function useRoutingAPIArguments({
amount?: CurrencyAmount<Currency>
tradeType: TradeType
routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE
}) {
}): GetQuoteArgs | undefined {
return useMemo(
() =>
!tokenIn || !tokenOut || !amount || tokenIn.equals(tokenOut) || tokenIn.wrapped.equals(tokenOut.wrapped)
......
......@@ -7,6 +7,7 @@ import { updateVersion } from './global/actions'
import { sentryEnhancer } from './logging'
import reducer from './reducer'
import { routingApi } from './routing/slice'
import { routingApiV2 } from './routing/v2Slice'
const PERSISTED_KEYS: string[] = ['user', 'transactions', 'lists']
......@@ -20,10 +21,11 @@ const store = configureStore({
// meta.arg and meta.baseQueryMeta are defaults. payload.trade is a nonserializable return value, but that's ok
// because we are not adding it into any persisted store that requires serialization (e.g. localStorage)
ignoredActionPaths: ['meta.arg', 'meta.baseQueryMeta', 'payload.trade'],
ignoredPaths: [routingApi.reducerPath],
ignoredPaths: [routingApi.reducerPath, routingApiV2.reducerPath],
},
})
.concat(routingApi.middleware)
.concat(routingApiV2.middleware)
.concat(save({ states: PERSISTED_KEYS, debounce: 1000 })),
preloadedState: load({ states: PERSISTED_KEYS, disableWarnings: isTestEnv() }),
})
......
......@@ -9,6 +9,7 @@ import logs from './logs/slice'
import mint from './mint/reducer'
import mintV3 from './mint/v3/reducer'
import { routingApi } from './routing/slice'
import { routingApiV2 } from './routing/v2Slice'
import transactions from './transactions/reducer'
import user from './user/reducer'
import wallets from './wallets/reducer'
......@@ -27,4 +28,5 @@ export default {
lists,
logs,
[routingApi.reducerPath]: routingApi.reducer,
[routingApiV2.reducerPath]: routingApiV2.reducer,
}
import { createApi, fetchBaseQuery, FetchBaseQueryError } from '@reduxjs/toolkit/query/react'
import { Protocol } from '@uniswap/router-sdk'
import { TradeType } from '@uniswap/sdk-core'
import { AlphaRouter, ChainId } from '@uniswap/smart-order-router'
import { RPC_PROVIDERS } from 'constants/providers'
import { getClientSideQuote, toSupportedChainId } from 'lib/hooks/routing/clientSideSmartOrderRouter'
import { ChainId } from '@uniswap/smart-order-router'
import { getClientSideQuote } from 'lib/hooks/routing/clientSideSmartOrderRouter'
import ms from 'ms.macro'
import qs from 'qs'
import { trace } from 'tracing/trace'
import { QuoteData, TradeResult } from './types'
import { isExactInput, transformRoutesToTrade } from './utils'
import { getRouter, isExactInput, shouldUseAPIRouter, transformRoutesToTrade } from './utils'
export enum RouterPreference {
AUTO = 'auto',
......@@ -21,22 +20,6 @@ export enum RouterPreference {
// internally for token -> USDC trades to get a USD value.
export const INTERNAL_ROUTER_PREFERENCE_PRICE = 'price' as const
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 = RPC_PROVIDERS[supportedChainId]
const router = new AlphaRouter({ chainId, provider })
routers.set(chainId, router)
return router
}
throw new Error(`Router does not support this chain (chainId: ${chainId}).`)
}
// 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',
......@@ -44,32 +27,6 @@ const API_QUERY_PARAMS = {
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 interface GetQuoteArgs {
tokenInAddress: string
......@@ -126,7 +83,7 @@ export const routingApi = createApi({
)
},
async queryFn(args, _api, _extraOptions, fetch) {
if (args.routerPreference === RouterPreference.API || args.routerPreference === RouterPreference.AUTO) {
if (shouldUseAPIRouter(args.routerPreference)) {
try {
const { tokenInAddress, tokenInChainId, tokenOutAddress, tokenOutChainId, amount, tradeType } = args
const type = isExactInput(tradeType) ? 'exactIn' : 'exactOut'
......
......@@ -3,6 +3,8 @@ import { Currency, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core'
import { Route as V2Route } from '@uniswap/v2-sdk'
import { Route as V3Route } from '@uniswap/v3-sdk'
import { RouterPreference } from './slice'
export enum TradeState {
LOADING,
INVALID,
......@@ -67,6 +69,11 @@ export interface QuoteData {
routeString: string
}
export type QuoteDataV2 = {
routing: RouterPreference.API
quote: QuoteData
}
export class ClassicTrade<
TInput extends Currency,
TOutput extends Currency,
......
......@@ -3,10 +3,12 @@ import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { IMetric, MetricLoggerUnit, setGlobalMetric } from '@uniswap/smart-order-router'
import { sendTiming } from 'components/analytics'
import { AVERAGE_L1_BLOCK_TIME } from 'constants/chainInfo'
import { useRoutingAPIV2Enabled } from 'featureFlags/flags/unifiedRouter'
import { useRoutingAPIArguments } from 'lib/hooks/routing/useRoutingAPIArguments'
import ms from 'ms.macro'
import { useMemo } from 'react'
import { INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference, useGetQuoteQuery } from 'state/routing/slice'
import { useGetQuoteQuery as useGetQuoteQueryV2 } from 'state/routing/v2Slice'
import { InterfaceTrade, QuoteState, TradeState } from './types'
......@@ -45,17 +47,34 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
routerPreference,
})
const shouldUseRoutingApiV2 = useRoutingAPIV2Enabled()
const {
isError,
data: tradeResult,
currentData: currentTradeResult,
} = useGetQuoteQuery(queryArgs ?? skipToken, {
isError: isLegacyAPIError,
data: legacyAPITradeResult,
currentData: currentLegacyAPITradeResult,
} = useGetQuoteQuery(shouldUseRoutingApiV2 ? skipToken : queryArgs ?? skipToken, {
// Price-fetching is informational and costly, so it's done less frequently.
pollingInterval: routerPreference === INTERNAL_ROUTER_PREFERENCE_PRICE ? ms`1m` : AVERAGE_L1_BLOCK_TIME,
// If latest quote from cache was fetched > 2m ago, instantly repoll for another instead of waiting for next poll period
refetchOnMountOrArgChange: 2 * 60,
})
const {
isError: isV2APIError,
data: v2TradeResult,
currentData: currentV2TradeResult,
} = useGetQuoteQueryV2(!shouldUseRoutingApiV2 ? skipToken : queryArgs ?? skipToken, {
// Price-fetching is informational and costly, so it's done less frequently.
pollingInterval: routerPreference === INTERNAL_ROUTER_PREFERENCE_PRICE ? ms`1m` : AVERAGE_L1_BLOCK_TIME,
// If latest quote from cache was fetched > 2m ago, instantly repoll for another instead of waiting for next poll period
refetchOnMountOrArgChange: 2 * 60,
})
const tradeResult = v2TradeResult ?? legacyAPITradeResult
const currentTradeResult = currentLegacyAPITradeResult ?? currentV2TradeResult
const isError = isLegacyAPIError || isV2APIError
const isCurrent = currentTradeResult === tradeResult
return useMemo(() => {
......
import { MixedRouteSDK } from '@uniswap/router-sdk'
import { Currency, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core'
import { AlphaRouter, ChainId } from '@uniswap/smart-order-router'
import { Pair, Route as V2Route } from '@uniswap/v2-sdk'
import { FeeAmount, Pool, Route as V3Route } from '@uniswap/v3-sdk'
import { isPolygonChain } from 'constants/chains'
import { RPC_PROVIDERS } from 'constants/providers'
import { nativeOnChain } from 'constants/tokens'
import { toSupportedChainId } from 'lib/hooks/routing/clientSideSmartOrderRouter'
import { GetQuoteArgs } from './slice'
import { GetQuoteArgs, INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference } from './slice'
import {
ClassicTrade,
PoolType,
......@@ -17,6 +20,22 @@ import {
V3PoolInRoute,
} from './types'
const routers = new Map<ChainId, AlphaRouter>()
export function getRouter(chainId: ChainId): AlphaRouter {
const router = routers.get(chainId)
if (router) return router
const supportedChainId = toSupportedChainId(chainId)
if (supportedChainId) {
const provider = RPC_PROVIDERS[supportedChainId]
const router = new AlphaRouter({ chainId, provider })
routers.set(chainId, router)
return router
}
throw new Error(`Router does not support this chain (chainId: ${chainId}).`)
}
/**
* Transforms a Routing API quote into an array of routes that can be used to
* create a `Trade`.
......@@ -166,3 +185,9 @@ export function currencyAddressForSwapQuote(currency: Currency): string {
return currency.address
}
export function shouldUseAPIRouter(
routerPreference?: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE
): boolean {
return routerPreference === RouterPreference.API || routerPreference === RouterPreference.AUTO
}
import { createApi, fetchBaseQuery, FetchBaseQueryError } from '@reduxjs/toolkit/query/react'
import { Protocol } from '@uniswap/router-sdk'
import { getClientSideQuote } from 'lib/hooks/routing/clientSideSmartOrderRouter'
import ms from 'ms.macro'
import { trace } from 'tracing/trace'
import { GetQuoteArgs, INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference } from './slice'
import { QuoteDataV2, QuoteState, TradeResult } from './types'
import { getRouter, isExactInput, shouldUseAPIRouter, transformRoutesToTrade } from './utils'
const CLIENT_PARAMS = {
protocols: [Protocol.V2, Protocol.V3, Protocol.MIXED],
}
// routing API quote query params: https://github.com/Uniswap/routing-api/blob/main/lib/handlers/quote/schema/quote-schema.ts
const CLASSIC_SWAP_QUERY_PARAMS = {
...CLIENT_PARAMS,
routingType: 'CLASSIC',
}
export const routingApiV2 = createApi({
reducerPath: 'routingApiV2',
baseQuery: fetchBaseQuery({
baseUrl: 'https://api.uniswap.org/v2/',
}),
endpoints: (build) => ({
getQuote: build.query<TradeResult, GetQuoteArgs>({
async onQueryStarted(args: GetQuoteArgs, { queryFulfilled }) {
trace(
'quote-v2',
async ({ setTraceError, setTraceStatus }) => {
try {
await queryFulfilled
} catch (error: unknown) {
if (error && typeof error === 'object' && 'error' in error) {
const queryError = (error as Record<'error', FetchBaseQueryError>).error
if (typeof queryError.status === 'number') {
setTraceStatus(queryError.status)
}
setTraceError(queryError)
} else {
throw error
}
}
},
{
data: {
...args,
isPrice: args.routerPreference === INTERNAL_ROUTER_PREFERENCE_PRICE,
isAutoRouter:
args.routerPreference === RouterPreference.AUTO || args.routerPreference === RouterPreference.API,
},
}
)
},
async queryFn(args: GetQuoteArgs, _api, _extraOptions, fetch) {
const routerPreference = args.routerPreference
if (shouldUseAPIRouter(routerPreference)) {
try {
const { tokenInAddress, tokenInChainId, tokenOutAddress, tokenOutChainId, amount, tradeType } = args
const type = isExactInput(tradeType) ? 'EXACT_INPUT' : 'EXACT_OUTPUT'
const requestBody = {
tokenInChainId,
tokenIn: tokenInAddress,
tokenOutChainId,
tokenOut: tokenOutAddress,
amount,
type,
configs: [CLASSIC_SWAP_QUERY_PARAMS],
}
const response = await fetch({
method: 'POST',
url: '/quote',
body: JSON.stringify(requestBody),
})
if (response.error) {
try {
// cast as any here because we do a runtime check on it being an object before indexing into .errorCode
const errorData = response.error.data as any
// NO_ROUTE should be treated as a valid response to prevent retries.
if (typeof errorData === 'object' && errorData?.errorCode === 'NO_ROUTE') {
return { data: { state: QuoteState.NOT_FOUND } }
}
} catch {
throw response.error
}
}
const quoteData = response.data as QuoteDataV2
const tradeResult = transformRoutesToTrade(args, quoteData.quote)
return { data: tradeResult }
} catch (error: any) {
console.warn(
`GetQuote failed on API v2, falling back to client: ${error?.message ?? error?.detail ?? error}`
)
}
}
try {
const router = getRouter(args.tokenInChainId)
const quoteResult = await getClientSideQuote(args, router, CLIENT_PARAMS)
if (quoteResult.state === QuoteState.SUCCESS) {
return { data: transformRoutesToTrade(args, quoteResult.data) }
} else {
return { data: quoteResult }
}
} catch (error: any) {
console.warn(`GetQuote failed on client: ${error}`)
return { error: { status: 'CUSTOM_ERROR', error: error?.detail ?? error?.message ?? error } }
}
},
keepUnusedDataFor: ms`10s`,
}),
}),
})
export const { useGetQuoteQuery } = routingApiV2
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