fix: skip all quote / pricing requests if window is not visible (#7554)

* skip all quote / pricing requests if window is not visible

* add unit tests

* add ts-ignore comment
This commit is contained in:
Tina 2023-11-08 15:14:50 -05:00 committed by GitHub
parent 418ee08b00
commit cee3390b71
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 151 additions and 84 deletions

@ -1,79 +0,0 @@
import { renderHook } from '@testing-library/react'
import { CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { DAI, USDC_MAINNET } from 'constants/tokens'
import { RouterPreference, TradeState } from 'state/routing/types'
import { usePreviewTrade } from 'state/routing/usePreviewTrade'
import { useRouterPreference } from 'state/user/hooks'
import { mocked } from 'test-utils/mocked'
import { useRoutingAPITrade } from '../state/routing/useRoutingAPITrade'
import useAutoRouterSupported from './useAutoRouterSupported'
import useDebounce from './useDebounce'
import { useDebouncedTrade } from './useDebouncedTrade'
import useIsWindowVisible from './useIsWindowVisible'
const USDCAmount = CurrencyAmount.fromRawAmount(USDC_MAINNET, '10000')
const DAIAmount = CurrencyAmount.fromRawAmount(DAI, '10000')
jest.mock('./useAutoRouterSupported')
jest.mock('./useDebounce')
jest.mock('./useIsWindowVisible')
jest.mock('state/routing/useRoutingAPITrade')
jest.mock('state/routing/usePreviewTrade')
jest.mock('state/user/hooks')
// helpers to set mock expectations
const expectRouterMock = (state: TradeState) => {
mocked(useRoutingAPITrade).mockReturnValue({ state, trade: undefined })
mocked(usePreviewTrade).mockReturnValue({ state, trade: undefined })
}
beforeEach(() => {
// ignore debounced value
mocked(useDebounce).mockImplementation((value) => value)
mocked(useIsWindowVisible).mockReturnValue(true)
mocked(useAutoRouterSupported).mockReturnValue(true)
mocked(useRouterPreference).mockReturnValue([RouterPreference.API, () => undefined])
})
describe('#useBestV3Trade ExactIn', () => {
it('does not compute routing api trade when window is not focused', async () => {
mocked(useIsWindowVisible).mockReturnValue(false)
expectRouterMock(TradeState.NO_ROUTE_FOUND)
const { result } = renderHook(() => useDebouncedTrade(TradeType.EXACT_INPUT, USDCAmount, DAI))
expect(useRoutingAPITrade).toHaveBeenCalledWith(
/* skipFetch = */ true,
TradeType.EXACT_INPUT,
USDCAmount,
DAI,
RouterPreference.API,
/* account = */ undefined,
/* inputTax = */ undefined,
/* outputTax = */ undefined
)
expect(result.current).toEqual({ state: TradeState.NO_ROUTE_FOUND, trade: undefined })
})
})
describe('#useDebouncedTrade ExactOut', () => {
it('does not compute routing api trade when window is not focused', () => {
mocked(useIsWindowVisible).mockReturnValue(false)
expectRouterMock(TradeState.NO_ROUTE_FOUND)
const { result } = renderHook(() => useDebouncedTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
expect(useRoutingAPITrade).toHaveBeenCalledWith(
/* skipFetch = */ true,
TradeType.EXACT_OUTPUT,
DAIAmount,
USDC_MAINNET,
RouterPreference.API,
/* account = */ undefined,
/* inputTax = */ undefined,
/* outputTax = */ undefined
)
expect(result.current).toEqual({ state: TradeState.NO_ROUTE_FOUND, trade: undefined })
})
})

@ -10,7 +10,6 @@ import { useRouterPreference } from 'state/user/hooks'
import useAutoRouterSupported from './useAutoRouterSupported'
import useDebounce from './useDebounce'
import useIsWindowVisible from './useIsWindowVisible'
// Prevents excessive quote requests between keystrokes.
const DEBOUNCE_TIME = 350
@ -71,7 +70,6 @@ export function useDebouncedTrade(
} {
const { chainId } = useWeb3React()
const autoRouterSupported = useAutoRouterSupported()
const isWindowVisible = useIsWindowVisible()
const inputs = useMemo<[CurrencyAmount<Currency> | undefined, Currency | undefined]>(
() => [amountSpecified, otherCurrency],
@ -94,7 +92,7 @@ export function useDebouncedTrade(
const [routerPreference] = useRouterPreference()
const skipBothFetches = !autoRouterSupported || !isWindowVisible || isWrap
const skipBothFetches = !autoRouterSupported || isWrap
const skipRoutingFetch = skipBothFetches || isDebouncing
const skipPreviewTradeFetch = skipBothFetches || isPreviewTradeDebouncing

@ -2,6 +2,7 @@ import { skipToken } from '@reduxjs/toolkit/query/react'
import { ChainId, Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core'
import { ZERO_PERCENT } from 'constants/misc'
import { useQuickRouteMainnetEnabled } from 'featureFlags/flags/quickRouteMainnet'
import useIsWindowVisible from 'hooks/useIsWindowVisible'
import { useMemo } from 'react'
import { useGetQuickRouteQuery, useGetQuickRouteQueryState } from './quickRouteSlice'
@ -78,9 +79,10 @@ export function usePreviewTrade(
inputTax,
outputTax,
})
const isWindowVisible = useIsWindowVisible()
const { isError, data: tradeResult, error, currentData } = useGetQuickRouteQueryState(queryArgs)
useGetQuickRouteQuery(skipFetch ? skipToken : queryArgs, {
useGetQuickRouteQuery(skipFetch || !isWindowVisible ? skipToken : queryArgs, {
// If latest quote from cache was fetched > 2m ago, instantly repoll for another instead of waiting for next poll period
refetchOnMountOrArgChange: 2 * 60,
})

@ -0,0 +1,143 @@
import { skipToken } from '@reduxjs/toolkit/query/react'
import { renderHook } from '@testing-library/react'
import { CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { AVERAGE_L1_BLOCK_TIME } from 'constants/chainInfo'
import { ZERO_PERCENT } from 'constants/misc'
import { USDC_MAINNET } from 'constants/tokens'
import { useUniswapXDefaultEnabled } from 'featureFlags/flags/uniswapXDefault'
import { useUniswapXEthOutputEnabled } from 'featureFlags/flags/uniswapXEthOutput'
import { useUniswapXExactOutputEnabled } from 'featureFlags/flags/uniswapXExactOutput'
import { useUniswapXSyntheticQuoteEnabled } from 'featureFlags/flags/uniswapXUseSyntheticQuote'
import { useFeesEnabled } from 'featureFlags/flags/useFees'
import useIsWindowVisible from 'hooks/useIsWindowVisible'
import ms from 'ms'
import { GetQuoteArgs, INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference } from 'state/routing/types'
import { useRouterPreference, useUserDisabledUniswapX, useUserOptedOutOfUniswapX } from 'state/user/hooks'
import { ETH_MAINNET } from 'test-utils/constants'
import { mocked } from 'test-utils/mocked'
import { useGetQuoteQuery, useGetQuoteQueryState } from './slice'
import { useRoutingAPITrade } from './useRoutingAPITrade'
import { currencyAddressForSwapQuote } from './utils'
const USDCAmount = CurrencyAmount.fromRawAmount(USDC_MAINNET, '10000')
jest.mock('hooks/useIsWindowVisible')
jest.mock('state/routing/usePreviewTrade')
jest.mock('./slice', () => {
return {
useGetQuoteQuery: jest.fn(),
useGetQuoteQueryState: jest.fn(),
}
})
jest.mock('state/user/hooks')
jest.mock('featureFlags/flags/uniswapXUseSyntheticQuote')
jest.mock('featureFlags/flags/uniswapXEthOutput')
jest.mock('featureFlags/flags/uniswapXExactOutput')
jest.mock('featureFlags/flags/uniswapXDefault')
jest.mock('featureFlags/flags/useFees')
beforeEach(() => {
mocked(useIsWindowVisible).mockReturnValue(true)
mocked(useRouterPreference).mockReturnValue([RouterPreference.API, () => undefined])
mocked(useUniswapXSyntheticQuoteEnabled).mockReturnValue(false)
mocked(useUserDisabledUniswapX).mockReturnValue(false)
mocked(useUserOptedOutOfUniswapX).mockReturnValue(false)
mocked(useUniswapXEthOutputEnabled).mockReturnValue(false)
mocked(useUniswapXExactOutputEnabled).mockReturnValue(false)
mocked(useUniswapXDefaultEnabled).mockReturnValue(false)
mocked(useFeesEnabled).mockReturnValue(true)
// @ts-ignore we dont use the response from this hook in useRoutingAPITrade so fine to mock as undefined
mocked(useGetQuoteQuery).mockReturnValue(undefined)
mocked(useGetQuoteQueryState).mockReturnValue({
refetch: jest.fn(),
isError: false,
data: undefined,
error: false,
currentData: undefined,
})
})
const MOCK_ARGS: GetQuoteArgs = {
account: undefined,
amount: USDCAmount.quotient.toString(),
tokenInAddress: currencyAddressForSwapQuote(USDCAmount.currency),
tokenInChainId: USDCAmount.currency.chainId,
tokenInDecimals: USDCAmount.currency.wrapped.decimals,
tokenInSymbol: USDCAmount.currency.wrapped.symbol,
tokenOutAddress: currencyAddressForSwapQuote(ETH_MAINNET),
tokenOutChainId: ETH_MAINNET.wrapped.chainId,
tokenOutDecimals: ETH_MAINNET.wrapped.decimals,
tokenOutSymbol: ETH_MAINNET.wrapped.symbol,
routerPreference: RouterPreference.API,
tradeType: TradeType.EXACT_INPUT,
needsWrapIfUniswapX: USDCAmount.currency.isNative,
uniswapXForceSyntheticQuotes: false,
userDisabledUniswapX: false,
userOptedOutOfUniswapX: false,
uniswapXEthOutputEnabled: false,
uniswapXExactOutputEnabled: false,
isUniswapXDefaultEnabled: false,
sendPortionEnabled: true,
inputTax: ZERO_PERCENT,
outputTax: ZERO_PERCENT,
}
describe('#useRoutingAPITrade ExactIn', () => {
it('does not call routing api when window is not focused for quote requests', () => {
mocked(useIsWindowVisible).mockReturnValue(false)
const { result } = renderHook(() =>
useRoutingAPITrade(false, TradeType.EXACT_INPUT, USDCAmount, ETH_MAINNET, RouterPreference.API)
)
expect(useGetQuoteQuery).toHaveBeenCalledWith(skipToken, {
pollingInterval: AVERAGE_L1_BLOCK_TIME,
refetchOnMountOrArgChange: 2 * 60,
})
expect(result.current?.trade).toEqual(undefined)
})
it('does call routing api when window is focused for quote requests', () => {
mocked(useIsWindowVisible).mockReturnValue(true)
renderHook(() => useRoutingAPITrade(false, TradeType.EXACT_INPUT, USDCAmount, ETH_MAINNET, RouterPreference.API))
expect(useGetQuoteQuery).toHaveBeenCalledWith(MOCK_ARGS, {
pollingInterval: AVERAGE_L1_BLOCK_TIME,
refetchOnMountOrArgChange: 2 * 60,
})
})
})
describe('#useRoutingAPITrade pricing', () => {
it('does not call routing api when window is not focused for price requests', () => {
mocked(useIsWindowVisible).mockReturnValue(false)
const { result } = renderHook(() =>
useRoutingAPITrade(false, TradeType.EXACT_INPUT, USDCAmount, ETH_MAINNET, INTERNAL_ROUTER_PREFERENCE_PRICE)
)
expect(useGetQuoteQuery).toHaveBeenCalledWith(skipToken, {
pollingInterval: ms(`1m`),
refetchOnMountOrArgChange: 2 * 60,
})
expect(result.current?.trade).toEqual(undefined)
})
it('does call routing api when window is focused for pricing requests', () => {
mocked(useIsWindowVisible).mockReturnValue(true)
renderHook(() =>
useRoutingAPITrade(false, TradeType.EXACT_INPUT, USDCAmount, ETH_MAINNET, INTERNAL_ROUTER_PREFERENCE_PRICE)
)
expect(useGetQuoteQuery).toHaveBeenCalledWith(
{ ...MOCK_ARGS, sendPortionEnabled: false, routerPreference: INTERNAL_ROUTER_PREFERENCE_PRICE },
{
pollingInterval: ms(`1m`),
refetchOnMountOrArgChange: 2 * 60,
}
)
})
})

@ -2,6 +2,7 @@ import { skipToken } from '@reduxjs/toolkit/query/react'
import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core'
import { AVERAGE_L1_BLOCK_TIME } from 'constants/chainInfo'
import { ZERO_PERCENT } from 'constants/misc'
import useIsWindowVisible from 'hooks/useIsWindowVisible'
import { useRoutingAPIArguments } from 'lib/hooks/routing/useRoutingAPIArguments'
import ms from 'ms'
import { useMemo } from 'react'
@ -92,9 +93,11 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
inputTax,
outputTax,
})
// skip all pricing and quote requests if the window is not focused
const isWindowVisible = useIsWindowVisible()
const { isError, data: tradeResult, error, currentData } = useGetQuoteQueryState(queryArgs)
useGetQuoteQuery(skipFetch ? skipToken : queryArgs, {
useGetQuoteQuery(skipFetch || !isWindowVisible ? skipToken : queryArgs, {
// 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