diff --git a/src/hooks/routing/useRouterTrade.ts b/src/hooks/routing/useRouterTrade.ts index e6a3c3503..df7e25c73 100644 --- a/src/hooks/routing/useRouterTrade.ts +++ b/src/hooks/routing/useRouterTrade.ts @@ -15,6 +15,7 @@ import { isExactInput } from 'utils/tradeType' export enum RouterPreference { PRICE, TRADE, + SKIP, } const TRADE_INVALID = { state: TradeState.INVALID, trade: undefined } @@ -40,10 +41,22 @@ export function useRouterTrade( gasUseEstimateUSD?: CurrencyAmount } { 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. @@ -60,12 +73,12 @@ 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) @@ -73,18 +86,21 @@ export function useRouterTrade( 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]) } diff --git a/src/hooks/swap/useSwapInfo.tsx b/src/hooks/swap/useSwapInfo.tsx index 9e49dc3c6..9a0eaf9c2 100644 --- a/src/hooks/swap/useSwapInfo.tsx +++ b/src/hooks/swap/useSwapInfo.tsx @@ -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' @@ -45,58 +43,53 @@ 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 { @@ -104,13 +97,13 @@ function useComputeSwapInfo(routerUrl?: string): SwapInfo { currency: currencyIn, amount: amountIn, balance: balanceIn, - usdc: inputUSDCValue, + usdc: usdcIn, }, [Field.OUTPUT]: { currency: currencyOut, amount: amountOut, balance: balanceOut, - usdc: outputUSDCValue, + usdc: usdcOut, }, error, trade, @@ -126,10 +119,10 @@ function useComputeSwapInfo(routerUrl?: string): SwapInfo { currencyOut, error, impact, - inputUSDCValue, - outputUSDCValue, slippage, trade, + usdcIn, + usdcOut, ]) } @@ -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 {children} } diff --git a/src/hooks/usePriceImpact.ts b/src/hooks/usePriceImpact.ts index aa61c2c22..881804467 100644 --- a/src/hooks/usePriceImpact.ts +++ b/src/hooks/usePriceImpact.ts @@ -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 | undefined; outputUSDCValue: CurrencyAmount | 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 diff --git a/src/state/routing/args.ts b/src/state/routing/args.ts index 4d5d809da..5d7fefa2b 100644 --- a/src/state/routing/args.ts +++ b/src/state/routing/args.ts @@ -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 - otherCurrency: Currency - routerUrl: string -}>): GetQuoteArgs | SkipToken { +export function useGetQuoteArgs( + { + provider, + tradeType, + amountSpecified, + otherCurrency, + routerUrl, + }: Partial<{ + provider: BaseProvider + tradeType: TradeType + amountSpecified: CurrencyAmount + otherCurrency: Currency + routerUrl: string + }>, + skip?: boolean +): GetQuoteArgs | SkipToken { const args = useMemo(() => { if (!provider || !amountSpecified || tradeType === undefined) return null @@ -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 }