Commit 82c30681 authored by Zach Pomerantz's avatar Zach Pomerantz Committed by GitHub

fix: ignore stale SOR fetches (#3313)

* fix: propagate ROUTE_NOT_FOUND and fallback appropriately

* fix: display insufficient liquidities

* fix: ignore stale SOR results

* fix: retain trade state while loading

* fix: mv debouncing to SOR logic for sync state
parent 41ef9616
......@@ -55,12 +55,9 @@ export default function Input({ disabled, focused }: InputProps) {
// extract eagerly in case of reversal
usePrefetchCurrencyColor(swapInputCurrency)
const isTradeLoading = useMemo(
() => TradeState.LOADING === tradeState || TradeState.SYNCING === tradeState,
[tradeState]
)
const isRouteLoading = tradeState === TradeState.SYNCING || tradeState === TradeState.LOADING
const isDependentField = useAtomValue(independentFieldAtom) !== Field.INPUT
const isLoading = isDependentField && isTradeLoading
const isLoading = isRouteLoading && isDependentField
//TODO(ianlapham): mimic logic from app swap page
const mockApproved = true
......
......@@ -48,14 +48,9 @@ export default function Output({ disabled, focused, children }: PropsWithChildre
const [swapOutputAmount, updateSwapOutputAmount] = useSwapAmount(Field.OUTPUT)
const [swapOutputCurrency, updateSwapOutputCurrency] = useSwapCurrency(Field.OUTPUT)
//loading status of the trade
const isTradeLoading = useMemo(
() => TradeState.LOADING === tradeState || TradeState.SYNCING === tradeState,
[tradeState]
)
const isRouteLoading = tradeState === TradeState.SYNCING || tradeState === TradeState.LOADING
const isDependentField = useAtomValue(independentFieldAtom) !== Field.OUTPUT
const isLoading = isDependentField && isTradeLoading
const isLoading = isRouteLoading && isDependentField
const overrideColor = useAtomValue(colorAtom)
const dynamicColor = useCurrencyColor(swapOutputCurrency)
......
......@@ -23,12 +23,7 @@ export default function Toolbar({ disabled }: { disabled?: boolean }) {
currencies: { [Field.INPUT]: inputCurrency, [Field.OUTPUT]: outputCurrency },
currencyBalances: { [Field.INPUT]: balance },
} = useSwapInfo()
const [routeFound, routeIsLoading] = useMemo(
() => [Boolean(trade?.swaps), TradeState.LOADING === state || TradeState.SYNCING === state],
[state, trade?.swaps]
)
const isRouteLoading = state === TradeState.SYNCING || state === TradeState.LOADING
const isAmountPopulated = useIsAmountPopulated()
const caption = useMemo(() => {
......@@ -41,10 +36,10 @@ export default function Toolbar({ disabled }: { disabled?: boolean }) {
}
if (inputCurrency && outputCurrency && isAmountPopulated) {
if (!trade || routeIsLoading) {
if (isRouteLoading) {
return <Caption.LoadingTrade />
}
if (!routeFound) {
if (!trade?.swaps) {
return <Caption.InsufficientLiquidity />
}
if (balance && trade?.inputAmount.greaterThan(balance)) {
......@@ -56,7 +51,7 @@ export default function Toolbar({ disabled }: { disabled?: boolean }) {
}
return <Caption.Empty />
}, [balance, chainId, disabled, inputCurrency, isAmountPopulated, outputCurrency, routeFound, routeIsLoading, trade])
}, [balance, chainId, disabled, inputCurrency, isAmountPopulated, isRouteLoading, outputCurrency, trade])
return (
<>
......
import { Protocol } from '@uniswap/router-sdk'
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 { GetQuoteResult, InterfaceTrade, TradeState } from 'state/routing/types'
......@@ -41,6 +42,12 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
state: TradeState
trade: InterfaceTrade<Currency, Currency, TTradeType> | undefined
} {
// Debounce is used to prevent excessive requests to SOR, as it is data intensive.
// This helps provide a "syncing" state the UI can reference for loading animations.
const inputs = useMemo(() => [tradeType, amountSpecified, otherCurrency], [tradeType, amountSpecified, otherCurrency])
const debouncedInputs = useDebounce(inputs, 200)
const isDebouncing = inputs !== debouncedInputs
const chainId = amountSpecified?.currency.chainId
const { library } = useActiveWeb3React()
......@@ -62,90 +69,87 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
const params = useMemo(() => chainId && library && { chainId, provider: library }, [chainId, library])
const [loading, setLoading] = useState(false)
const [{ quoteResult, error }, setFetchedResult] = useState<{
quoteResult: GetQuoteResult | undefined
error: unknown
}>({
quoteResult: undefined,
error: undefined,
})
const [{ data: quoteResult, error }, setResult] = useState<{
data?: GetQuoteResult
error?: unknown
}>({ error: undefined })
const config = useMemo(() => getConfig(chainId), [chainId])
// When arguments update, make a new call to SOR for updated quote
useEffect(() => {
setLoading(true)
if (isDebouncing) return
let stale = false
fetchQuote()
return () => {
stale = true
setLoading(false)
}
async function fetchQuote() {
try {
if (queryArgs && params) {
const result = await getClientSideQuote(queryArgs, params, config)
setFetchedResult({
quoteResult: result.data,
error: result.error,
})
if (queryArgs && params) {
let result
try {
result = await getClientSideQuote(queryArgs, params, config)
} catch {
result = { error: true }
}
if (!stale) {
setResult(result)
setLoading(false)
}
} catch (e) {
setFetchedResult({
quoteResult: undefined,
error: true,
})
} finally {
setLoading(false)
}
}
}, [queryArgs, params, config])
}, [queryArgs, params, config, isDebouncing])
const route = useMemo(
() => computeRoutes(currencyIn, currencyOut, tradeType, quoteResult),
[currencyIn, currencyOut, quoteResult, tradeType]
)
// get USD gas cost of trade in active chains stablecoin amount
const gasUseEstimateUSD = useStablecoinAmountFromFiatValue(quoteResult?.gasUseEstimateUSD) ?? null
const trade = useMemo(() => {
if (route) {
try {
return route && transformRoutesToTrade(route, tradeType, gasUseEstimateUSD)
} catch (e: unknown) {
console.debug('transformRoutesToTrade failed: ', e)
}
}
return
}, [gasUseEstimateUSD, route, tradeType])
return useMemo(() => {
if (!currencyIn || !currencyOut) {
return {
state: TradeState.INVALID,
trade: undefined,
}
return { state: TradeState.INVALID, trade: undefined }
}
if (loading && !quoteResult) {
// only on first hook render
return {
state: TradeState.LOADING,
trade: undefined,
}
// 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 }
}
let otherAmount = undefined
if (tradeType === TradeType.EXACT_INPUT && currencyOut && quoteResult) {
otherAmount = CurrencyAmount.fromRawAmount(currencyOut, quoteResult.quote)
}
if (tradeType === TradeType.EXACT_OUTPUT && currencyIn && quoteResult) {
otherAmount = CurrencyAmount.fromRawAmount(currencyIn, quoteResult.quote)
if (quoteResult) {
switch (tradeType) {
case TradeType.EXACT_INPUT:
otherAmount = CurrencyAmount.fromRawAmount(currencyOut, quoteResult.quote)
break
case TradeType.EXACT_OUTPUT:
otherAmount = CurrencyAmount.fromRawAmount(currencyIn, quoteResult.quote)
break
}
}
if (error || !otherAmount || !route || route.length === 0 || !queryArgs) {
return {
state: TradeState.NO_ROUTE_FOUND,
trade: undefined,
}
return { state: TradeState.NO_ROUTE_FOUND, trade: undefined }
}
try {
const trade = transformRoutesToTrade(route, tradeType, gasUseEstimateUSD)
return {
// always return VALID regardless of isFetching status
state: TradeState.VALID,
trade,
}
} catch (e) {
console.debug('transformRoutesToTrade failed: ', e)
return { state: TradeState.INVALID, trade: undefined }
if (trade) {
return { state: TradeState.VALID, trade }
}
}, [currencyIn, currencyOut, loading, quoteResult, tradeType, error, route, queryArgs, gasUseEstimateUSD])
return { state: TradeState.INVALID, trade: undefined }
}, [currencyIn, currencyOut, isDebouncing, loading, quoteResult, error, route, queryArgs, trade, tradeType])
}
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { useClientSideV3Trade } from 'hooks/useClientSideV3Trade'
import useDebounce from 'hooks/useDebounce'
import { useMemo } from 'react'
import { InterfaceTrade, TradeState } from 'state/routing/types'
import useClientSideSmartOrderRouterTrade from '../routing/useClientSideSmartOrderRouterTrade'
/**
* Returns the currency amount from independent field, currency from independent field,
* and currency from dependent field.
*/
function getTradeInputs(
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined,
tradeType: TradeType
): [CurrencyAmount<Currency> | undefined, Currency | undefined, Currency | undefined] {
if (trade) {
if (tradeType === TradeType.EXACT_INPUT) {
return [trade.inputAmount, trade.inputAmount.currency, trade.outputAmount.currency]
}
if (tradeType === TradeType.EXACT_OUTPUT) {
return [trade.outputAmount, trade.outputAmount.currency, trade.inputAmount.currency]
}
}
return [undefined, undefined, undefined]
}
interface TradeDebouncingParams {
amounts: [CurrencyAmount<Currency> | undefined, CurrencyAmount<Currency> | undefined]
indepdenentCurrencies: [Currency | undefined, Currency | undefined]
dependentCurrencies: [Currency | undefined, Currency | undefined]
}
/**
* Returns wether debounced values are stale compared to latest values from trade.
*/
function isTradeDebouncing({ amounts, indepdenentCurrencies, dependentCurrencies }: TradeDebouncingParams): boolean {
// Ensure that amount from user input matches latest trade.
const amountsMatch = amounts[0] && amounts[1]?.equalTo(amounts[0])
// Ensure active swap currencies match latest trade.
const currenciesMatch =
indepdenentCurrencies[0] &&
indepdenentCurrencies[1]?.equals(indepdenentCurrencies[0]) &&
dependentCurrencies[0] &&
dependentCurrencies[1]?.equals(dependentCurrencies[0])
return !amountsMatch || !currenciesMatch
}
/**
* Returns the best v2+v3 trade for a desired swap.
* @param tradeType whether the swap is an exact in/out
......@@ -62,47 +18,15 @@ export function useBestTrade(
state: TradeState
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
} {
// Debounce is used to prevent excessive requests to SOR, as it is data intensive.
// This helps provide a "syncing" state the UI can reference for loading animations.
const [debouncedAmount, debouncedOtherCurrency] = useDebounce(
useMemo(() => [amountSpecified, otherCurrency], [amountSpecified, otherCurrency]),
200
)
const clientSORTrade = useClientSideSmartOrderRouterTrade(tradeType, debouncedAmount, debouncedOtherCurrency)
const [amountFromLatestTrade, currencyFromTrade, otherCurrencyFromTrade] = getTradeInputs(
clientSORTrade.trade,
tradeType
)
const debouncing =
(amountSpecified && debouncedAmount && amountSpecified !== debouncedAmount) ||
(amountSpecified && debouncedOtherCurrency && otherCurrency && debouncedOtherCurrency !== otherCurrency)
const syncing =
amountSpecified &&
isTradeDebouncing({
amounts: [amountFromLatestTrade, amountSpecified],
indepdenentCurrencies: [currencyFromTrade, amountSpecified?.currency],
dependentCurrencies: [otherCurrencyFromTrade, debouncedOtherCurrency],
})
const useFallback = !syncing && clientSORTrade.state === TradeState.NO_ROUTE_FOUND
const clientSORTrade = useClientSideSmartOrderRouterTrade(tradeType, amountSpecified, otherCurrency)
// Use a simple client side logic as backup if SOR is not available.
const useFallback = clientSORTrade.state === TradeState.NO_ROUTE_FOUND || clientSORTrade.state === TradeState.INVALID
const fallbackTrade = useClientSideV3Trade(
tradeType,
useFallback ? debouncedAmount : undefined,
useFallback ? debouncedOtherCurrency : undefined
useFallback ? amountSpecified : undefined,
useFallback ? otherCurrency : undefined
)
return useMemo(
() => ({
...(useFallback ? fallbackTrade : clientSORTrade),
...(syncing ? { state: TradeState.SYNCING } : {}),
...(debouncing ? { state: TradeState.LOADING } : {}),
}),
[debouncing, fallbackTrade, syncing, clientSORTrade, useFallback]
)
return useFallback ? fallbackTrade : clientSORTrade
}
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