diff --git a/src/lib/hooks/routing/clientSideSmartOrderRouter.ts b/src/lib/hooks/routing/clientSideSmartOrderRouter.ts index 9f28367082..e35ed4fe75 100644 --- a/src/lib/hooks/routing/clientSideSmartOrderRouter.ts +++ b/src/lib/hooks/routing/clientSideSmartOrderRouter.ts @@ -1,6 +1,7 @@ import { BigintIsh, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core' import { AlphaRouter, AlphaRouterConfig, AlphaRouterParams, ChainId } from '@uniswap/smart-order-router' import JSBI from 'jsbi' +import useBlockNumber from 'lib/hooks/useBlockNumber' import { GetQuoteResult } from 'state/routing/types' import { transformSwapRouteToGetQuoteResult } from 'utils/transformSwapRouteToGetQuoteResult' @@ -98,3 +99,10 @@ export async function getClientSideQuote( routerConfig ) } + +export function useFreshQuote(quoteResult: GetQuoteResult | undefined, maxBlockAge = 10): GetQuoteResult | undefined { + const block = useBlockNumber() + if (!block || !quoteResult) return undefined + if (block - (Number(quoteResult.blockNumber) || 0) > maxBlockAge) return undefined + return quoteResult +} diff --git a/src/lib/hooks/routing/useClientSideSmartOrderRouterTrade.ts b/src/lib/hooks/routing/useClientSideSmartOrderRouterTrade.ts index aea5406936..8a1cee85d4 100644 --- a/src/lib/hooks/routing/useClientSideSmartOrderRouterTrade.ts +++ b/src/lib/hooks/routing/useClientSideSmartOrderRouterTrade.ts @@ -3,13 +3,14 @@ import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' import { ChainId } from '@uniswap/smart-order-router' import useDebounce from 'hooks/useDebounce' import { useStablecoinAmountFromFiatValue } from 'hooks/useUSDCPrice' -import { useEffect, useMemo, useState } from 'react' +import { useCallback, useMemo } from 'react' import { GetQuoteResult, InterfaceTrade, TradeState } from 'state/routing/types' import { computeRoutes, transformRoutesToTrade } from 'state/routing/utils' import useWrapCallback, { WrapType } from '../swap/useWrapCallback' import useActiveWeb3React from '../useActiveWeb3React' -import { getClientSideQuote } from './clientSideSmartOrderRouter' +import usePoll from '../usePoll' +import { getClientSideQuote, useFreshQuote } from './clientSideSmartOrderRouter' import { useRoutingAPIArguments } from './useRoutingAPIArguments' /** @@ -23,14 +24,11 @@ const DistributionPercents: { [key: number]: number } = { [ChainId.ARBITRUM_ONE]: 25, [ChainId.ARBITRUM_RINKEBY]: 25, } - const DEFAULT_DISTRIBUTION_PERCENT = 10 - function getConfig(chainId: ChainId | undefined) { return { // Limit to only V2 and V3. protocols: [Protocol.V2, Protocol.V3], - distributionPercent: (chainId && DistributionPercents[chainId]) ?? DEFAULT_DISTRIBUTION_PERCENT, } } @@ -68,45 +66,22 @@ export default function useClientSideSmartOrderRouterTrade chainId && library && { chainId, provider: library }, [chainId, library]) - - const [loading, setLoading] = useState(false) - const [{ data: quoteResult, error }, setResult] = useState<{ - data?: GetQuoteResult - error?: unknown - }>({ error: undefined }) const config = useMemo(() => getConfig(chainId), [chainId]) const { type: wrapType } = useWrapCallback() - // When arguments update, make a new call to SOR for updated quote - useEffect(() => { - if (wrapType !== WrapType.NOT_APPLICABLE) { - return + const getQuoteResult = useCallback(async (): Promise<{ data?: GetQuoteResult; error?: unknown }> => { + if (wrapType !== WrapType.NOT_APPLICABLE) return { error: undefined } + if (!queryArgs || !params) return { error: undefined } + try { + return await getClientSideQuote(queryArgs, params, config) + } catch { + return { error: true } } - setLoading(true) - if (isDebouncing) return - - let stale = false - fetchQuote() - return () => { - stale = true - setLoading(false) - } - - async function fetchQuote() { - if (queryArgs && params) { - let result - try { - result = await getClientSideQuote(queryArgs, params, config) - } catch { - result = { error: true } - } - if (!stale) { - setResult(result) - setLoading(false) - } - } - } - }, [queryArgs, params, config, isDebouncing, wrapType]) + }, [config, params, queryArgs, wrapType]) + const { data, error } = usePoll(getQuoteResult, JSON.stringify(queryArgs)) ?? { + error: undefined, + } + const quoteResult = useFreshQuote(data) const route = useMemo( () => computeRoutes(currencyIn, currencyOut, tradeType, quoteResult), @@ -130,10 +105,12 @@ export default function useClientSideSmartOrderRouterTrade( + fetch: () => Promise, + key = '', + pollingInterval = DEFAULT_POLLING_INTERVAL, + keepUnusedDataFor = DEFAULT_KEEP_UNUSED_DATA_FOR +): T | undefined { + const cache = useMemo(() => new Map(), []) + const [data, setData] = useState<{ key: string; result?: T }>({ key }) + + useEffect(() => { + let timeout: number + + const entry = cache.get(key) + if (entry && entry.ttl + keepUnusedDataFor > Date.now()) { + // If there is a fresh entry, return it and queue the next poll. + setData({ key, result: entry.result }) + timeout = setTimeout(poll, Math.max(0, entry.ttl - Date.now())) + } else { + // Otherwise, set a new entry (to avoid duplicate polling) and trigger a poll immediately. + cache.set(key, { ttl: Date.now() + pollingInterval }) + setData({ key }) + poll() + } + + return () => { + clearTimeout(timeout) + } + + async function poll(ttl = Date.now() + pollingInterval) { + timeout = setTimeout(poll, pollingInterval) + const result = await fetch() + // Always set the result in the cache, but only set it as data if the key is still being queried. + cache.set(key, { ttl, result }) + setData((data) => { + return data.key === key ? { key, result } : data + }) + } + }, [cache, fetch, keepUnusedDataFor, key, pollingInterval]) + + useEffect(() => { + // Cleanup stale entries when a new key is used. + void key + + const now = Date.now() + cache.forEach(({ ttl }, key) => { + if (ttl + keepUnusedDataFor <= now) { + cache.delete(key) + } + }) + }, [cache, keepUnusedDataFor, key]) + + return data.result +} diff --git a/src/state/routing/useRoutingAPITrade.ts b/src/state/routing/useRoutingAPITrade.ts index 5b5b5f40d9..bebd87679a 100644 --- a/src/state/routing/useRoutingAPITrade.ts +++ b/src/state/routing/useRoutingAPITrade.ts @@ -2,8 +2,8 @@ import { skipToken } from '@reduxjs/toolkit/query/react' import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' import { IMetric, MetricLoggerUnit, setGlobalMetric } from '@uniswap/smart-order-router' import { useStablecoinAmountFromFiatValue } from 'hooks/useUSDCPrice' +import { useFreshQuote } from 'lib/hooks/routing/clientSideSmartOrderRouter' import { useRoutingAPIArguments } from 'lib/hooks/routing/useRoutingAPIArguments' -import useBlockNumber from 'lib/hooks/useBlockNumber' import ms from 'ms.macro' import { useMemo } from 'react' import ReactGA from 'react-ga' @@ -13,17 +13,6 @@ import { useClientSideRouter } from 'state/user/hooks' import { GetQuoteResult, InterfaceTrade, TradeState } from './types' import { computeRoutes, transformRoutesToTrade } from './utils' -function useFreshData(data: T, dataBlockNumber: number, maxBlockAge = 10): T | undefined { - const localBlockNumber = useBlockNumber() - - if (!localBlockNumber) return undefined - if (localBlockNumber - dataBlockNumber > maxBlockAge) { - return undefined - } - - return data -} - /** * 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 @@ -61,7 +50,7 @@ export function useRoutingAPITrade( refetchOnFocus: true, }) - const quoteResult: GetQuoteResult | undefined = useFreshData(data, Number(data?.blockNumber) || 0) + const quoteResult: GetQuoteResult | undefined = useFreshQuote(data) const route = useMemo( () => computeRoutes(currencyIn, currencyOut, tradeType, quoteResult),