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

fix: use a min fresh block (#3568)

* chore: mv useFilterFresh to its own hook

* fix: use a minimum fresh block

* fix: re-poll on stale data

* chore: rename to staleCallback

* check for undefined

* chore: rename fresh->valid
Co-authored-by: default avatarianlapham <ianlapham@gmail.com>
parent f5087880
...@@ -13,6 +13,7 @@ import { useSwapCallback } from 'lib/hooks/swap/useSwapCallback' ...@@ -13,6 +13,7 @@ import { useSwapCallback } from 'lib/hooks/swap/useSwapCallback'
import useWrapCallback, { WrapError, WrapType } from 'lib/hooks/swap/useWrapCallback' import useWrapCallback, { WrapError, WrapType } from 'lib/hooks/swap/useWrapCallback'
import { useAddTransaction, usePendingApproval } from 'lib/hooks/transactions' import { useAddTransaction, usePendingApproval } from 'lib/hooks/transactions'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React' import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import { useSetOldestValidBlock } from 'lib/hooks/useIsValidBlock'
import useTransactionDeadline from 'lib/hooks/useTransactionDeadline' import useTransactionDeadline from 'lib/hooks/useTransactionDeadline'
import { Spinner } from 'lib/icons' import { Spinner } from 'lib/icons'
import { displayTxHashAtom, feeOptionsAtom, Field } from 'lib/state/swap' import { displayTxHashAtom, feeOptionsAtom, Field } from 'lib/state/swap'
...@@ -177,6 +178,7 @@ export default memo(function SwapButton({ disabled }: SwapButtonProps) { ...@@ -177,6 +178,7 @@ export default memo(function SwapButton({ disabled }: SwapButtonProps) {
//@TODO(ianlapham): add a loading state, process errors //@TODO(ianlapham): add a loading state, process errors
const setDisplayTxHash = useUpdateAtom(displayTxHashAtom) const setDisplayTxHash = useUpdateAtom(displayTxHashAtom)
const setOldestValidBlock = useSetOldestValidBlock()
const onConfirm = useCallback(() => { const onConfirm = useCallback(() => {
swapCallback?.() swapCallback?.()
.then((response) => { .then((response) => {
...@@ -189,6 +191,12 @@ export default memo(function SwapButton({ disabled }: SwapButtonProps) { ...@@ -189,6 +191,12 @@ export default memo(function SwapButton({ disabled }: SwapButtonProps) {
inputCurrencyAmount: inputTradeCurrencyAmount, inputCurrencyAmount: inputTradeCurrencyAmount,
outputCurrencyAmount: outputTradeCurrencyAmount, outputCurrencyAmount: outputTradeCurrencyAmount,
}) })
// Set the block containing the response to the oldest valid block to ensure that the
// completed trade's impact is reflected in future fetched trades.
response.wait(1).then((receipt) => {
setOldestValidBlock(receipt.blockNumber)
})
}) })
.catch((error) => { .catch((error) => {
//@TODO(ianlapham): add error handling //@TODO(ianlapham): add error handling
...@@ -197,7 +205,15 @@ export default memo(function SwapButton({ disabled }: SwapButtonProps) { ...@@ -197,7 +205,15 @@ export default memo(function SwapButton({ disabled }: SwapButtonProps) {
.finally(() => { .finally(() => {
setActiveTrade(undefined) setActiveTrade(undefined)
}) })
}, [addTransaction, inputTradeCurrencyAmount, outputTradeCurrencyAmount, setDisplayTxHash, swapCallback, tradeType]) }, [
addTransaction,
inputTradeCurrencyAmount,
outputTradeCurrencyAmount,
setDisplayTxHash,
setOldestValidBlock,
swapCallback,
tradeType,
])
const ButtonText = useCallback(() => { const ButtonText = useCallback(() => {
if ((wrapType === WrapType.WRAP || wrapType === WrapType.UNWRAP) && wrapError !== WrapError.NO_ERROR) { if ((wrapType === WrapType.WRAP || wrapType === WrapType.UNWRAP) && wrapError !== WrapError.NO_ERROR) {
......
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'
...@@ -99,14 +98,3 @@ export async function getClientSideQuote( ...@@ -99,14 +98,3 @@ export async function getClientSideQuote(
routerConfig routerConfig
) )
} }
/** Used to keep quotes up to date given a certain block age. Returns undefined if past limit. */
export function useFilterFreshQuote(
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
}
...@@ -10,8 +10,9 @@ import { computeRoutes, transformRoutesToTrade } from 'state/routing/utils' ...@@ -10,8 +10,9 @@ 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 { useGetIsValidBlock } from '../useIsValidBlock'
import usePoll from '../usePoll' import usePoll from '../usePoll'
import { getClientSideQuote, useFilterFreshQuote } from './clientSideSmartOrderRouter' import { getClientSideQuote } from './clientSideSmartOrderRouter'
import { useRoutingAPIArguments } from './useRoutingAPIArguments' import { useRoutingAPIArguments } from './useRoutingAPIArguments'
/** /**
...@@ -80,11 +81,13 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr ...@@ -80,11 +81,13 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
} }
}, [config, params, queryArgs, wrapType]) }, [config, params, queryArgs, wrapType])
const { data, error } = usePoll(getQuoteResult, JSON.stringify(queryArgs), isDebouncing) ?? { const getIsValidBlock = useGetIsValidBlock()
const { data: quoteResult, error } = usePoll(getQuoteResult, JSON.stringify(queryArgs), {
debounce: isDebouncing,
staleCallback: useCallback(({ data }) => getIsValidBlock(Number(data?.blockNumber) || 0), [getIsValidBlock]),
}) ?? {
error: undefined, error: undefined,
} }
const quoteResult = useFilterFreshQuote(data)
const isLoading = !quoteResult const isLoading = !quoteResult
const route = useMemo( const route = useMemo(
......
import { atomWithImmer } from 'jotai/immer'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { useCallback } from 'react'
import useActiveWeb3React from './useActiveWeb3React'
import useBlockNumber from './useBlockNumber'
// The oldest block (per chain) to be considered valid.
const oldestBlockMapAtom = atomWithImmer<{ [chainId: number]: number }>({})
const DEFAULT_MAX_BLOCK_AGE = 10
export function useSetOldestValidBlock(): (block: number) => void {
const { chainId } = useActiveWeb3React()
const updateValidBlock = useUpdateAtom(oldestBlockMapAtom)
return useCallback(
(block: number) => {
if (!chainId) return
updateValidBlock((oldestBlockMap) => {
oldestBlockMap[chainId] = Math.max(block, oldestBlockMap[chainId] || 0)
})
},
[chainId, updateValidBlock]
)
}
export function useGetIsValidBlock(maxBlockAge = DEFAULT_MAX_BLOCK_AGE): (block: number) => boolean {
const { chainId } = useActiveWeb3React()
const currentBlock = useBlockNumber()
const oldestBlockMap = useAtomValue(oldestBlockMapAtom)
const oldestBlock = chainId ? oldestBlockMap[chainId] : 0
return useCallback(
(block: number) => {
if (!currentBlock) return false
if (currentBlock - block > maxBlockAge) return false
if (currentBlock < oldestBlock) return false
return true
},
[currentBlock, maxBlockAge, oldestBlock]
)
}
export default function useIsValidBlock(block: number): boolean {
return useGetIsValidBlock()(block)
}
...@@ -4,23 +4,38 @@ import { useEffect, useMemo, useState } from 'react' ...@@ -4,23 +4,38 @@ import { useEffect, useMemo, useState } from 'react'
const DEFAULT_POLLING_INTERVAL = ms`15s` const DEFAULT_POLLING_INTERVAL = ms`15s`
const DEFAULT_KEEP_UNUSED_DATA_FOR = ms`10s` const DEFAULT_KEEP_UNUSED_DATA_FOR = ms`10s`
interface PollingOptions<T> {
// If true, any cached result will be returned, but no new fetch will be initiated.
debounce?: boolean
// If stale, a result will not be returned, and a new fetch will be immediately initiated.
staleCallback?: (value: T) => boolean
pollingInterval?: number
keepUnusedDataFor?: number
}
export default function usePoll<T>( export default function usePoll<T>(
fetch: () => Promise<T>, fetch: () => Promise<T>,
key = '', key = '',
check = false, // set to true to check the cache without initiating a new request {
pollingInterval = DEFAULT_POLLING_INTERVAL, debounce = false,
keepUnusedDataFor = DEFAULT_KEEP_UNUSED_DATA_FOR staleCallback,
pollingInterval = DEFAULT_POLLING_INTERVAL,
keepUnusedDataFor = DEFAULT_KEEP_UNUSED_DATA_FOR,
}: PollingOptions<T>
): T | undefined { ): T | undefined {
const cache = useMemo(() => new Map<string, { ttl: number; result?: T }>(), []) const cache = useMemo(() => new Map<string, { ttl: number; result?: T }>(), [])
const [, setData] = useState<{ key: string; result?: T }>({ key }) const [, setData] = useState<{ key: string; result?: T }>({ key })
useEffect(() => { useEffect(() => {
if (check) return if (debounce) return
let timeout: number let timeout: number
const entry = cache.get(key) const entry = cache.get(key)
if (entry && entry.ttl + keepUnusedDataFor > Date.now()) { const isStale = staleCallback && entry?.result !== undefined ? staleCallback(entry.result) : false
if (entry && entry.ttl + keepUnusedDataFor > Date.now() && !isStale) {
// If there is a fresh entry, return it and queue the next poll. // If there is a fresh entry, return it and queue the next poll.
setData({ key, result: entry.result }) setData({ key, result: entry.result })
timeout = setTimeout(poll, Math.max(0, entry.ttl - Date.now())) timeout = setTimeout(poll, Math.max(0, entry.ttl - Date.now()))
...@@ -44,7 +59,7 @@ export default function usePoll<T>( ...@@ -44,7 +59,7 @@ export default function usePoll<T>(
return data.key === key ? { key, result } : data return data.key === key ? { key, result } : data
}) })
} }
}, [cache, check, fetch, keepUnusedDataFor, key, pollingInterval]) }, [cache, debounce, fetch, keepUnusedDataFor, key, pollingInterval, staleCallback])
useEffect(() => { useEffect(() => {
// Cleanup stale entries when a new key is used. // Cleanup stale entries when a new key is used.
......
...@@ -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 { useFilterFreshQuote } from 'lib/hooks/routing/clientSideSmartOrderRouter'
import { useRoutingAPIArguments } from 'lib/hooks/routing/useRoutingAPIArguments' import { useRoutingAPIArguments } from 'lib/hooks/routing/useRoutingAPIArguments'
import useIsValidBlock from 'lib/hooks/useIsValidBlock'
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'
...@@ -50,7 +50,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>( ...@@ -50,7 +50,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
refetchOnFocus: true, refetchOnFocus: true,
}) })
const quoteResult: GetQuoteResult | undefined = useFilterFreshQuote(data) const quoteResult: GetQuoteResult | undefined = useIsValidBlock(Number(data?.blockNumber) || 0) ? data : undefined
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