feat: usePoll (#3530)
* feat: usePoll * chore: comments * fix: cleanup * fix: review updates * fix: lint nits
This commit is contained in:
parent
7c88a5a008
commit
828bf540ba
@ -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
|
||||
}
|
||||
|
@ -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<TTradeType extends Tr
|
||||
useClientSideRouter: true,
|
||||
})
|
||||
const params = useMemo(() => 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<TTradeType extends Tr
|
||||
}
|
||||
|
||||
// Returns the last trade state while syncing/loading to avoid jank from clearing the last trade while loading.
|
||||
if (isDebouncing) {
|
||||
return { state: TradeState.SYNCING, trade }
|
||||
} else if (loading) {
|
||||
return { state: TradeState.LOADING, trade }
|
||||
if (!quoteResult && !error) {
|
||||
if (isDebouncing) {
|
||||
return { state: TradeState.SYNCING, trade }
|
||||
} else {
|
||||
return { state: TradeState.LOADING, trade }
|
||||
}
|
||||
}
|
||||
|
||||
let otherAmount = undefined
|
||||
@ -156,5 +133,5 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
|
||||
return { state: TradeState.VALID, trade }
|
||||
}
|
||||
return { state: TradeState.INVALID, trade: undefined }
|
||||
}, [currencyIn, currencyOut, isDebouncing, loading, quoteResult, error, route, queryArgs, trade, tradeType])
|
||||
}, [currencyIn, currencyOut, quoteResult, error, route, queryArgs, trade, isDebouncing, tradeType])
|
||||
}
|
||||
|
59
src/lib/hooks/usePoll.ts
Normal file
59
src/lib/hooks/usePoll.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import ms from 'ms.macro'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
const DEFAULT_POLLING_INTERVAL = ms`15s`
|
||||
const DEFAULT_KEEP_UNUSED_DATA_FOR = ms`10s`
|
||||
|
||||
export default function usePoll<T>(
|
||||
fetch: () => Promise<T>,
|
||||
key = '',
|
||||
pollingInterval = DEFAULT_POLLING_INTERVAL,
|
||||
keepUnusedDataFor = DEFAULT_KEEP_UNUSED_DATA_FOR
|
||||
): T | undefined {
|
||||
const cache = useMemo(() => new Map<string, { ttl: number; result?: T }>(), [])
|
||||
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
|
||||
}
|
@ -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<T>(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<TTradeType extends TradeType>(
|
||||
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),
|
||||
|
Loading…
Reference in New Issue
Block a user