Skip to content

Commit

Permalink
fix: improve responsiveness of useSwapInfo (#226)
Browse files Browse the repository at this point in the history
* refactor: clearer quote var

* feat: add RouterPreference.SKIP

* refactor: clearer input/output convention

* fix: immediately update ux for new trades

* Revert "refactor: clearer input/output convention"

This reverts commit 0c9cf78.

* feat: rm unused auto-switcher
  • Loading branch information
zzmp authored Oct 24, 2022
1 parent 5479440 commit c379661
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 91 deletions.
50 changes: 33 additions & 17 deletions src/hooks/routing/useRouterTrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { isExactInput } from 'utils/tradeType'
export enum RouterPreference {
PRICE,
TRADE,
SKIP,
}

const TRADE_INVALID = { state: TradeState.INVALID, trade: undefined }
Expand All @@ -40,10 +41,22 @@ export function useRouterTrade(
gasUseEstimateUSD?: CurrencyAmount<Token>
} {
const { provider } = useWeb3React()
const queryArgs = useGetQuoteArgs({ provider, tradeType, amountSpecified, otherCurrency, routerUrl })
const queryArgs = useGetQuoteArgs(
{ provider, tradeType, amountSpecified, otherCurrency, routerUrl },
/*skip=*/ routerPreference === RouterPreference.SKIP
)

// PRICE fetching is informational and costly, so it is done less frequently.
const pollingInterval = routerPreference === RouterPreference.PRICE ? ms`2m` : ms`15s`
const pollingInterval = useMemo(() => {
switch (routerPreference) {
// PRICE fetching is informational and costly, so it is done less frequently.
case RouterPreference.PRICE:
return ms`2m`
case RouterPreference.TRADE:
return ms`15s`
case RouterPreference.SKIP:
return Infinity
}
}, [routerPreference])

// Get the cached state *immediately* to update the UI without sending a request - using useGetQuoteQueryState -
// but debounce the actual request - using useLazyGetQuoteQuery - to avoid flooding the router / JSON-RPC endpoints.
Expand All @@ -60,31 +73,34 @@ export function useRouterTrade(
}, [fulfilledTimeStamp, isFetching, pollingInterval, queryArgs, trigger])
useTimeout(request, 200)

const result = typeof data === 'object' ? data : undefined
const quote = typeof data === 'object' ? data : undefined
const trade = useMemo(() => {
const [currencyIn, currencyOut] = isExactInput(tradeType)
? [amountSpecified?.currency, otherCurrency]
: [otherCurrency, amountSpecified?.currency]
const routes = computeRoutes(currencyIn, currencyOut, tradeType, result)
const routes = computeRoutes(currencyIn, currencyOut, tradeType, quote)
if (!routes || routes.length === 0) return
try {
return transformRoutesToTrade(routes, tradeType)
} catch (e: unknown) {
console.debug('transformRoutesToTrade failed: ', e)
return
}
}, [amountSpecified?.currency, otherCurrency, result, tradeType])
const isValidBlock = useIsValidBlock(Number(result?.blockNumber))
const isLoading = currentData !== data || !isValidBlock
const gasUseEstimateUSD = useStablecoinAmountFromFiatValue(result?.gasUseEstimateUSD)
}, [amountSpecified?.currency, otherCurrency, quote, tradeType])
const isValidBlock = useIsValidBlock(Number(quote?.blockNumber))
const isValid = currentData === data && isValidBlock
const gasUseEstimateUSD = useStablecoinAmountFromFiatValue(quote?.gasUseEstimateUSD)

return useMemo(() => {
if (queryArgs === skipToken) return TRADE_INVALID
if (data === NO_ROUTE) return TRADE_NOT_FOUND

if (!trade) return isError ? TRADE_NOT_FOUND : TRADE_LOADING

const state = isLoading ? TradeState.LOADING : TradeState.VALID
return { state, trade, gasUseEstimateUSD }
}, [queryArgs, data, trade, isError, isLoading, gasUseEstimateUSD])
if (isError || queryArgs === skipToken) {
return TRADE_INVALID
} else if (data === NO_ROUTE) {
return TRADE_NOT_FOUND
} else if (!trade) {
return TRADE_LOADING
} else {
const state = isValid ? TradeState.VALID : TradeState.LOADING
return { state, trade, gasUseEstimateUSD }
}
}, [isError, queryArgs, data, trade, isValid, gasUseEstimateUSD])
}
80 changes: 28 additions & 52 deletions src/hooks/swap/useSwapInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import { useCurrencyBalances } from 'hooks/useCurrencyBalance'
import useOnSupportedNetwork from 'hooks/useOnSupportedNetwork'
import { PriceImpact, usePriceImpact } from 'hooks/usePriceImpact'
import useSlippage, { DEFAULT_SLIPPAGE, Slippage } from 'hooks/useSlippage'
import useSwitchChain from 'hooks/useSwitchChain'
import { useUSDCValue } from 'hooks/useUSDCPrice'
import useConnectors from 'hooks/web3/useConnectors'
import { useAtomValue } from 'jotai/utils'
import { createContext, PropsWithChildren, useContext, useEffect, useMemo, useRef } from 'react'
import { InterfaceTrade, TradeState } from 'state/routing/types'
Expand Down Expand Up @@ -45,72 +43,67 @@ interface SwapInfo {
impact?: PriceImpact
}

// from the current swap inputs, compute the best trade and return it.
/** Returns the best computed swap (trade/wrap). */
function useComputeSwapInfo(routerUrl?: string): SwapInfo {
const { account, chainId, isActivating, isActive } = useWeb3React()
const isSupported = useOnSupportedNetwork()
const { type, amount, [Field.INPUT]: currencyIn, [Field.OUTPUT]: currencyOut } = useAtomValue(swapAtom)
const isWrap = useIsWrap()

const chainIn = currencyIn?.chainId
const chainOut = currencyOut?.chainId
const tokenChainId = chainIn || chainOut
const chainIdIn = currencyIn?.chainId
const chainIdOut = currencyOut?.chainId
const tokenChainId = chainIdIn || chainIdOut
const error = useMemo(() => {
if (!isActive) return isActivating ? ChainError.ACTIVATING_CHAIN : ChainError.UNCONNECTED_CHAIN
if (!isSupported) return ChainError.UNSUPPORTED_CHAIN
if (chainIn && chainOut && chainIn !== chainOut) return ChainError.MISMATCHED_TOKEN_CHAINS
if (chainIdIn && chainIdOut && chainIdIn !== chainIdOut) return ChainError.MISMATCHED_TOKEN_CHAINS
if (chainId && tokenChainId && chainId !== tokenChainId) return ChainError.MISMATCHED_CHAINS
return
}, [chainId, chainIn, chainOut, isActivating, isActive, isSupported, tokenChainId])
}, [chainId, chainIdIn, chainIdOut, isActivating, isActive, isSupported, tokenChainId])

const parsedAmount = useMemo(
() => tryParseCurrencyAmount(amount, (isExactInput(type) ? currencyIn : currencyOut) ?? undefined),
[amount, type, currencyIn, currencyOut]
[amount, currencyIn, currencyOut, type]
)
const hasAmounts = currencyIn && currencyOut && parsedAmount && !isWrap
const trade = useRouterTrade(
type,
hasAmounts ? parsedAmount : undefined,
hasAmounts ? (isExactInput(type) ? currencyOut : currencyIn) : undefined,
RouterPreference.TRADE,
parsedAmount,
isExactInput(type) ? currencyOut : currencyIn,
isWrap || error ? RouterPreference.SKIP : RouterPreference.TRADE,
routerUrl
)

const amountIn = useMemo(
() => (isWrap || isExactInput(type) ? parsedAmount : trade.trade?.inputAmount),
[isWrap, parsedAmount, trade.trade?.inputAmount, type]
)
const amountOut = useMemo(
() => (isWrap || !isExactInput(type) ? parsedAmount : trade.trade?.outputAmount),
[isWrap, parsedAmount, trade.trade?.outputAmount, type]
)

const [balanceIn, balanceOut] = useCurrencyBalances(
account,
useMemo(() => [currencyIn, currencyOut], [currencyIn, currencyOut])
)
// Use the parsed amount when applicable (exact amounts and wraps) immediately responsive UI.
const [amountIn, amountOut] = useMemo(() => {
if (isWrap) {
return isExactInput(type)
? [parsedAmount, tryParseCurrencyAmount(amount, currencyOut)]
: [tryParseCurrencyAmount(amount, currencyIn), parsedAmount]
}
return isExactInput(type) ? [parsedAmount, trade.trade?.outputAmount] : [trade.trade?.inputAmount, parsedAmount]
}, [amount, currencyIn, currencyOut, isWrap, parsedAmount, trade.trade?.inputAmount, trade.trade?.outputAmount, type])
const currencies = useMemo(() => [currencyIn, currencyOut], [currencyIn, currencyOut])
const [balanceIn, balanceOut] = useCurrencyBalances(account, currencies)
const [usdcIn, usdcOut] = [useUSDCValue(amountIn), useUSDCValue(amountOut)]

// Compute slippage and impact off of the trade so that it refreshes with the trade.
// (Using amountIn/amountOut would show (incorrect) intermediate values.)
// Wait until the trade is valid to avoid displaying incorrect intermediate values.
const slippage = useSlippage(trade)
const inputUSDCValue = useUSDCValue(trade.trade?.inputAmount)
const outputUSDCValue = useUSDCValue(trade.trade?.outputAmount)

const impact = usePriceImpact(trade.trade, { inputUSDCValue, outputUSDCValue })
const impact = usePriceImpact(trade.trade)

return useMemo(() => {
return {
[Field.INPUT]: {
currency: currencyIn,
amount: amountIn,
balance: balanceIn,
usdc: inputUSDCValue,
usdc: usdcIn,
},
[Field.OUTPUT]: {
currency: currencyOut,
amount: amountOut,
balance: balanceOut,
usdc: outputUSDCValue,
usdc: usdcOut,
},
error,
trade,
Expand All @@ -126,10 +119,10 @@ function useComputeSwapInfo(routerUrl?: string): SwapInfo {
currencyOut,
error,
impact,
inputUSDCValue,
outputUSDCValue,
slippage,
trade,
usdcIn,
usdcOut,
])
}

Expand Down Expand Up @@ -157,23 +150,6 @@ export function SwapInfoProvider({ children, routerUrl }: PropsWithChildren<{ ro
}
}, [onInitialSwapQuote, swap, swapInfo.trade.state, swapInfo.trade.trade])

const {
error,
[Field.INPUT]: { currency: currencyIn },
[Field.OUTPUT]: { currency: currencyOut },
} = swapInfo
const { connector } = useWeb3React()
const switchChain = useSwitchChain()
const chainIn = currencyIn?.chainId
const chainOut = currencyOut?.chainId
const tokenChainId = chainIn || chainOut
const { network } = useConnectors()
// The network connector should be auto-switched, as it is a read-only interface that should "just work".
if (error === ChainError.MISMATCHED_CHAINS && tokenChainId && connector === network) {
delete swapInfo.error // avoids flashing an error whilst switching
switchChain(tokenChainId)
}

return <SwapInfoContext.Provider value={swapInfo}>{children}</SwapInfoContext.Provider>
}

Expand Down
13 changes: 5 additions & 8 deletions src/hooks/usePriceImpact.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
import { CurrencyAmount, Percent, Token } from '@uniswap/sdk-core'
import { Percent } from '@uniswap/sdk-core'
import { useMemo } from 'react'
import { InterfaceTrade } from 'state/routing/types'
import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
import { computeRealizedPriceImpact, getPriceImpactWarning, largerPercentValue } from 'utils/prices'

import { useUSDCValue } from './useUSDCPrice'

export interface PriceImpact {
percent: Percent
warning?: 'warning' | 'error'
toString(): string
}

export function usePriceImpact(
trade: InterfaceTrade | undefined,
{
inputUSDCValue,
outputUSDCValue,
}: { inputUSDCValue: CurrencyAmount<Token> | undefined; outputUSDCValue: CurrencyAmount<Token> | undefined }
) {
export function usePriceImpact(trade?: InterfaceTrade) {
const [inputUSDCValue, outputUSDCValue] = [useUSDCValue(trade?.inputAmount), useUSDCValue(trade?.outputAmount)]
return useMemo(() => {
const fiatPriceImpact = computeFiatValuePriceImpact(inputUSDCValue, outputUSDCValue)
const marketPriceImpact = trade ? computeRealizedPriceImpact(trade) : undefined
Expand Down
33 changes: 19 additions & 14 deletions src/state/routing/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,22 @@ export function serializeGetQuoteArgs({ endpointName, queryArgs }: { endpointNam
* (this includes if the window is not visible).
* NB: Input arguments do not need to be memoized, as they will be destructured.
*/
export function useGetQuoteArgs({
provider,
tradeType,
amountSpecified,
otherCurrency,
routerUrl,
}: Partial<{
provider: BaseProvider
tradeType: TradeType
amountSpecified: CurrencyAmount<Currency>
otherCurrency: Currency
routerUrl: string
}>): GetQuoteArgs | SkipToken {
export function useGetQuoteArgs(
{
provider,
tradeType,
amountSpecified,
otherCurrency,
routerUrl,
}: Partial<{
provider: BaseProvider
tradeType: TradeType
amountSpecified: CurrencyAmount<Currency>
otherCurrency: Currency
routerUrl: string
}>,
skip?: boolean
): GetQuoteArgs | SkipToken {
const args = useMemo(() => {
if (!provider || !amountSpecified || tradeType === undefined) return null

Expand All @@ -73,5 +76,7 @@ export function useGetQuoteArgs({
}, [provider, amountSpecified, tradeType, otherCurrency, routerUrl])

const isWindowVisible = useIsWindowVisible()
return (isWindowVisible ? args : null) ?? skipToken
if (skip || !isWindowVisible) return skipToken

return args ?? skipToken
}

1 comment on commit c379661

@vercel
Copy link

@vercel vercel bot commented on c379661 Oct 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

widgets – ./

widgets-uniswap.vercel.app
widgets-git-main-uniswap.vercel.app
widgets-seven-tau.vercel.app

Please sign in to comment.