From 2aa1e40481ad9d70ee871f498fed3ae3cf9f1100 Mon Sep 17 00:00:00 2001 From: Ian Lapham Date: Fri, 4 Feb 2022 18:38:27 -0500 Subject: [PATCH] feat: create use best trade hook for widgets (#3226) * create use best trade hook for widgets * update comment in hook file * refactor loading state conditional * update logic in use best trade * clean code in best trade hook --- src/lib/hooks/swap/useBestTrade.ts | 106 +++++++++++++++++++++++++++++ src/lib/hooks/swap/useSwapInfo.tsx | 4 +- 2 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 src/lib/hooks/swap/useBestTrade.ts diff --git a/src/lib/hooks/swap/useBestTrade.ts b/src/lib/hooks/swap/useBestTrade.ts new file mode 100644 index 0000000000..51f2d3a108 --- /dev/null +++ b/src/lib/hooks/swap/useBestTrade.ts @@ -0,0 +1,106 @@ +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 | undefined, + tradeType: TradeType +): [CurrencyAmount | 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 | undefined, CurrencyAmount | 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 + * @param amountSpecified the exact amount to swap in/out + * @param otherCurrency the desired output/payment currency + */ +export function useBestTrade( + tradeType: TradeType, + amountSpecified?: CurrencyAmount, + otherCurrency?: Currency +): { + state: TradeState + trade: InterfaceTrade | 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) || + (debouncedOtherCurrency && otherCurrency && debouncedOtherCurrency !== otherCurrency) + + const syncing = isTradeDebouncing({ + amounts: [amountFromLatestTrade, debouncedAmount], + indepdenentCurrencies: [currencyFromTrade, debouncedOtherCurrency], + dependentCurrencies: [otherCurrencyFromTrade, otherCurrency], + }) + + const useFallback = !syncing && clientSORTrade.state === TradeState.NO_ROUTE_FOUND + + // Use a simple client side logic as backup if SOR is not available. + const fallbackTrade = useClientSideV3Trade( + tradeType, + useFallback ? debouncedAmount : undefined, + useFallback ? debouncedOtherCurrency : undefined + ) + + return useMemo( + () => ({ + ...(useFallback ? fallbackTrade : clientSORTrade), + ...(syncing ? { state: TradeState.SYNCING } : {}), + ...(debouncing ? { state: TradeState.LOADING } : {}), + }), + [debouncing, fallbackTrade, syncing, clientSORTrade, useFallback] + ) +} diff --git a/src/lib/hooks/swap/useSwapInfo.tsx b/src/lib/hooks/swap/useSwapInfo.tsx index cf12eb99a9..1ad0e56e91 100644 --- a/src/lib/hooks/swap/useSwapInfo.tsx +++ b/src/lib/hooks/swap/useSwapInfo.tsx @@ -12,8 +12,8 @@ import { ReactNode, useEffect, useMemo } from 'react' import { InterfaceTrade, TradeState } from 'state/routing/types' import { isAddress } from '../../../utils' -import useClientSideSmartOrderRouterTrade from '../routing/useClientSideSmartOrderRouterTrade' import useActiveWeb3React from '../useActiveWeb3React' +import { useBestTrade } from './useBestTrade' interface SwapInfo { currencies: { [field in Field]?: Currency } @@ -60,7 +60,7 @@ function useComputeSwapInfo(): SwapInfo { ) //@TODO(ianlapham): this would eventually be replaced with routing api logic. - const trade = useClientSideSmartOrderRouterTrade( + const trade = useBestTrade( isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT, parsedAmount, (isExactIn ? outputCurrency : inputCurrency) ?? undefined