Commit 828bf540 authored by Zach Pomerantz's avatar Zach Pomerantz Committed by GitHub

feat: usePoll (#3530)

* feat: usePoll

* chore: comments

* fix: cleanup

* fix: review updates

* fix: lint nits
parent 7c88a5a0
import { BigintIsh, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core' import { BigintIsh, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core'
import { AlphaRouter, AlphaRouterConfig, AlphaRouterParams, ChainId } from '@uniswap/smart-order-router' import { AlphaRouter, AlphaRouterConfig, AlphaRouterParams, ChainId } from '@uniswap/smart-order-router'
import JSBI from 'jsbi' import JSBI from 'jsbi'
import useBlockNumber from 'lib/hooks/useBlockNumber'
import { GetQuoteResult } from 'state/routing/types' import { GetQuoteResult } from 'state/routing/types'
import { transformSwapRouteToGetQuoteResult } from 'utils/transformSwapRouteToGetQuoteResult' import { transformSwapRouteToGetQuoteResult } from 'utils/transformSwapRouteToGetQuoteResult'
...@@ -98,3 +99,10 @@ export async function getClientSideQuote( ...@@ -98,3 +99,10 @@ export async function getClientSideQuote(
routerConfig 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' ...@@ -3,13 +3,14 @@ import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { ChainId } from '@uniswap/smart-order-router' import { ChainId } from '@uniswap/smart-order-router'
import useDebounce from 'hooks/useDebounce' import useDebounce from 'hooks/useDebounce'
import { useStablecoinAmountFromFiatValue } from 'hooks/useUSDCPrice' 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 { GetQuoteResult, InterfaceTrade, TradeState } from 'state/routing/types'
import { computeRoutes, transformRoutesToTrade } from 'state/routing/utils' import { computeRoutes, transformRoutesToTrade } from 'state/routing/utils'
import useWrapCallback, { WrapType } from '../swap/useWrapCallback' import useWrapCallback, { WrapType } from '../swap/useWrapCallback'
import useActiveWeb3React from '../useActiveWeb3React' import useActiveWeb3React from '../useActiveWeb3React'
import { getClientSideQuote } from './clientSideSmartOrderRouter' import usePoll from '../usePoll'
import { getClientSideQuote, useFreshQuote } from './clientSideSmartOrderRouter'
import { useRoutingAPIArguments } from './useRoutingAPIArguments' import { useRoutingAPIArguments } from './useRoutingAPIArguments'
/** /**
...@@ -23,14 +24,11 @@ const DistributionPercents: { [key: number]: number } = { ...@@ -23,14 +24,11 @@ const DistributionPercents: { [key: number]: number } = {
[ChainId.ARBITRUM_ONE]: 25, [ChainId.ARBITRUM_ONE]: 25,
[ChainId.ARBITRUM_RINKEBY]: 25, [ChainId.ARBITRUM_RINKEBY]: 25,
} }
const DEFAULT_DISTRIBUTION_PERCENT = 10 const DEFAULT_DISTRIBUTION_PERCENT = 10
function getConfig(chainId: ChainId | undefined) { function getConfig(chainId: ChainId | undefined) {
return { return {
// Limit to only V2 and V3. // Limit to only V2 and V3.
protocols: [Protocol.V2, Protocol.V3], protocols: [Protocol.V2, Protocol.V3],
distributionPercent: (chainId && DistributionPercents[chainId]) ?? DEFAULT_DISTRIBUTION_PERCENT, distributionPercent: (chainId && DistributionPercents[chainId]) ?? DEFAULT_DISTRIBUTION_PERCENT,
} }
} }
...@@ -68,45 +66,22 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr ...@@ -68,45 +66,22 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
useClientSideRouter: true, useClientSideRouter: true,
}) })
const params = useMemo(() => chainId && library && { chainId, provider: library }, [chainId, library]) 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 config = useMemo(() => getConfig(chainId), [chainId])
const { type: wrapType } = useWrapCallback() const { type: wrapType } = useWrapCallback()
// When arguments update, make a new call to SOR for updated quote const getQuoteResult = useCallback(async (): Promise<{ data?: GetQuoteResult; error?: unknown }> => {
useEffect(() => { if (wrapType !== WrapType.NOT_APPLICABLE) return { error: undefined }
if (wrapType !== WrapType.NOT_APPLICABLE) { if (!queryArgs || !params) return { error: undefined }
return try {
} return await getClientSideQuote(queryArgs, params, config)
setLoading(true) } catch {
if (isDebouncing) return return { error: true }
let stale = false
fetchQuote()
return () => {
stale = true
setLoading(false)
} }
}, [config, params, queryArgs, wrapType])
async function fetchQuote() { const { data, error } = usePoll(getQuoteResult, JSON.stringify(queryArgs)) ?? {
if (queryArgs && params) { error: undefined,
let result }
try { const quoteResult = useFreshQuote(data)
result = await getClientSideQuote(queryArgs, params, config)
} catch {
result = { error: true }
}
if (!stale) {
setResult(result)
setLoading(false)
}
}
}
}, [queryArgs, params, config, isDebouncing, wrapType])
const route = useMemo( const route = useMemo(
() => computeRoutes(currencyIn, currencyOut, tradeType, quoteResult), () => computeRoutes(currencyIn, currencyOut, tradeType, quoteResult),
...@@ -130,10 +105,12 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr ...@@ -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. // Returns the last trade state while syncing/loading to avoid jank from clearing the last trade while loading.
if (isDebouncing) { if (!quoteResult && !error) {
return { state: TradeState.SYNCING, trade } if (isDebouncing) {
} else if (loading) { return { state: TradeState.SYNCING, trade }
return { state: TradeState.LOADING, trade } } else {
return { state: TradeState.LOADING, trade }
}
} }
let otherAmount = undefined let otherAmount = undefined
...@@ -156,5 +133,5 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr ...@@ -156,5 +133,5 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
return { state: TradeState.VALID, trade } return { state: TradeState.VALID, trade }
} }
return { state: TradeState.INVALID, trade: undefined } 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])
} }
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' ...@@ -2,8 +2,8 @@ import { skipToken } from '@reduxjs/toolkit/query/react'
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { IMetric, MetricLoggerUnit, setGlobalMetric } from '@uniswap/smart-order-router' import { IMetric, MetricLoggerUnit, setGlobalMetric } from '@uniswap/smart-order-router'
import { useStablecoinAmountFromFiatValue } from 'hooks/useUSDCPrice' import { useStablecoinAmountFromFiatValue } from 'hooks/useUSDCPrice'
import { useFreshQuote } from 'lib/hooks/routing/clientSideSmartOrderRouter'
import { useRoutingAPIArguments } from 'lib/hooks/routing/useRoutingAPIArguments' import { useRoutingAPIArguments } from 'lib/hooks/routing/useRoutingAPIArguments'
import useBlockNumber from 'lib/hooks/useBlockNumber'
import ms from 'ms.macro' import ms from 'ms.macro'
import { useMemo } from 'react' import { useMemo } from 'react'
import ReactGA from 'react-ga' import ReactGA from 'react-ga'
...@@ -13,17 +13,6 @@ import { useClientSideRouter } from 'state/user/hooks' ...@@ -13,17 +13,6 @@ import { useClientSideRouter } from 'state/user/hooks'
import { GetQuoteResult, InterfaceTrade, TradeState } from './types' import { GetQuoteResult, InterfaceTrade, TradeState } from './types'
import { computeRoutes, transformRoutesToTrade } from './utils' 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 * 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 * @param tradeType whether the swap is an exact in/out
...@@ -61,7 +50,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>( ...@@ -61,7 +50,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
refetchOnFocus: true, refetchOnFocus: true,
}) })
const quoteResult: GetQuoteResult | undefined = useFreshData(data, Number(data?.blockNumber) || 0) const quoteResult: GetQuoteResult | undefined = useFreshQuote(data)
const route = useMemo( const route = useMemo(
() => computeRoutes(currencyIn, currencyOut, tradeType, quoteResult), () => computeRoutes(currencyIn, currencyOut, tradeType, quoteResult),
......
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