From 8431ad91611de7c8a11a2b31299ad4e5084f922f Mon Sep 17 00:00:00 2001 From: Tina <59578595+tinaszheng@users.noreply.github.com> Date: Tue, 16 May 2023 16:33:46 -0400 Subject: [PATCH] chore: Refactor swap request flow (#6499) * Refactor swap quote flow with widget logic * remove console logging * add ignore path for serialization check and pass in native currencies for client side routing * apply stashed changes * revert node version change * remove TODO comment because maybe no longer relevant * update unit tests * wip: add snapshot test * add snapshot test for gas estimate badge * address PR comments: rename variables, fix client side router initialization * update Trade type * add TODO comment about isExactInput util * change | undefined convention to ? * PR comments * update type * remove client side initialization logic and replace with TODO * use routing-api for price fetching trades too * remove QuoteType.Initialized --- .../swap/AdvancedSwapDetails.test.tsx | 10 +- src/components/swap/AdvancedSwapDetails.tsx | 6 +- src/components/swap/ConfirmSwapModal.tsx | 7 +- src/components/swap/GasEstimateTooltip.tsx | 7 +- .../swap/SwapDetailsDropdown.test.tsx | 4 +- src/components/swap/SwapDetailsDropdown.tsx | 4 +- src/components/swap/SwapModalFooter.tsx | 8 +- src/components/swap/SwapModalHeader.tsx | 6 +- src/components/swap/SwapRoute.tsx | 13 +- .../AdvancedSwapDetails.test.tsx.snap | 50 ++- .../SwapDetailsDropdown.test.tsx.snap | 87 +++- .../SwapModalHeader.test.tsx.snap | 46 +- src/constants/chains.ts | 4 + src/hooks/useAutoSlippageTolerance.ts | 23 +- src/hooks/useBestTrade.test.ts | 18 - src/hooks/useBestTrade.ts | 2 +- src/hooks/useClientSideV3Trade.ts | 6 +- src/hooks/useUSDPrice.ts | 4 +- .../routing/clientSideSmartOrderRouter.ts | 63 ++- .../hooks/routing/useRoutingAPIArguments.ts | 7 +- src/lib/utils/analytics.ts | 6 +- src/nft/components/bag/BagFooter.tsx | 8 +- .../useDerivedPayWithAnyTokenSwapInfo.ts | 2 +- src/nft/hooks/usePayWithAnyTokenSwap.ts | 4 +- src/nft/hooks/usePriceImpact.ts | 4 +- src/nft/utils/tokenRoutes.ts | 4 +- src/pages/Swap/index.tsx | 11 +- src/state/index.ts | 10 +- src/state/routing/slice.ts | 73 ++- src/state/routing/types.ts | 50 ++- src/state/routing/useRoutingAPITrade.ts | 88 +--- src/state/routing/utils.test.ts | 419 +++++++++--------- src/state/routing/utils.ts | 184 +++++--- src/state/swap/hooks.tsx | 2 +- src/test-utils/constants.ts | 7 +- src/utils/getRoutingDiagramEntries.ts | 4 +- .../transformSwapRouteToGetQuoteResult.ts | 17 +- 37 files changed, 711 insertions(+), 557 deletions(-) diff --git a/src/components/swap/AdvancedSwapDetails.test.tsx b/src/components/swap/AdvancedSwapDetails.test.tsx index d90d860bf1..e4559db9a3 100644 --- a/src/components/swap/AdvancedSwapDetails.test.tsx +++ b/src/components/swap/AdvancedSwapDetails.test.tsx @@ -1,11 +1,5 @@ import userEvent from '@testing-library/user-event' -import { - TEST_ALLOWED_SLIPPAGE, - TEST_TOKEN_1, - TEST_TRADE_EXACT_INPUT, - TEST_TRADE_EXACT_OUTPUT, - toCurrencyAmount, -} from 'test-utils/constants' +import { TEST_ALLOWED_SLIPPAGE, TEST_TRADE_EXACT_INPUT, TEST_TRADE_EXACT_OUTPUT } from 'test-utils/constants' import { act, render, screen } from 'test-utils/render' import { AdvancedSwapDetails } from './AdvancedSwapDetails' @@ -27,7 +21,7 @@ describe('AdvancedSwapDetails.tsx', () => { }) it('renders correct tooltips for test trade with exact output and gas use estimate USD', async () => { - TEST_TRADE_EXACT_OUTPUT.gasUseEstimateUSD = toCurrencyAmount(TEST_TOKEN_1, 1) + TEST_TRADE_EXACT_OUTPUT.gasUseEstimateUSD = '1.00' render() await act(() => userEvent.hover(screen.getByText(/Maximum input/i))) expect(await screen.getByText(/The minimum amount you are guaranteed to receive./i)).toBeVisible() diff --git a/src/components/swap/AdvancedSwapDetails.tsx b/src/components/swap/AdvancedSwapDetails.tsx index 7345e5045b..1df1bd52bd 100644 --- a/src/components/swap/AdvancedSwapDetails.tsx +++ b/src/components/swap/AdvancedSwapDetails.tsx @@ -1,7 +1,7 @@ import { Trans } from '@lingui/macro' import { sendAnalyticsEvent } from '@uniswap/analytics' import { InterfaceElementName, SwapEventName } from '@uniswap/analytics-events' -import { Currency, Percent, TradeType } from '@uniswap/sdk-core' +import { Percent, TradeType } from '@uniswap/sdk-core' import { useWeb3React } from '@web3-react/core' import { LoadingRows } from 'components/Loader/styled' import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains' @@ -16,7 +16,7 @@ import RouterLabel from './RouterLabel' import SwapRoute from './SwapRoute' interface AdvancedSwapDetailsProps { - trade: InterfaceTrade + trade: InterfaceTrade allowedSlippage: Percent syncing?: boolean } @@ -60,7 +60,7 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }: - ~${trade.gasUseEstimateUSD.toFixed(2)} + ~${trade.gasUseEstimateUSD} )} diff --git a/src/components/swap/ConfirmSwapModal.tsx b/src/components/swap/ConfirmSwapModal.tsx index 8096c4859f..8bdd809f6a 100644 --- a/src/components/swap/ConfirmSwapModal.tsx +++ b/src/components/swap/ConfirmSwapModal.tsx @@ -1,8 +1,7 @@ import { Trans } from '@lingui/macro' import { Trace } from '@uniswap/analytics' import { InterfaceModalName } from '@uniswap/analytics-events' -import { Trade } from '@uniswap/router-sdk' -import { Currency, Percent, TradeType } from '@uniswap/sdk-core' +import { Percent } from '@uniswap/sdk-core' import { ReactNode, useCallback, useMemo, useState } from 'react' import { InterfaceTrade } from 'state/routing/types' import { tradeMeaningfullyDiffers } from 'utils/tradeMeaningFullyDiffer' @@ -31,8 +30,8 @@ export default function ConfirmSwapModal({ fiatValueOutput, }: { isOpen: boolean - trade: InterfaceTrade | undefined - originalTrade: Trade | undefined + trade: InterfaceTrade | undefined + originalTrade: InterfaceTrade | undefined attemptingTxn: boolean txHash: string | undefined recipient: string | null diff --git a/src/components/swap/GasEstimateTooltip.tsx b/src/components/swap/GasEstimateTooltip.tsx index d600d0c426..04d40dccea 100644 --- a/src/components/swap/GasEstimateTooltip.tsx +++ b/src/components/swap/GasEstimateTooltip.tsx @@ -1,6 +1,5 @@ import { sendAnalyticsEvent } from '@uniswap/analytics' import { InterfaceElementName, SwapEventName } from '@uniswap/analytics-events' -import { Currency, TradeType } from '@uniswap/sdk-core' import { LoadingOpacityContainer } from 'components/Loader/styled' import { RowFixed } from 'components/Row' import { MouseoverTooltip, TooltipSize } from 'components/Tooltip' @@ -26,14 +25,14 @@ export default function GasEstimateTooltip({ loading, disabled, }: { - trade: InterfaceTrade // dollar amount in active chain's stablecoin + trade: InterfaceTrade // dollar amount in active chain's stablecoin loading: boolean disabled?: boolean }) { const formattedGasPriceString = trade?.gasUseEstimateUSD - ? trade.gasUseEstimateUSD.toFixed(2) === '0.00' + ? trade.gasUseEstimateUSD === '0.00' ? '<$0.01' - : '$' + trade.gasUseEstimateUSD.toFixed(2) + : '$' + trade.gasUseEstimateUSD : undefined return ( diff --git a/src/components/swap/SwapDetailsDropdown.test.tsx b/src/components/swap/SwapDetailsDropdown.test.tsx index 74afab475e..5f70a47fca 100644 --- a/src/components/swap/SwapDetailsDropdown.test.tsx +++ b/src/components/swap/SwapDetailsDropdown.test.tsx @@ -1,5 +1,5 @@ import userEvent from '@testing-library/user-event' -import { TEST_ALLOWED_SLIPPAGE, TEST_TOKEN_1, TEST_TRADE_EXACT_INPUT, toCurrencyAmount } from 'test-utils/constants' +import { TEST_ALLOWED_SLIPPAGE, TEST_TRADE_EXACT_INPUT } from 'test-utils/constants' import { act, render, screen } from 'test-utils/render' import SwapDetailsDropdown from './SwapDetailsDropdown' @@ -25,7 +25,7 @@ describe('SwapDetailsDropdown.tsx', () => { }) it('is interactive once loaded', async () => { - TEST_TRADE_EXACT_INPUT.gasUseEstimateUSD = toCurrencyAmount(TEST_TOKEN_1, 1) + TEST_TRADE_EXACT_INPUT.gasUseEstimateUSD = '1.00' render( | undefined + trade: InterfaceTrade | undefined syncing: boolean loading: boolean allowedSlippage: Percent diff --git a/src/components/swap/SwapModalFooter.tsx b/src/components/swap/SwapModalFooter.tsx index 1af2bb3651..f5c536b3cc 100644 --- a/src/components/swap/SwapModalFooter.tsx +++ b/src/components/swap/SwapModalFooter.tsx @@ -1,7 +1,7 @@ import { Trans } from '@lingui/macro' import { TraceEvent } from '@uniswap/analytics' import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/analytics-events' -import { Currency, Percent, TradeType } from '@uniswap/sdk-core' +import { Percent } from '@uniswap/sdk-core' import useTransactionDeadline from 'hooks/useTransactionDeadline' import { formatPercentInBasisPointsNumber, @@ -24,7 +24,7 @@ import { AutoRow } from '../Row' import { SwapCallbackError } from './styleds' interface AnalyticsEventProps { - trade: InterfaceTrade + trade: InterfaceTrade hash: string | undefined allowedSlippage: Percent transactionDeadlineSecondsSinceEpoch: number | undefined @@ -75,7 +75,7 @@ const formatAnalyticsEventProperties = ({ fiatValueInput, fiatValueOutput, }: AnalyticsEventProps) => ({ - estimated_network_fee_usd: trade.gasUseEstimateUSD ? formatToDecimal(trade.gasUseEstimateUSD, 2) : undefined, + estimated_network_fee_usd: trade.gasUseEstimateUSD ?? undefined, transaction_hash: hash, transaction_deadline_seconds: getDurationUntilTimestampSeconds(transactionDeadlineSecondsSinceEpoch), token_in_address: getTokenAddress(trade.inputAmount.currency), @@ -112,7 +112,7 @@ export default function SwapModalFooter({ fiatValueInput, fiatValueOutput, }: { - trade: InterfaceTrade + trade: InterfaceTrade hash: string | undefined allowedSlippage: Percent onConfirm: () => void diff --git a/src/components/swap/SwapModalHeader.tsx b/src/components/swap/SwapModalHeader.tsx index c471ef3e6b..fd5743c755 100644 --- a/src/components/swap/SwapModalHeader.tsx +++ b/src/components/swap/SwapModalHeader.tsx @@ -1,7 +1,7 @@ import { Trans } from '@lingui/macro' import { sendAnalyticsEvent } from '@uniswap/analytics' import { SwapEventName, SwapPriceUpdateUserResponse } from '@uniswap/analytics-events' -import { Currency, Percent, TradeType } from '@uniswap/sdk-core' +import { Percent, TradeType } from '@uniswap/sdk-core' import { useUSDPrice } from 'hooks/useUSDPrice' import { getPriceUpdateBasisPoints } from 'lib/utils/analytics' import { useEffect, useState } from 'react' @@ -42,7 +42,7 @@ const ArrowWrapper = styled.div` ` const formatAnalyticsEventProperties = ( - trade: InterfaceTrade, + trade: InterfaceTrade, priceUpdate: number | undefined, response: SwapPriceUpdateUserResponse ) => ({ @@ -65,7 +65,7 @@ export default function SwapModalHeader({ showAcceptChanges, onAcceptChanges, }: { - trade: InterfaceTrade + trade: InterfaceTrade shouldLogModalCloseEvent: boolean setShouldLogModalCloseEvent: (shouldLog: boolean) => void allowedSlippage: Percent diff --git a/src/components/swap/SwapRoute.tsx b/src/components/swap/SwapRoute.tsx index 53c354fef7..45b020ba6d 100644 --- a/src/components/swap/SwapRoute.tsx +++ b/src/components/swap/SwapRoute.tsx @@ -1,5 +1,4 @@ import { Trans } from '@lingui/macro' -import { Currency, TradeType } from '@uniswap/sdk-core' import { useWeb3React } from '@web3-react/core' import Column from 'components/Column' import { LoadingRows } from 'components/Loader/styled' @@ -12,13 +11,7 @@ import getRoutingDiagramEntries from 'utils/getRoutingDiagramEntries' import RouterLabel from './RouterLabel' -export default function SwapRoute({ - trade, - syncing, -}: { - trade: InterfaceTrade - syncing: boolean -}) { +export default function SwapRoute({ trade, syncing }: { trade: InterfaceTrade; syncing: boolean }) { const { chainId } = useWeb3React() const autoRouterSupported = useAutoRouterSupported() @@ -28,9 +21,9 @@ export default function SwapRoute({ // TODO(WEB-3303) // Can `trade.gasUseEstimateUSD` be defined when `chainId` is not in `SUPPORTED_GAS_ESTIMATE_CHAIN_IDS`? trade.gasUseEstimateUSD && chainId && SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) - ? trade.gasUseEstimateUSD.toFixed(2) === '0.00' + ? trade.gasUseEstimateUSD === '0.00' ? '<$0.01' - : '$' + trade.gasUseEstimateUSD.toFixed(2) + : '$' + trade.gasUseEstimateUSD : undefined return ( diff --git a/src/components/swap/__snapshots__/AdvancedSwapDetails.test.tsx.snap b/src/components/swap/__snapshots__/AdvancedSwapDetails.test.tsx.snap index 6ba2eacca6..1fde48fabe 100644 --- a/src/components/swap/__snapshots__/AdvancedSwapDetails.test.tsx.snap +++ b/src/components/swap/__snapshots__/AdvancedSwapDetails.test.tsx.snap @@ -32,17 +32,17 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = ` justify-content: space-between; } -.c5 { +.c8 { width: -webkit-fit-content; width: -moz-fit-content; width: fit-content; } -.c7 { +.c6 { color: #7780A0; } -.c8 { +.c7 { color: #0D111C; } @@ -67,7 +67,7 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = ` gap: 12px; } -.c6 { +.c5 { display: inline-block; height: inherit; } @@ -82,14 +82,34 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = ` class="c2 c3 c4" >
+
+
+ Network fee +
+
+
+
+ ~$1.00 +
+ +
+
Minimum output
@@ -97,7 +117,7 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
0.00000000000000098 DEF
@@ -106,14 +126,14 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = ` class="c2 c3 c4" >
Expected output
@@ -121,7 +141,7 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
0.000000000000001 DEF
@@ -133,16 +153,16 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = ` class="c2 c3 c4" >
Order routing
Uniswap API
diff --git a/src/components/swap/__snapshots__/SwapDetailsDropdown.test.tsx.snap b/src/components/swap/__snapshots__/SwapDetailsDropdown.test.tsx.snap index 0b004ff4e8..6c1ee5f925 100644 --- a/src/components/swap/__snapshots__/SwapDetailsDropdown.test.tsx.snap +++ b/src/components/swap/__snapshots__/SwapDetailsDropdown.test.tsx.snap @@ -42,11 +42,11 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = ` color: #0D111C; } -.c15 { +.c12 { color: #7780A0; } -.c13 { +.c16 { width: 100%; height: 1px; background-color: #D2D9EE; @@ -66,7 +66,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = ` justify-content: flex-start; } -.c12 { +.c15 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -89,11 +89,20 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = ` transition: opacity 0.2s ease-in-out; } -.c14 { +.c10 { display: inline-block; height: inherit; } +.c11 { + margin-right: 4px; + height: 18px; +} + +.c11 > * { + stroke: #98A1C0; +} + .c8 { background-color: transparent; border: none; @@ -135,7 +144,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = ` cursor: pointer; } -.c10 { +.c13 { -webkit-transform: none; -ms-transform: none; transform: none; @@ -144,7 +153,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = ` transition: transform 0.1s linear; } -.c11 { +.c14 { padding-top: 12px; } @@ -184,8 +193,32 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
- +
+
+
+ + gas-icon.svg + +
+ $1.00 +
+
+
+
+
+
+
+
+
+
+ Network fee +
+
+
+
+ ~$1.00 +
+
@@ -223,11 +276,11 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = ` class="c2 c3 c6" >
Minimum output
@@ -247,11 +300,11 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = ` class="c2 c3 c6" >
Expected output
@@ -265,18 +318,18 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
Order routing
+
+
+ Network fee +
+
+
+
+ ~$1.00 +
+
+
+
Minimum output
@@ -454,14 +474,14 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp class="c5 c6 c7" >
Expected output
@@ -481,12 +501,12 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp class="c5 c6 c7" >
Order routing
Output is estimated. You will receive at least @@ -520,7 +540,7 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp style="padding: 12px 0px 0px 0px;" >
Output will be sent to | undefined -): Percent { +export default function useAutoSlippageTolerance(trade?: InterfaceTrade): Percent { const { chainId } = useWeb3React() const onL2 = chainId && L2_CHAIN_IDS.includes(chainId) const outputDollarValue = useStablecoinValue(trade?.outputAmount) const nativeGasPrice = useGasPrice() const gasEstimate = guesstimateGas(trade) + const gasEstimateUSD = useStablecoinAmountFromFiatValue(trade?.gasUseEstimateUSD) ?? null const nativeCurrency = useNativeCurrency(chainId) const nativeCurrencyPrice = useStablecoinPrice((trade && nativeCurrency) ?? undefined) @@ -100,9 +99,7 @@ export default function useAutoSlippageTolerance( // NOTE - dont use gas estimate for L2s yet - need to verify accuracy // if not, use local heuristic const dollarCostToUse = - chainId && SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) && trade?.gasUseEstimateUSD - ? trade.gasUseEstimateUSD - : dollarGasCost + chainId && SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) && gasEstimateUSD ? gasEstimateUSD : dollarGasCost if (outputDollarValue && dollarCostToUse) { // optimize for highest possible slippage without getting MEV'd @@ -121,5 +118,15 @@ export default function useAutoSlippageTolerance( } return DEFAULT_AUTO_SLIPPAGE - }, [trade, onL2, nativeGasPrice, gasEstimate, nativeCurrency, nativeCurrencyPrice, chainId, outputDollarValue]) + }, [ + trade, + onL2, + nativeGasPrice, + gasEstimate, + nativeCurrency, + nativeCurrencyPrice, + chainId, + gasEstimateUSD, + outputDollarValue, + ]) } diff --git a/src/hooks/useBestTrade.test.ts b/src/hooks/useBestTrade.test.ts index d642f804db..241daff080 100644 --- a/src/hooks/useBestTrade.test.ts +++ b/src/hooks/useBestTrade.test.ts @@ -83,15 +83,6 @@ describe('#useBestV3Trade ExactIn', () => { expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, undefined) expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined }) }) - - it('does not compute client side v3 trade if routing api is SYNCING', () => { - expectRouterMock(TradeState.SYNCING) - - const { result } = renderHook(() => useBestTrade(TradeType.EXACT_INPUT, USDCAmount, DAI)) - - expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, undefined) - expect(result.current).toEqual({ state: TradeState.SYNCING, trade: undefined }) - }) }) describe('when routing api is in error state', () => { @@ -167,15 +158,6 @@ describe('#useBestV3Trade ExactOut', () => { expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined) expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined }) }) - - it('does not compute client side v3 trade if routing api is SYNCING', () => { - expectRouterMock(TradeState.SYNCING) - - const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET)) - - expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined) - expect(result.current).toEqual({ state: TradeState.SYNCING, trade: undefined }) - }) }) describe('when routing api is in error state', () => { diff --git a/src/hooks/useBestTrade.ts b/src/hooks/useBestTrade.ts index a1db4074d3..533f11a9ba 100644 --- a/src/hooks/useBestTrade.ts +++ b/src/hooks/useBestTrade.ts @@ -23,7 +23,7 @@ export function useBestTrade( otherCurrency?: Currency ): { state: TradeState - trade: InterfaceTrade | undefined + trade?: InterfaceTrade } { const { chainId } = useWeb3React() const autoRouterSupported = useAutoRouterSupported() diff --git a/src/hooks/useClientSideV3Trade.ts b/src/hooks/useClientSideV3Trade.ts index 02b2cd1928..6283eb3d59 100644 --- a/src/hooks/useClientSideV3Trade.ts +++ b/src/hooks/useClientSideV3Trade.ts @@ -5,7 +5,7 @@ import { SupportedChainId } from 'constants/chains' import JSBI from 'jsbi' import { useSingleContractWithCallData } from 'lib/hooks/multicall' import { useMemo } from 'react' -import { InterfaceTrade, TradeState } from 'state/routing/types' +import { ClassicTrade, InterfaceTrade, TradeState } from 'state/routing/types' import { isCelo } from '../constants/tokens' import { useAllV3Routes } from './useAllV3Routes' @@ -33,7 +33,7 @@ export function useClientSideV3Trade( tradeType: TTradeType, amountSpecified?: CurrencyAmount, otherCurrency?: Currency -): { state: TradeState; trade: InterfaceTrade | undefined } { +): { state: TradeState; trade: InterfaceTrade | undefined } { const [currencyIn, currencyOut] = tradeType === TradeType.EXACT_INPUT ? [amountSpecified?.currency, otherCurrency] @@ -135,7 +135,7 @@ export function useClientSideV3Trade( return { state: TradeState.VALID, - trade: new InterfaceTrade({ + trade: new ClassicTrade({ v2Routes: [], v3Routes: [ { diff --git a/src/hooks/useUSDPrice.ts b/src/hooks/useUSDPrice.ts index 37db91e5c9..56782ba2f8 100644 --- a/src/hooks/useUSDPrice.ts +++ b/src/hooks/useUSDPrice.ts @@ -41,8 +41,8 @@ function useETHValue(currencyAmount?: CurrencyAmount): { } } - if (!trade || !currencyAmount?.currency || !isGqlSupportedChain(chainId)) { - return { data: undefined, isLoading: state === TradeState.LOADING || state === TradeState.SYNCING } + if (!trade || state === TradeState.LOADING || !currencyAmount?.currency || !isGqlSupportedChain(chainId)) { + return { data: undefined, isLoading: state === TradeState.LOADING } } const { numerator, denominator } = trade.routes[0].midPrice diff --git a/src/lib/hooks/routing/clientSideSmartOrderRouter.ts b/src/lib/hooks/routing/clientSideSmartOrderRouter.ts index 44ec7cb305..82fe4022e8 100644 --- a/src/lib/hooks/routing/clientSideSmartOrderRouter.ts +++ b/src/lib/hooks/routing/clientSideSmartOrderRouter.ts @@ -3,8 +3,10 @@ import { BigintIsh, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core' // eslint-disable-next-line no-restricted-imports import { AlphaRouter, AlphaRouterConfig, ChainId } from '@uniswap/smart-order-router' import { SupportedChainId } from 'constants/chains' +import { nativeOnChain } from 'constants/tokens' import JSBI from 'jsbi' -import { GetQuoteResult } from 'state/routing/types' +import { GetQuoteArgs } from 'state/routing/slice' +import { QuoteResult, QuoteState, SwapRouterNativeAssets } from 'state/routing/types' import { transformSwapRouteToGetQuoteResult } from 'utils/transformSwapRouteToGetQuoteResult' export function toSupportedChainId(chainId: ChainId): SupportedChainId | undefined { @@ -19,50 +21,41 @@ export function isSupportedChainId(chainId: ChainId | undefined): boolean { async function getQuote( { - type, + tradeType, tokenIn, tokenOut, amount: amountRaw, }: { - type: 'exactIn' | 'exactOut' + tradeType: TradeType tokenIn: { address: string; chainId: number; decimals: number; symbol?: string } tokenOut: { address: string; chainId: number; decimals: number; symbol?: string } amount: BigintIsh }, router: AlphaRouter, - config: Partial -): Promise<{ data: GetQuoteResult; error?: unknown }> { - const currencyIn = new Token(tokenIn.chainId, tokenIn.address, tokenIn.decimals, tokenIn.symbol) - const currencyOut = new Token(tokenOut.chainId, tokenOut.address, tokenOut.decimals, tokenOut.symbol) + routerConfig: Partial +): Promise { + const tokenInIsNative = Object.values(SwapRouterNativeAssets).includes(tokenIn.address as SwapRouterNativeAssets) + const tokenOutIsNative = Object.values(SwapRouterNativeAssets).includes(tokenOut.address as SwapRouterNativeAssets) + + const currencyIn = tokenInIsNative + ? nativeOnChain(tokenIn.chainId) + : new Token(tokenIn.chainId, tokenIn.address, tokenIn.decimals, tokenIn.symbol) + const currencyOut = tokenOutIsNative + ? nativeOnChain(tokenOut.chainId) + : new Token(tokenOut.chainId, tokenOut.address, tokenOut.decimals, tokenOut.symbol) + + const baseCurrency = tradeType === TradeType.EXACT_INPUT ? currencyIn : currencyOut + const quoteCurrency = tradeType === TradeType.EXACT_INPUT ? currencyOut : currencyIn - const baseCurrency = type === 'exactIn' ? currencyIn : currencyOut - const quoteCurrency = type === 'exactIn' ? currencyOut : currencyIn const amount = CurrencyAmount.fromRawAmount(baseCurrency, JSBI.BigInt(amountRaw)) + // TODO (WEB-2055): explore initializing client side routing on first load (when amountRaw is null) if there are enough users using client-side router preference. + const swapRoute = await router.route(amount, quoteCurrency, tradeType, /*swapConfig=*/ undefined, routerConfig) - const swapRoute = await router.route( - amount, - quoteCurrency, - type === 'exactIn' ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT, - /*swapConfig=*/ undefined, - config - ) + if (!swapRoute) { + return { state: QuoteState.NOT_FOUND } + } - if (!swapRoute) throw new Error('Failed to generate client side quote') - - return { data: transformSwapRouteToGetQuoteResult(type, amount, swapRoute) } -} - -interface QuoteArguments { - tokenInAddress: string - tokenInChainId: ChainId - tokenInDecimals: number - tokenInSymbol?: string - tokenOutAddress: string - tokenOutChainId: ChainId - tokenOutDecimals: number - tokenOutSymbol?: string - amount: string - type: 'exactIn' | 'exactOut' + return transformSwapRouteToGetQuoteResult(tradeType, amount, swapRoute) } export async function getClientSideQuote( @@ -76,14 +69,14 @@ export async function getClientSideQuote( tokenOutDecimals, tokenOutSymbol, amount, - type, - }: QuoteArguments, + tradeType, + }: GetQuoteArgs, router: AlphaRouter, config: Partial ) { return getQuote( { - type, + tradeType, tokenIn: { address: tokenInAddress, chainId: tokenInChainId, diff --git a/src/lib/hooks/routing/useRoutingAPIArguments.ts b/src/lib/hooks/routing/useRoutingAPIArguments.ts index 72d455bbd9..e97504a2a8 100644 --- a/src/lib/hooks/routing/useRoutingAPIArguments.ts +++ b/src/lib/hooks/routing/useRoutingAPIArguments.ts @@ -1,6 +1,7 @@ import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' import { useMemo } from 'react' import { INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference } from 'state/routing/slice' +import { currencyAddressForSwapQuote } from 'state/routing/utils' /** * Returns query arguments for the Routing API query or undefined if the @@ -26,16 +27,16 @@ export function useRoutingAPIArguments({ ? undefined : { amount: amount.quotient.toString(), - tokenInAddress: tokenIn.wrapped.address, + tokenInAddress: currencyAddressForSwapQuote(tokenIn), tokenInChainId: tokenIn.wrapped.chainId, tokenInDecimals: tokenIn.wrapped.decimals, tokenInSymbol: tokenIn.wrapped.symbol, - tokenOutAddress: tokenOut.wrapped.address, + tokenOutAddress: currencyAddressForSwapQuote(tokenOut), tokenOutChainId: tokenOut.wrapped.chainId, tokenOutDecimals: tokenOut.wrapped.decimals, tokenOutSymbol: tokenOut.wrapped.symbol, routerPreference, - type: (tradeType === TradeType.EXACT_INPUT ? 'exactIn' : 'exactOut') as 'exactIn' | 'exactOut', + tradeType, }, [amount, routerPreference, tokenIn, tokenOut, tradeType] ) diff --git a/src/lib/utils/analytics.ts b/src/lib/utils/analytics.ts index f27a6cb76c..a835fbfdce 100644 --- a/src/lib/utils/analytics.ts +++ b/src/lib/utils/analytics.ts @@ -39,7 +39,7 @@ export const formatSwapSignedAnalyticsEventProperties = ({ fiatValues, txHash, }: { - trade: InterfaceTrade | Trade + trade: InterfaceTrade | Trade fiatValues: { amountIn: number | undefined; amountOut: number | undefined } txHash: string }) => ({ @@ -61,7 +61,7 @@ export const formatSwapSignedAnalyticsEventProperties = ({ export const formatSwapQuoteReceivedEventProperties = ( trade: Trade, - gasUseEstimateUSD?: CurrencyAmount, + gasUseEstimateUSD?: string, fetchingSwapQuoteStartTime?: Date ) => { return { @@ -70,7 +70,7 @@ export const formatSwapQuoteReceivedEventProperties = ( token_in_address: getTokenAddress(trade.inputAmount.currency), token_out_address: getTokenAddress(trade.outputAmount.currency), price_impact_basis_points: trade ? formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)) : undefined, - estimated_network_fee_usd: gasUseEstimateUSD ? formatToDecimal(gasUseEstimateUSD, 2) : undefined, + estimated_network_fee_usd: gasUseEstimateUSD, chain_id: trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId ? trade.inputAmount.currency.chainId diff --git a/src/nft/components/bag/BagFooter.tsx b/src/nft/components/bag/BagFooter.tsx index 2b24105251..1a51800afc 100644 --- a/src/nft/components/bag/BagFooter.tsx +++ b/src/nft/components/bag/BagFooter.tsx @@ -3,7 +3,7 @@ import { formatEther, parseEther } from '@ethersproject/units' import { t, Trans } from '@lingui/macro' import { sendAnalyticsEvent, TraceEvent } from '@uniswap/analytics' import { BrowserEvent, InterfaceElementName, NFTEventName } from '@uniswap/analytics-events' -import { Currency, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core' +import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core' import { useWeb3React } from '@web3-react/core' import { useToggleAccountDrawer } from 'components/AccountDrawer' import Column from 'components/Column' @@ -208,7 +208,7 @@ const InputCurrencyValue = ({ totalEthPrice: BigNumber activeCurrency: Currency | undefined | null tradeState: TradeState - trade: InterfaceTrade | undefined + trade: InterfaceTrade | undefined }) => { if (!usingPayWithAnyToken) { return ( @@ -219,7 +219,7 @@ const InputCurrencyValue = ({ ) } - if (tradeState === TradeState.LOADING) { + if (tradeState === TradeState.LOADING && !trade) { return ( Fetching price... @@ -228,7 +228,7 @@ const InputCurrencyValue = ({ } return ( - + {ethNumberStandardFormatter(trade?.inputAmount.toExact())} ) diff --git a/src/nft/hooks/useDerivedPayWithAnyTokenSwapInfo.ts b/src/nft/hooks/useDerivedPayWithAnyTokenSwapInfo.ts index 9bf8413bff..09473ecd89 100644 --- a/src/nft/hooks/useDerivedPayWithAnyTokenSwapInfo.ts +++ b/src/nft/hooks/useDerivedPayWithAnyTokenSwapInfo.ts @@ -9,7 +9,7 @@ export default function useDerivedPayWithAnyTokenSwapInfo( parsedOutputAmount?: CurrencyAmount ): { state: TradeState - trade: InterfaceTrade | undefined + trade: InterfaceTrade | undefined maximumAmountIn: CurrencyAmount | undefined allowedSlippage: Percent } { diff --git a/src/nft/hooks/usePayWithAnyTokenSwap.ts b/src/nft/hooks/usePayWithAnyTokenSwap.ts index e70bdeb7f8..7fa46b08ae 100644 --- a/src/nft/hooks/usePayWithAnyTokenSwap.ts +++ b/src/nft/hooks/usePayWithAnyTokenSwap.ts @@ -1,4 +1,4 @@ -import { Currency, Percent, TradeType } from '@uniswap/sdk-core' +import { Percent } from '@uniswap/sdk-core' import { PermitInput, TokenTradeRoutesInput, TokenTradeType } from 'graphql/data/__generated__/types-and-hooks' import { Allowance } from 'hooks/usePermit2Allowance' import { buildAllTradeRouteInputs } from 'nft/utils/tokenRoutes' @@ -8,7 +8,7 @@ import { InterfaceTrade } from 'state/routing/types' import { useTokenInput } from './useTokenInput' export default function usePayWithAnyTokenSwap( - trade?: InterfaceTrade | undefined, + trade?: InterfaceTrade | undefined, allowance?: Allowance, allowedSlippage?: Percent ) { diff --git a/src/nft/hooks/usePriceImpact.ts b/src/nft/hooks/usePriceImpact.ts index 328756f4d4..9422e1c625 100644 --- a/src/nft/hooks/usePriceImpact.ts +++ b/src/nft/hooks/usePriceImpact.ts @@ -1,4 +1,4 @@ -import { Currency, Percent, TradeType } from '@uniswap/sdk-core' +import { Percent } from '@uniswap/sdk-core' import { useMemo } from 'react' import { InterfaceTrade } from 'state/routing/types' import { useTheme } from 'styled-components/macro' @@ -14,7 +14,7 @@ interface PriceImpactSeverity { color: string } -export function usePriceImpact(trade?: InterfaceTrade): PriceImpact | undefined { +export function usePriceImpact(trade?: InterfaceTrade): PriceImpact | undefined { const theme = useTheme() return useMemo(() => { diff --git a/src/nft/utils/tokenRoutes.ts b/src/nft/utils/tokenRoutes.ts index 42f1ccbee8..60a0f4da21 100644 --- a/src/nft/utils/tokenRoutes.ts +++ b/src/nft/utils/tokenRoutes.ts @@ -1,5 +1,5 @@ import { IRoute, Protocol } from '@uniswap/router-sdk' -import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' +import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { Pair } from '@uniswap/v2-sdk' import { Pool } from '@uniswap/v3-sdk' import { TokenAmountInput, TokenTradeRouteInput, TradePoolInput } from 'graphql/data/__generated__/types-and-hooks' @@ -108,7 +108,7 @@ function buildTradeRouteInput(swap: Swap): TokenTradeRouteInput { } } -export function buildAllTradeRouteInputs(trade: InterfaceTrade): { +export function buildAllTradeRouteInputs(trade: InterfaceTrade): { mixedTokenTradeRouteInputs: TokenTradeRouteInput[] | undefined v2TokenTradeRouteInputs: TokenTradeRouteInput[] | undefined v3TokenTradeRouteInputs: TokenTradeRouteInput[] | undefined diff --git a/src/pages/Swap/index.tsx b/src/pages/Swap/index.tsx index 2064c6f590..b45b794416 100644 --- a/src/pages/Swap/index.tsx +++ b/src/pages/Swap/index.tsx @@ -8,8 +8,7 @@ import { InterfaceSectionName, SwapEventName, } from '@uniswap/analytics-events' -import { Trade } from '@uniswap/router-sdk' -import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core' +import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core' import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk' import { useWeb3React } from '@web3-react/core' import { useToggleAccountDrawer } from 'components/AccountDrawer' @@ -115,11 +114,11 @@ const OutputSwapSection = styled(SwapSection)` ` function getIsValidSwapQuote( - trade: InterfaceTrade | undefined, + trade: InterfaceTrade | undefined, tradeState: TradeState, swapInputError?: ReactNode ): boolean { - return !!swapInputError && !!trade && (tradeState === TradeState.VALID || tradeState === TradeState.SYNCING) + return Boolean(swapInputError && trade && tradeState === TradeState.VALID) } function largerPercentValue(a?: Percent, b?: Percent) { @@ -293,7 +292,7 @@ export function Swap({ const fiatValueOutput = useUSDPrice(parsedAmounts[Field.OUTPUT]) const [routeNotFound, routeIsLoading, routeIsSyncing] = useMemo( - () => [!trade?.swaps, TradeState.LOADING === tradeState, TradeState.SYNCING === tradeState], + () => [!trade?.swaps, TradeState.LOADING === tradeState, TradeState.LOADING === tradeState && Boolean(trade)], [trade, tradeState] ) @@ -336,7 +335,7 @@ export function Swap({ // modal and loading const [{ showConfirm, tradeToConfirm, swapErrorMessage, attemptingTxn, txHash }, setSwapState] = useState<{ showConfirm: boolean - tradeToConfirm: Trade | undefined + tradeToConfirm: InterfaceTrade | undefined attemptingTxn: boolean swapErrorMessage: string | undefined txHash: string | undefined diff --git a/src/state/index.ts b/src/state/index.ts index 7da06c5e47..d36c815f02 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -14,7 +14,15 @@ const store = configureStore({ reducer, enhancers: (defaultEnhancers) => defaultEnhancers.concat(sentryEnhancer), middleware: (getDefaultMiddleware) => - getDefaultMiddleware({ thunk: true }) + getDefaultMiddleware({ + thunk: true, + serializableCheck: { + // meta.arg and meta.baseQueryMeta are defaults. payload.trade is a nonserializable return value, but that's ok + // because we are not adding it into any persisted store that requires serialization (e.g. localStorage) + ignoredActionPaths: ['meta.arg', 'meta.baseQueryMeta', 'payload.trade'], + ignoredPaths: [routingApi.reducerPath], + }, + }) .concat(routingApi.middleware) .concat(save({ states: PERSISTED_KEYS, debounce: 1000 })), preloadedState: load({ states: PERSISTED_KEYS, disableWarnings: isTestEnv() }), diff --git a/src/state/routing/slice.ts b/src/state/routing/slice.ts index 0ee3087a08..4a3d4b11e2 100644 --- a/src/state/routing/slice.ts +++ b/src/state/routing/slice.ts @@ -1,5 +1,6 @@ import { createApi, fetchBaseQuery, FetchBaseQueryError } from '@reduxjs/toolkit/query/react' import { Protocol } from '@uniswap/router-sdk' +import { TradeType } from '@uniswap/sdk-core' import { AlphaRouter, ChainId } from '@uniswap/smart-order-router' import { RPC_PROVIDERS } from 'constants/providers' import { getClientSideQuote, toSupportedChainId } from 'lib/hooks/routing/clientSideSmartOrderRouter' @@ -7,7 +8,8 @@ import ms from 'ms.macro' import qs from 'qs' import { trace } from 'tracing/trace' -import { GetQuoteResult } from './types' +import { QuoteData, TradeResult } from './types' +import { isExactInput, transformRoutesToTrade } from './utils' export enum RouterPreference { AUTO = 'auto', @@ -69,7 +71,7 @@ const PRICE_PARAMS = { distributionPercent: 100, } -interface GetQuoteArgs { +export interface GetQuoteArgs { tokenInAddress: string tokenInChainId: ChainId tokenInDecimals: number @@ -80,7 +82,12 @@ interface GetQuoteArgs { tokenOutSymbol?: string amount: string routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE - type: 'exactIn' | 'exactOut' + tradeType: TradeType +} + +enum QuoteState { + SUCCESS = 'Success', + NOT_FOUND = 'Not found', } export const routingApi = createApi({ @@ -89,7 +96,7 @@ export const routingApi = createApi({ baseUrl: 'https://api.uniswap.org/v1/', }), endpoints: (build) => ({ - getQuote: build.query({ + getQuote: build.query({ async onQueryStarted(args: GetQuoteArgs, { queryFulfilled }) { trace( 'quote', @@ -119,11 +126,14 @@ export const routingApi = createApi({ ) }, async queryFn(args, _api, _extraOptions, fetch) { - const { tokenInAddress, tokenInChainId, tokenOutAddress, tokenOutChainId, amount, routerPreference, type } = - args - - try { - if (routerPreference === RouterPreference.API || routerPreference === RouterPreference.AUTO) { + if ( + args.routerPreference === RouterPreference.API || + args.routerPreference === RouterPreference.AUTO || + args.routerPreference === INTERNAL_ROUTER_PREFERENCE_PRICE + ) { + try { + const { tokenInAddress, tokenInChainId, tokenOutAddress, tokenOutChainId, amount, tradeType } = args + const type = isExactInput(tradeType) ? 'exactIn' : 'exactOut' const query = qs.stringify({ ...API_QUERY_PARAMS, tokenInAddress, @@ -133,21 +143,40 @@ export const routingApi = createApi({ amount, type, }) - return (await fetch(`quote?${query}`)) as { data: GetQuoteResult } | { error: FetchBaseQueryError } - } else { - const router = getRouter(args.tokenInChainId) - return await getClientSideQuote( - args, - router, - // TODO(zzmp): Use PRICE_PARAMS for RouterPreference.PRICE. - // This change is intentionally being deferred to first see what effect router caching has. - CLIENT_PARAMS + const response = await fetch(`quote?${query}`) + if (response.error) { + try { + // cast as any here because we do a runtime check on it being an object before indexing into .errorCode + const errorData = response.error.data as any + // NO_ROUTE should be treated as a valid response to prevent retries. + if (typeof errorData === 'object' && errorData?.errorCode === 'NO_ROUTE') { + return { data: { state: QuoteState.NOT_FOUND } } + } + } catch { + throw response.error + } + } + + const quoteData = response.data as QuoteData + const tradeResult = transformRoutesToTrade(args, quoteData) + return { data: tradeResult } + } catch (error: any) { + console.warn( + `GetQuote failed on routing API, falling back to client: ${error?.message ?? error?.detail ?? error}` ) } - } catch (error) { - // TODO: fall back to client-side quoter when auto router fails. - // deprecate 'legacy' v2/v3 routers first. - return { error: { status: 'CUSTOM_ERROR', error: error.toString() } } + } + try { + const router = getRouter(args.tokenInChainId) + const quoteResult = await getClientSideQuote(args, router, CLIENT_PARAMS) + if (quoteResult.state === QuoteState.SUCCESS) { + return { data: transformRoutesToTrade(args, quoteResult.data) } + } else { + return { data: quoteResult } + } + } catch (error: any) { + console.warn(`GetQuote failed on client: ${error}`) + return { error: { status: 'CUSTOM_ERROR', error: error?.detail ?? error?.message ?? error } } } }, keepUnusedDataFor: ms`10s`, diff --git a/src/state/routing/types.ts b/src/state/routing/types.ts index 35da496089..dd78fb8cad 100644 --- a/src/state/routing/types.ts +++ b/src/state/routing/types.ts @@ -8,7 +8,6 @@ export enum TradeState { INVALID, NO_ROUTE_FOUND, VALID, - SYNCING, } // from https://github.com/Uniswap/routing-api/blob/main/lib/handlers/schema.ts @@ -49,7 +48,7 @@ export type V2PoolInRoute = { address?: string } -export interface GetQuoteResult { +export interface QuoteData { quoteId?: string blockNumber: string amount: string @@ -68,12 +67,12 @@ export interface GetQuoteResult { routeString: string } -export class InterfaceTrade< +export class ClassicTrade< TInput extends Currency, TOutput extends Currency, TTradeType extends TradeType > extends Trade { - gasUseEstimateUSD: CurrencyAmount | null | undefined + gasUseEstimateUSD: string | null | undefined blockNumber: string | null | undefined constructor({ @@ -81,8 +80,8 @@ export class InterfaceTrade< blockNumber, ...routes }: { - gasUseEstimateUSD?: CurrencyAmount | undefined | null - blockNumber?: string | null | undefined + gasUseEstimateUSD?: string | null + blockNumber?: string | null v2Routes: { routev2: V2Route inputAmount: CurrencyAmount @@ -105,3 +104,42 @@ export class InterfaceTrade< this.gasUseEstimateUSD = gasUseEstimateUSD } } + +export type InterfaceTrade = ClassicTrade + +export enum QuoteState { + SUCCESS = 'Success', + NOT_FOUND = 'Not found', +} + +export type QuoteResult = + | { + state: QuoteState.NOT_FOUND + data?: undefined + } + | { + state: QuoteState.SUCCESS + data: QuoteData + } + +export type TradeResult = + | { + state: QuoteState.NOT_FOUND + trade?: undefined + } + | { + state: QuoteState.SUCCESS + trade: InterfaceTrade + } + +export enum PoolType { + V2Pool = 'v2-pool', + V3Pool = 'v3-pool', +} + +// swap router API special cases these strings to represent native currencies +// all chains have "ETH" as native currency symbol except for polygon +export enum SwapRouterNativeAssets { + MATIC = 'MATIC', + ETH = 'ETH', +} diff --git a/src/state/routing/useRoutingAPITrade.ts b/src/state/routing/useRoutingAPITrade.ts index 4a8e1af492..07ce9dad29 100644 --- a/src/state/routing/useRoutingAPITrade.ts +++ b/src/state/routing/useRoutingAPITrade.ts @@ -3,14 +3,16 @@ import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' import { IMetric, MetricLoggerUnit, setGlobalMetric } from '@uniswap/smart-order-router' import { sendTiming } from 'components/analytics' import { AVERAGE_L1_BLOCK_TIME } from 'constants/chainInfo' -import { useStablecoinAmountFromFiatValue } from 'hooks/useStablecoinPrice' import { useRoutingAPIArguments } from 'lib/hooks/routing/useRoutingAPIArguments' import ms from 'ms.macro' import { useMemo } from 'react' import { INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference, useGetQuoteQuery } from 'state/routing/slice' -import { InterfaceTrade, TradeState } from './types' -import { computeRoutes, transformRoutesToTrade } from './utils' +import { InterfaceTrade, QuoteState, TradeState } from './types' + +const TRADE_INVALID = { state: TradeState.INVALID, trade: undefined } as const +const TRADE_NOT_FOUND = { state: TradeState.NO_ROUTE_FOUND, trade: undefined } as const +const TRADE_LOADING = { state: TradeState.LOADING, trade: undefined } as const /** * Returns the best trade by invoking the routing api or the smart order router on the client @@ -25,7 +27,7 @@ export function useRoutingAPITrade( routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE ): { state: TradeState - trade: InterfaceTrade | undefined + trade?: InterfaceTrade } { const [currencyIn, currencyOut]: [Currency | undefined, Currency | undefined] = useMemo( () => @@ -44,10 +46,9 @@ export function useRoutingAPITrade( }) const { - isLoading, isError, - data: quoteResult, - currentData, + data: tradeResult, + currentData: currentTradeResult, } = useGetQuoteQuery(queryArgs ?? skipToken, { // Price-fetching is informational and costly, so it's done less frequently. pollingInterval: routerPreference === INTERNAL_ROUTER_PREFERENCE_PRICE ? ms`1m` : AVERAGE_L1_BLOCK_TIME, @@ -55,72 +56,23 @@ export function useRoutingAPITrade( refetchOnMountOrArgChange: 2 * 60, }) - 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 isSyncing = currentData !== quoteResult + const isCurrent = currentTradeResult === tradeResult return useMemo(() => { - if (!currencyIn || !currencyOut || currencyIn.equals(currencyOut)) { + if (!amountSpecified || isError || !queryArgs) { + return TRADE_INVALID + } else if (tradeResult?.state === QuoteState.NOT_FOUND && isCurrent) { + return TRADE_NOT_FOUND + } else if (!tradeResult?.trade) { + // TODO(WEB-3307): use `isLoading` returned by rtk-query hook instead of checking for `trade` status + return TRADE_LOADING + } else { return { - state: TradeState.INVALID, - trade: undefined, + state: isCurrent ? TradeState.VALID : TradeState.LOADING, + trade: tradeResult.trade, } } - - if (isLoading && !quoteResult) { - // only on first hook render - return { - state: TradeState.LOADING, - trade: undefined, - } - } - - let otherAmount = undefined - if (quoteResult) { - if (tradeType === TradeType.EXACT_INPUT && currencyOut) { - otherAmount = CurrencyAmount.fromRawAmount(currencyOut, quoteResult.quote) - } - - if (tradeType === TradeType.EXACT_OUTPUT && currencyIn) { - otherAmount = CurrencyAmount.fromRawAmount(currencyIn, quoteResult.quote) - } - } - - if (isError || !otherAmount || !route || route.length === 0 || !queryArgs) { - return { - state: TradeState.NO_ROUTE_FOUND, - trade: undefined, - } - } - - try { - const trade = transformRoutesToTrade(route, tradeType, quoteResult?.blockNumber, gasUseEstimateUSD) - return { - // always return VALID regardless of isFetching status - state: isSyncing ? TradeState.SYNCING : TradeState.VALID, - trade, - } - } catch (e) { - return { state: TradeState.INVALID, trade: undefined } - } - }, [ - currencyIn, - currencyOut, - quoteResult, - isLoading, - tradeType, - isError, - route, - queryArgs, - gasUseEstimateUSD, - isSyncing, - ]) + }, [amountSpecified, isCurrent, isError, queryArgs, tradeResult]) } // only want to enable this when app hook called diff --git a/src/state/routing/utils.test.ts b/src/state/routing/utils.test.ts index 2a7d7dd071..c24bf2d542 100644 --- a/src/state/routing/utils.test.ts +++ b/src/state/routing/utils.test.ts @@ -1,51 +1,42 @@ -import { Token, TradeType } from '@uniswap/sdk-core' +import { Token } from '@uniswap/sdk-core' +import { SupportedChainId } from 'constants/chains' +import { nativeOnChain } from 'constants/tokens' -import { nativeOnChain } from '../../constants/tokens' +import { PoolType } from './types' import { computeRoutes } from './utils' const USDC = new Token(1, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC') const DAI = new Token(1, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 6, 'DAI') const MKR = new Token(1, '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2', 6, 'MKR') -const ETH = nativeOnChain(1) +const ETH = nativeOnChain(SupportedChainId.MAINNET) // helper function to make amounts more readable const amount = (raw: TemplateStringsArray) => (parseInt(raw[0]) * 1e6).toString() describe('#useRoute', () => { - it('handles an undefined payload', () => { - const result = computeRoutes(undefined, undefined, TradeType.EXACT_INPUT, undefined) - - expect(result).toBeUndefined() - }) - it('handles empty edges and nodes', () => { - const result = computeRoutes(USDC, DAI, TradeType.EXACT_INPUT, { - route: [], - }) - + const result = computeRoutes(false, false, []) expect(result).toEqual([]) }) it('handles a single route trade from DAI to USDC from v3', () => { - const result = computeRoutes(DAI, USDC, TradeType.EXACT_INPUT, { - route: [ - [ - { - type: 'v3-pool', - address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2', - amountIn: amount`1`, - amountOut: amount`5`, - fee: '500', - sqrtRatioX96: '2437312313659959819381354528', - liquidity: '10272714736694327408', - tickCurrent: '-69633', - tokenIn: DAI, - tokenOut: USDC, - }, - ], + const result = computeRoutes(false, false, [ + [ + { + type: 'v3-pool', + address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2', + amountIn: amount`1`, + amountOut: amount`5`, + fee: '500', + sqrtRatioX96: '2437312313659959819381354528', + liquidity: '10272714736694327408', + tickCurrent: '-69633', + tokenIn: DAI, + tokenOut: USDC, + }, ], - }) + ]) const r = result?.[0] @@ -60,28 +51,26 @@ describe('#useRoute', () => { }) it('handles a single route trade from DAI to USDC from v2', () => { - const result = computeRoutes(DAI, USDC, TradeType.EXACT_INPUT, { - route: [ - [ - { - type: 'v2-pool', - address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2', - amountIn: amount`1`, - amountOut: amount`5`, - tokenIn: DAI, - tokenOut: USDC, - reserve0: { - token: DAI, - quotient: amount`100`, - }, - reserve1: { - token: USDC, - quotient: amount`200`, - }, + const result = computeRoutes(false, false, [ + [ + { + type: 'v2-pool', + address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2', + amountIn: amount`1`, + amountOut: amount`5`, + tokenIn: DAI, + tokenOut: USDC, + reserve0: { + token: DAI, + quotient: amount`100`, }, - ], + reserve1: { + token: USDC, + quotient: amount`200`, + }, + }, ], - }) + ]) const r = result?.[0] @@ -96,54 +85,52 @@ describe('#useRoute', () => { }) it('handles a multi-route trade from DAI to USDC', () => { - const result = computeRoutes(DAI, USDC, TradeType.EXACT_OUTPUT, { - route: [ - [ - { - type: 'v2-pool', - address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2', - amountIn: amount`5`, - amountOut: amount`6`, - tokenIn: DAI, - tokenOut: USDC, - reserve0: { - token: DAI, - quotient: amount`1000`, - }, - reserve1: { - token: USDC, - quotient: amount`500`, - }, + const result = computeRoutes(false, false, [ + [ + { + type: 'v2-pool', + address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2', + amountIn: amount`5`, + amountOut: amount`6`, + tokenIn: DAI, + tokenOut: USDC, + reserve0: { + token: DAI, + quotient: amount`1000`, }, - ], - [ - { - type: 'v3-pool', - address: '0x2f8F72aA9304c8B593d555F12eF6589cC3A579A2', - amountIn: amount`10`, - amountOut: amount`1`, - fee: '3000', - tokenIn: DAI, - tokenOut: MKR, - sqrtRatioX96: '2437312313659959819381354528', - liquidity: '10272714736694327408', - tickCurrent: '-69633', + reserve1: { + token: USDC, + quotient: amount`500`, }, - { - type: 'v3-pool', - address: '0x3f8F72aA9304c8B593d555F12eF6589cC3A579A2', - amountIn: amount`1`, - amountOut: amount`200`, - fee: '10000', - tokenIn: MKR, - tokenOut: USDC, - sqrtRatioX96: '2437312313659959819381354528', - liquidity: '10272714736694327408', - tickCurrent: '-69633', - }, - ], + }, ], - }) + [ + { + type: 'v3-pool', + address: '0x2f8F72aA9304c8B593d555F12eF6589cC3A579A2', + amountIn: amount`10`, + amountOut: amount`1`, + fee: '3000', + tokenIn: DAI, + tokenOut: MKR, + sqrtRatioX96: '2437312313659959819381354528', + liquidity: '10272714736694327408', + tickCurrent: '-69633', + }, + { + type: 'v3-pool', + address: '0x3f8F72aA9304c8B593d555F12eF6589cC3A579A2', + amountIn: amount`1`, + amountOut: amount`200`, + fee: '10000', + tokenIn: MKR, + tokenOut: USDC, + sqrtRatioX96: '2437312313659959819381354528', + liquidity: '10272714736694327408', + tickCurrent: '-69633', + }, + ], + ]) expect(result).toBeDefined() expect(result?.length).toBe(2) @@ -165,38 +152,36 @@ describe('#useRoute', () => { }) it('handles a single route trade with same token pair, different fee tiers', () => { - const result = computeRoutes(DAI, USDC, TradeType.EXACT_INPUT, { - route: [ - [ - { - type: 'v3-pool', - address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2', - amountIn: amount`1`, - amountOut: amount`5`, - fee: '500', - tokenIn: DAI, - tokenOut: USDC, - sqrtRatioX96: '2437312313659959819381354528', - liquidity: '10272714736694327408', - tickCurrent: '-69633', - }, - ], - [ - { - type: 'v3-pool', - address: '0x2f8F72aA9304c8B593d555F12eF6589cC3A579A2', - amountIn: amount`10`, - amountOut: amount`50`, - fee: '3000', - tokenIn: DAI, - tokenOut: USDC, - sqrtRatioX96: '2437312313659959819381354528', - liquidity: '10272714736694327408', - tickCurrent: '-69633', - }, - ], + const result = computeRoutes(false, false, [ + [ + { + type: 'v3-pool', + address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2', + amountIn: amount`1`, + amountOut: amount`5`, + fee: '500', + tokenIn: DAI, + tokenOut: USDC, + sqrtRatioX96: '2437312313659959819381354528', + liquidity: '10272714736694327408', + tickCurrent: '-69633', + }, ], - }) + [ + { + type: 'v3-pool', + address: '0x2f8F72aA9304c8B593d555F12eF6589cC3A579A2', + amountIn: amount`10`, + amountOut: amount`50`, + fee: '3000', + tokenIn: DAI, + tokenOut: USDC, + sqrtRatioX96: '2437312313659959819381354528', + liquidity: '10272714736694327408', + tickCurrent: '-69633', + }, + ], + ]) expect(result).toBeDefined() expect(result?.length).toBe(2) @@ -206,28 +191,68 @@ describe('#useRoute', () => { expect(result?.[0].inputAmount.toSignificant()).toBe('1') }) + it('computes mixed routes correctly', () => { + const result = computeRoutes(false, false, [ + [ + { + type: PoolType.V3Pool, + address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2', + amountIn: amount`1`, + amountOut: amount`5`, + fee: '500', + tokenIn: DAI, + tokenOut: USDC, + sqrtRatioX96: '2437312313659959819381354528', + liquidity: '10272714736694327408', + tickCurrent: '-69633', + }, + { + type: PoolType.V2Pool, + address: 'x2f8F72aA9304c8B593d555F12eF6589cC3A579A2', + amountIn: amount`10`, + amountOut: amount`50`, + tokenIn: USDC, + tokenOut: MKR, + reserve0: { + token: USDC, + quotient: amount`100`, + }, + reserve1: { + token: MKR, + quotient: amount`200`, + }, + }, + ], + ]) + + expect(result).toBeDefined() + expect(result?.length).toBe(1) + expect(result?.[0].routev3).toBeNull() + expect(result?.[0].routev2).toBeNull() + expect(result?.[0].mixedRoute?.output).toStrictEqual(MKR) + expect(result?.[0].inputAmount.toSignificant()).toBe('1') + }) + describe('with ETH', () => { it('outputs native ETH as input currency', () => { const WETH = ETH.wrapped - const result = computeRoutes(ETH, USDC, TradeType.EXACT_OUTPUT, { - route: [ - [ - { - type: 'v3-pool', - address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2', - amountIn: (1e18).toString(), - amountOut: amount`5`, - fee: '500', - sqrtRatioX96: '2437312313659959819381354528', - liquidity: '10272714736694327408', - tickCurrent: '-69633', - tokenIn: WETH, - tokenOut: USDC, - }, - ], + const result = computeRoutes(true, false, [ + [ + { + type: 'v3-pool', + address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2', + amountIn: (1e18).toString(), + amountOut: amount`5`, + fee: '500', + sqrtRatioX96: '2437312313659959819381354528', + liquidity: '10272714736694327408', + tickCurrent: '-69633', + tokenIn: WETH, + tokenOut: USDC, + }, ], - }) + ]) expect(result).toBeDefined() expect(result?.length).toBe(1) @@ -239,24 +264,22 @@ describe('#useRoute', () => { it('outputs native ETH as output currency', () => { const WETH = new Token(1, ETH.wrapped.address, 18, 'WETH') - const result = computeRoutes(USDC, ETH, TradeType.EXACT_OUTPUT, { - route: [ - [ - { - type: 'v3-pool', - address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2', - amountIn: amount`5`, - amountOut: (1e18).toString(), - fee: '500', - sqrtRatioX96: '2437312313659959819381354528', - liquidity: '10272714736694327408', - tickCurrent: '-69633', - tokenIn: USDC, - tokenOut: WETH, - }, - ], + const result = computeRoutes(false, true, [ + [ + { + type: 'v3-pool', + address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2', + amountIn: amount`5`, + amountOut: (1e18).toString(), + fee: '500', + sqrtRatioX96: '2437312313659959819381354528', + liquidity: '10272714736694327408', + tickCurrent: '-69633', + tokenIn: USDC, + tokenOut: WETH, + }, ], - }) + ]) expect(result?.length).toBe(1) expect(result?.[0].routev3?.input).toStrictEqual(USDC) @@ -268,28 +291,26 @@ describe('#useRoute', () => { it('outputs native ETH as input currency for v2 routes', () => { const WETH = ETH.wrapped - const result = computeRoutes(ETH, USDC, TradeType.EXACT_OUTPUT, { - route: [ - [ - { - type: 'v2-pool', - address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2', - amountIn: (1e18).toString(), - amountOut: amount`5`, - tokenIn: WETH, - tokenOut: USDC, - reserve0: { - token: WETH, - quotient: amount`100`, - }, - reserve1: { - token: USDC, - quotient: amount`200`, - }, + const result = computeRoutes(true, false, [ + [ + { + type: 'v2-pool', + address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2', + amountIn: (1e18).toString(), + amountOut: amount`5`, + tokenIn: WETH, + tokenOut: USDC, + reserve0: { + token: WETH, + quotient: amount`100`, }, - ], + reserve1: { + token: USDC, + quotient: amount`200`, + }, + }, ], - }) + ]) expect(result).toBeDefined() expect(result?.length).toBe(1) @@ -301,28 +322,26 @@ describe('#useRoute', () => { it('outputs native ETH as output currency for v2 routes', () => { const WETH = new Token(1, ETH.wrapped.address, 18, 'WETH') - const result = computeRoutes(USDC, ETH, TradeType.EXACT_OUTPUT, { - route: [ - [ - { - type: 'v2-pool', - address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2', - amountIn: amount`5`, - amountOut: (1e18).toString(), - tokenIn: USDC, - tokenOut: WETH, - reserve0: { - token: WETH, - quotient: amount`100`, - }, - reserve1: { - token: USDC, - quotient: amount`200`, - }, + const result = computeRoutes(false, true, [ + [ + { + type: 'v2-pool', + address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2', + amountIn: amount`5`, + amountOut: (1e18).toString(), + tokenIn: USDC, + tokenOut: WETH, + reserve0: { + token: WETH, + quotient: amount`100`, }, - ], + reserve1: { + token: USDC, + quotient: amount`200`, + }, + }, ], - }) + ]) expect(result?.length).toBe(1) expect(result?.[0].routev2?.input).toStrictEqual(USDC) diff --git a/src/state/routing/utils.ts b/src/state/routing/utils.ts index 57cd3d9c3f..473ee2208d 100644 --- a/src/state/routing/utils.ts +++ b/src/state/routing/utils.ts @@ -1,32 +1,50 @@ -import { MixedRouteSDK, Protocol } from '@uniswap/router-sdk' +import { MixedRouteSDK } from '@uniswap/router-sdk' import { Currency, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core' import { Pair, Route as V2Route } from '@uniswap/v2-sdk' import { FeeAmount, Pool, Route as V3Route } from '@uniswap/v3-sdk' +import { isPolygonChain } from 'constants/chains' +import { nativeOnChain } from 'constants/tokens' -import { GetQuoteResult, InterfaceTrade, V2PoolInRoute, V3PoolInRoute } from './types' +import { GetQuoteArgs } from './slice' +import { + ClassicTrade, + PoolType, + QuoteData, + QuoteState, + SwapRouterNativeAssets, + TradeResult, + V2PoolInRoute, + V3PoolInRoute, +} from './types' /** - * Transforms a Routing API quote into an array of routes that can be used to create - * a `Trade`. + * Transforms a Routing API quote into an array of routes that can be used to + * create a `Trade`. */ export function computeRoutes( - currencyIn: Currency | undefined, - currencyOut: Currency | undefined, - tradeType: TradeType, - quoteResult: Pick | undefined -) { - if (!quoteResult || !quoteResult.route || !currencyIn || !currencyOut) return undefined + tokenInIsNative: boolean, + tokenOutIsNative: boolean, + routes: QuoteData['route'] +): + | { + routev3: V3Route | null + routev2: V2Route | null + mixedRoute: MixedRouteSDK | null + inputAmount: CurrencyAmount + outputAmount: CurrencyAmount + }[] + | undefined { + if (routes.length === 0) return [] - if (quoteResult.route.length === 0) return [] + const tokenIn = routes[0]?.[0]?.tokenIn + const tokenOut = routes[0]?.[routes[0]?.length - 1]?.tokenOut + if (!tokenIn || !tokenOut) throw new Error('Expected both tokenIn and tokenOut to be present') - const parsedTokenIn = parseToken(quoteResult.route[0][0].tokenIn) - const parsedTokenOut = parseToken(quoteResult.route[0][quoteResult.route[0].length - 1].tokenOut) - if (parsedTokenIn.address !== currencyIn.wrapped.address) return undefined - if (parsedTokenOut.address !== currencyOut.wrapped.address) return undefined - if (parsedTokenIn.wrapped.equals(parsedTokenOut.wrapped)) return undefined + const parsedCurrencyIn = tokenInIsNative ? nativeOnChain(tokenIn.chainId) : parseToken(tokenIn) + const parsedCurrencyOut = tokenOutIsNative ? nativeOnChain(tokenOut.chainId) : parseToken(tokenOut) try { - return quoteResult.route.map((route) => { + return routes.map((route) => { if (route.length === 0) { throw new Error('Expected route to have at least one pair or pool') } @@ -37,68 +55,90 @@ export function computeRoutes( throw new Error('Expected both amountIn and amountOut to be present') } - const routeProtocol = getRouteProtocol(route) + const isOnlyV2 = isVersionedRoute(PoolType.V2Pool, route) + const isOnlyV3 = isVersionedRoute(PoolType.V3Pool, route) return { - routev3: - routeProtocol === Protocol.V3 - ? new V3Route(route.map(genericPoolPairParser) as Pool[], currencyIn, currencyOut) - : null, - routev2: - routeProtocol === Protocol.V2 - ? new V2Route(route.map(genericPoolPairParser) as Pair[], currencyIn, currencyOut) - : null, + routev3: isOnlyV3 ? new V3Route(route.map(parsePool), parsedCurrencyIn, parsedCurrencyOut) : null, + routev2: isOnlyV2 ? new V2Route(route.map(parsePair), parsedCurrencyIn, parsedCurrencyOut) : null, mixedRoute: - routeProtocol === Protocol.MIXED - ? new MixedRouteSDK(route.map(genericPoolPairParser), currencyIn, currencyOut) + !isOnlyV3 && !isOnlyV2 + ? new MixedRouteSDK(route.map(parsePoolOrPair), parsedCurrencyIn, parsedCurrencyOut) : null, - inputAmount: CurrencyAmount.fromRawAmount(currencyIn, rawAmountIn), - outputAmount: CurrencyAmount.fromRawAmount(currencyOut, rawAmountOut), + inputAmount: CurrencyAmount.fromRawAmount(parsedCurrencyIn, rawAmountIn), + outputAmount: CurrencyAmount.fromRawAmount(parsedCurrencyOut, rawAmountOut), } }) } catch (e) { - // `Route` constructor may throw if inputs/outputs are temporarily out of sync - // (RTK-Query always returns the latest data which may not be the right inputs/outputs) - // This is not fatal and will fix itself in future render cycles - console.error(e) - return undefined + console.error('Error computing routes', e) + return } } -export function transformRoutesToTrade( - route: ReturnType, - tradeType: TTradeType, - blockNumber?: string | null, - gasUseEstimateUSD?: CurrencyAmount | null -): InterfaceTrade { - return new InterfaceTrade({ - v2Routes: - route - ?.filter((r): r is typeof route[0] & { routev2: NonNullable } => r.routev2 !== null) - .map(({ routev2, inputAmount, outputAmount }) => ({ routev2, inputAmount, outputAmount })) ?? [], - v3Routes: - route - ?.filter((r): r is typeof route[0] & { routev3: NonNullable } => r.routev3 !== null) - .map(({ routev3, inputAmount, outputAmount }) => ({ routev3, inputAmount, outputAmount })) ?? [], - mixedRoutes: - route - ?.filter( - (r): r is typeof route[0] & { mixedRoute: NonNullable } => - r.mixedRoute !== null - ) - .map(({ mixedRoute, inputAmount, outputAmount }) => ({ mixedRoute, inputAmount, outputAmount })) ?? [], - tradeType, - gasUseEstimateUSD, - blockNumber, - }) +const parsePoolOrPair = (pool: V3PoolInRoute | V2PoolInRoute): Pool | Pair => { + return pool.type === PoolType.V3Pool ? parsePool(pool) : parsePair(pool) } -const parseToken = ({ address, chainId, decimals, symbol }: GetQuoteResult['route'][0][0]['tokenIn']): Token => { +function isVersionedRoute( + type: T['type'], + route: (V3PoolInRoute | V2PoolInRoute)[] +): route is T[] { + return route.every((pool) => pool.type === type) +} + +export function transformRoutesToTrade(args: GetQuoteArgs, data: QuoteData): TradeResult { + const { tokenInAddress, tokenOutAddress, tradeType } = args + const tokenInIsNative = Object.values(SwapRouterNativeAssets).includes(tokenInAddress as SwapRouterNativeAssets) + const tokenOutIsNative = Object.values(SwapRouterNativeAssets).includes(tokenOutAddress as SwapRouterNativeAssets) + const { gasUseEstimateUSD, blockNumber } = data + const routes = computeRoutes(tokenInIsNative, tokenOutIsNative, data.route) + + const trade = new ClassicTrade({ + v2Routes: + routes + ?.filter( + (r): r is typeof routes[0] & { routev2: NonNullable } => r.routev2 !== null + ) + .map(({ routev2, inputAmount, outputAmount }) => ({ + routev2, + inputAmount, + outputAmount, + })) ?? [], + v3Routes: + routes + ?.filter( + (r): r is typeof routes[0] & { routev3: NonNullable } => r.routev3 !== null + ) + .map(({ routev3, inputAmount, outputAmount }) => ({ + routev3, + inputAmount, + outputAmount, + })) ?? [], + mixedRoutes: + routes + ?.filter( + (r): r is typeof routes[0] & { mixedRoute: NonNullable } => + r.mixedRoute !== null + ) + .map(({ mixedRoute, inputAmount, outputAmount }) => ({ + mixedRoute, + inputAmount, + outputAmount, + })) ?? [], + tradeType, + gasUseEstimateUSD: parseFloat(gasUseEstimateUSD).toFixed(2).toString(), + blockNumber, + }) + + return { state: QuoteState.SUCCESS, trade } +} + +function parseToken({ address, chainId, decimals, symbol }: QuoteData['route'][0][0]['tokenIn']): Token { return new Token(chainId, address, parseInt(decimals.toString()), symbol) } -const parsePool = ({ fee, sqrtRatioX96, liquidity, tickCurrent, tokenIn, tokenOut }: V3PoolInRoute): Pool => - new Pool( +function parsePool({ fee, sqrtRatioX96, liquidity, tickCurrent, tokenIn, tokenOut }: V3PoolInRoute): Pool { + return new Pool( parseToken(tokenIn), parseToken(tokenOut), parseInt(fee) as FeeAmount, @@ -106,6 +146,7 @@ const parsePool = ({ fee, sqrtRatioX96, liquidity, tickCurrent, tokenIn, tokenOu liquidity, parseInt(tickCurrent) ) +} const parsePair = ({ reserve0, reserve1 }: V2PoolInRoute): Pair => new Pair( @@ -113,12 +154,15 @@ const parsePair = ({ reserve0, reserve1 }: V2PoolInRoute): Pair => CurrencyAmount.fromRawAmount(parseToken(reserve1.token), reserve1.quotient) ) -const genericPoolPairParser = (pool: V3PoolInRoute | V2PoolInRoute): Pool | Pair => { - return pool.type === 'v3-pool' ? parsePool(pool) : parsePair(pool) +// TODO(WEB-2050): Convert other instances of tradeType comparison to use this utility function +export function isExactInput(tradeType: TradeType): boolean { + return tradeType === TradeType.EXACT_INPUT } -function getRouteProtocol(route: (V3PoolInRoute | V2PoolInRoute)[]): Protocol { - if (route.every((pool) => pool.type === 'v2-pool')) return Protocol.V2 - if (route.every((pool) => pool.type === 'v3-pool')) return Protocol.V3 - return Protocol.MIXED +export function currencyAddressForSwapQuote(currency: Currency): string { + if (currency.isNative) { + return isPolygonChain(currency.chainId) ? SwapRouterNativeAssets.MATIC : SwapRouterNativeAssets.ETH + } + + return currency.address } diff --git a/src/state/swap/hooks.tsx b/src/state/swap/hooks.tsx index 751a8457d2..ea172fe76e 100644 --- a/src/state/swap/hooks.tsx +++ b/src/state/swap/hooks.tsx @@ -81,7 +81,7 @@ export function useDerivedSwapInfo( parsedAmount: CurrencyAmount | undefined inputError?: ReactNode trade: { - trade: InterfaceTrade | undefined + trade?: InterfaceTrade state: TradeState } allowedSlippage: Percent diff --git a/src/test-utils/constants.ts b/src/test-utils/constants.ts index a4ca99a523..6d7e432d5d 100644 --- a/src/test-utils/constants.ts +++ b/src/test-utils/constants.ts @@ -2,7 +2,7 @@ import { CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core' import { V3Route } from '@uniswap/smart-order-router' import { FeeAmount, Pool } from '@uniswap/v3-sdk' import JSBI from 'jsbi' -import { InterfaceTrade } from 'state/routing/types' +import { ClassicTrade } from 'state/routing/types' export const TEST_TOKEN_1 = new Token(1, '0x0000000000000000000000000000000000000001', 18, 'ABC', 'Abc') export const TEST_TOKEN_2 = new Token(1, '0x0000000000000000000000000000000000000002', 18, 'DEF', 'Def') @@ -30,7 +30,7 @@ export const TEST_POOL_13 = new Pool( export const toCurrencyAmount = (token: Token, amount: number) => CurrencyAmount.fromRawAmount(token, JSBI.BigInt(amount)) -export const TEST_TRADE_EXACT_INPUT = new InterfaceTrade({ +export const TEST_TRADE_EXACT_INPUT = new ClassicTrade({ v3Routes: [ { routev3: new V3Route([TEST_POOL_12], TEST_TOKEN_1, TEST_TOKEN_2), @@ -40,9 +40,10 @@ export const TEST_TRADE_EXACT_INPUT = new InterfaceTrade({ ], v2Routes: [], tradeType: TradeType.EXACT_INPUT, + gasUseEstimateUSD: '1.00', }) -export const TEST_TRADE_EXACT_OUTPUT = new InterfaceTrade({ +export const TEST_TRADE_EXACT_OUTPUT = new ClassicTrade({ v3Routes: [ { routev3: new V3Route([TEST_POOL_13], TEST_TOKEN_1, TEST_TOKEN_3), diff --git a/src/utils/getRoutingDiagramEntries.ts b/src/utils/getRoutingDiagramEntries.ts index 7a795bea11..782d2a52e1 100644 --- a/src/utils/getRoutingDiagramEntries.ts +++ b/src/utils/getRoutingDiagramEntries.ts @@ -15,9 +15,7 @@ const V2_DEFAULT_FEE_TIER = 3000 /** * Loops through all routes on a trade and returns an array of diagram entries. */ -export default function getRoutingDiagramEntries( - trade: InterfaceTrade -): RoutingDiagramEntry[] { +export default function getRoutingDiagramEntries(trade: InterfaceTrade): RoutingDiagramEntry[] { return trade.swaps.map(({ route: { path: tokenPath, pools, protocol }, inputAmount, outputAmount }) => { const portion = trade.tradeType === TradeType.EXACT_INPUT diff --git a/src/utils/transformSwapRouteToGetQuoteResult.ts b/src/utils/transformSwapRouteToGetQuoteResult.ts index 6a1d8502ec..ce92c8c9af 100644 --- a/src/utils/transformSwapRouteToGetQuoteResult.ts +++ b/src/utils/transformSwapRouteToGetQuoteResult.ts @@ -1,12 +1,13 @@ import { Protocol } from '@uniswap/router-sdk' -import { Currency, CurrencyAmount } from '@uniswap/sdk-core' +import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' import { routeAmountsToString, SwapRoute } from '@uniswap/smart-order-router' import { Pool } from '@uniswap/v3-sdk' -import { GetQuoteResult, V2PoolInRoute, V3PoolInRoute } from 'state/routing/types' +import { QuoteResult, QuoteState } from 'state/routing/types' +import { QuoteData, V2PoolInRoute, V3PoolInRoute } from 'state/routing/types' // from routing-api (https://github.com/Uniswap/routing-api/blob/main/lib/handlers/quote/quote.ts#L243-L311) export function transformSwapRouteToGetQuoteResult( - type: 'exactIn' | 'exactOut', + tradeType: TradeType, amount: CurrencyAmount, { quote, @@ -19,7 +20,7 @@ export function transformSwapRouteToGetQuoteResult( methodParameters, blockNumber, }: SwapRoute -): GetQuoteResult { +): QuoteResult { const routeResponse: Array<(V3PoolInRoute | V2PoolInRoute)[]> = [] for (const subRoute of route) { @@ -34,12 +35,12 @@ export function transformSwapRouteToGetQuoteResult( let edgeAmountIn = undefined if (i === 0) { - edgeAmountIn = type === 'exactIn' ? amount.quotient.toString() : quote.quotient.toString() + edgeAmountIn = tradeType === TradeType.EXACT_INPUT ? amount.quotient.toString() : quote.quotient.toString() } let edgeAmountOut = undefined if (i === pools.length - 1) { - edgeAmountOut = type === 'exactIn' ? quote.quotient.toString() : amount.quotient.toString() + edgeAmountOut = tradeType === TradeType.EXACT_INPUT ? quote.quotient.toString() : amount.quotient.toString() } if (nextPool instanceof Pool) { @@ -109,7 +110,7 @@ export function transformSwapRouteToGetQuoteResult( routeResponse.push(curRoute) } - const result: GetQuoteResult = { + const result: QuoteData = { methodParameters, blockNumber: blockNumber.toString(), amount: amount.quotient.toString(), @@ -127,5 +128,5 @@ export function transformSwapRouteToGetQuoteResult( routeString: routeAmountsToString(route), } - return result + return { state: QuoteState.SUCCESS, data: result } }