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
This commit is contained in:
Tina 2023-05-16 16:33:46 -04:00 committed by GitHub
parent fd1aded517
commit 8431ad9161
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 711 additions and 557 deletions

@ -1,11 +1,5 @@
import userEvent from '@testing-library/user-event' import userEvent from '@testing-library/user-event'
import { import { TEST_ALLOWED_SLIPPAGE, TEST_TRADE_EXACT_INPUT, TEST_TRADE_EXACT_OUTPUT } from 'test-utils/constants'
TEST_ALLOWED_SLIPPAGE,
TEST_TOKEN_1,
TEST_TRADE_EXACT_INPUT,
TEST_TRADE_EXACT_OUTPUT,
toCurrencyAmount,
} from 'test-utils/constants'
import { act, render, screen } from 'test-utils/render' import { act, render, screen } from 'test-utils/render'
import { AdvancedSwapDetails } from './AdvancedSwapDetails' 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 () => { 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(<AdvancedSwapDetails trade={TEST_TRADE_EXACT_OUTPUT} allowedSlippage={TEST_ALLOWED_SLIPPAGE} />) render(<AdvancedSwapDetails trade={TEST_TRADE_EXACT_OUTPUT} allowedSlippage={TEST_ALLOWED_SLIPPAGE} />)
await act(() => userEvent.hover(screen.getByText(/Maximum input/i))) await act(() => userEvent.hover(screen.getByText(/Maximum input/i)))
expect(await screen.getByText(/The minimum amount you are guaranteed to receive./i)).toBeVisible() expect(await screen.getByText(/The minimum amount you are guaranteed to receive./i)).toBeVisible()

@ -1,7 +1,7 @@
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { sendAnalyticsEvent } from '@uniswap/analytics' import { sendAnalyticsEvent } from '@uniswap/analytics'
import { InterfaceElementName, SwapEventName } from '@uniswap/analytics-events' 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 { useWeb3React } from '@web3-react/core'
import { LoadingRows } from 'components/Loader/styled' import { LoadingRows } from 'components/Loader/styled'
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains' import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
@ -16,7 +16,7 @@ import RouterLabel from './RouterLabel'
import SwapRoute from './SwapRoute' import SwapRoute from './SwapRoute'
interface AdvancedSwapDetailsProps { interface AdvancedSwapDetailsProps {
trade: InterfaceTrade<Currency, Currency, TradeType> trade: InterfaceTrade
allowedSlippage: Percent allowedSlippage: Percent
syncing?: boolean syncing?: boolean
} }
@ -60,7 +60,7 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }:
</ThemedText.BodySmall> </ThemedText.BodySmall>
</MouseoverTooltip> </MouseoverTooltip>
<TextWithLoadingPlaceholder syncing={syncing} width={50}> <TextWithLoadingPlaceholder syncing={syncing} width={50}>
<ThemedText.BodySmall>~${trade.gasUseEstimateUSD.toFixed(2)}</ThemedText.BodySmall> <ThemedText.BodySmall>~${trade.gasUseEstimateUSD}</ThemedText.BodySmall>
</TextWithLoadingPlaceholder> </TextWithLoadingPlaceholder>
</RowBetween> </RowBetween>
)} )}

@ -1,8 +1,7 @@
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { Trace } from '@uniswap/analytics' import { Trace } from '@uniswap/analytics'
import { InterfaceModalName } from '@uniswap/analytics-events' import { InterfaceModalName } from '@uniswap/analytics-events'
import { Trade } from '@uniswap/router-sdk' import { Percent } from '@uniswap/sdk-core'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { ReactNode, useCallback, useMemo, useState } from 'react' import { ReactNode, useCallback, useMemo, useState } from 'react'
import { InterfaceTrade } from 'state/routing/types' import { InterfaceTrade } from 'state/routing/types'
import { tradeMeaningfullyDiffers } from 'utils/tradeMeaningFullyDiffer' import { tradeMeaningfullyDiffers } from 'utils/tradeMeaningFullyDiffer'
@ -31,8 +30,8 @@ export default function ConfirmSwapModal({
fiatValueOutput, fiatValueOutput,
}: { }: {
isOpen: boolean isOpen: boolean
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined trade: InterfaceTrade | undefined
originalTrade: Trade<Currency, Currency, TradeType> | undefined originalTrade: InterfaceTrade | undefined
attemptingTxn: boolean attemptingTxn: boolean
txHash: string | undefined txHash: string | undefined
recipient: string | null recipient: string | null

@ -1,6 +1,5 @@
import { sendAnalyticsEvent } from '@uniswap/analytics' import { sendAnalyticsEvent } from '@uniswap/analytics'
import { InterfaceElementName, SwapEventName } from '@uniswap/analytics-events' import { InterfaceElementName, SwapEventName } from '@uniswap/analytics-events'
import { Currency, TradeType } from '@uniswap/sdk-core'
import { LoadingOpacityContainer } from 'components/Loader/styled' import { LoadingOpacityContainer } from 'components/Loader/styled'
import { RowFixed } from 'components/Row' import { RowFixed } from 'components/Row'
import { MouseoverTooltip, TooltipSize } from 'components/Tooltip' import { MouseoverTooltip, TooltipSize } from 'components/Tooltip'
@ -26,14 +25,14 @@ export default function GasEstimateTooltip({
loading, loading,
disabled, disabled,
}: { }: {
trade: InterfaceTrade<Currency, Currency, TradeType> // dollar amount in active chain's stablecoin trade: InterfaceTrade // dollar amount in active chain's stablecoin
loading: boolean loading: boolean
disabled?: boolean disabled?: boolean
}) { }) {
const formattedGasPriceString = trade?.gasUseEstimateUSD const formattedGasPriceString = trade?.gasUseEstimateUSD
? trade.gasUseEstimateUSD.toFixed(2) === '0.00' ? trade.gasUseEstimateUSD === '0.00'
? '<$0.01' ? '<$0.01'
: '$' + trade.gasUseEstimateUSD.toFixed(2) : '$' + trade.gasUseEstimateUSD
: undefined : undefined
return ( return (

@ -1,5 +1,5 @@
import userEvent from '@testing-library/user-event' 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 { act, render, screen } from 'test-utils/render'
import SwapDetailsDropdown from './SwapDetailsDropdown' import SwapDetailsDropdown from './SwapDetailsDropdown'
@ -25,7 +25,7 @@ describe('SwapDetailsDropdown.tsx', () => {
}) })
it('is interactive once loaded', async () => { it('is interactive once loaded', async () => {
TEST_TRADE_EXACT_INPUT.gasUseEstimateUSD = toCurrencyAmount(TEST_TOKEN_1, 1) TEST_TRADE_EXACT_INPUT.gasUseEstimateUSD = '1.00'
render( render(
<SwapDetailsDropdown <SwapDetailsDropdown
trade={TEST_TRADE_EXACT_INPUT} trade={TEST_TRADE_EXACT_INPUT}

@ -1,7 +1,7 @@
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { TraceEvent } from '@uniswap/analytics' import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/analytics-events' import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/analytics-events'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core' import { Percent } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import AnimatedDropdown from 'components/AnimatedDropdown' import AnimatedDropdown from 'components/AnimatedDropdown'
import Column from 'components/Column' import Column from 'components/Column'
@ -92,7 +92,7 @@ const Wrapper = styled(Column)`
` `
interface SwapDetailsInlineProps { interface SwapDetailsInlineProps {
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined trade: InterfaceTrade | undefined
syncing: boolean syncing: boolean
loading: boolean loading: boolean
allowedSlippage: Percent allowedSlippage: Percent

@ -1,7 +1,7 @@
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { TraceEvent } from '@uniswap/analytics' import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/analytics-events' 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 useTransactionDeadline from 'hooks/useTransactionDeadline'
import { import {
formatPercentInBasisPointsNumber, formatPercentInBasisPointsNumber,
@ -24,7 +24,7 @@ import { AutoRow } from '../Row'
import { SwapCallbackError } from './styleds' import { SwapCallbackError } from './styleds'
interface AnalyticsEventProps { interface AnalyticsEventProps {
trade: InterfaceTrade<Currency, Currency, TradeType> trade: InterfaceTrade
hash: string | undefined hash: string | undefined
allowedSlippage: Percent allowedSlippage: Percent
transactionDeadlineSecondsSinceEpoch: number | undefined transactionDeadlineSecondsSinceEpoch: number | undefined
@ -75,7 +75,7 @@ const formatAnalyticsEventProperties = ({
fiatValueInput, fiatValueInput,
fiatValueOutput, fiatValueOutput,
}: AnalyticsEventProps) => ({ }: AnalyticsEventProps) => ({
estimated_network_fee_usd: trade.gasUseEstimateUSD ? formatToDecimal(trade.gasUseEstimateUSD, 2) : undefined, estimated_network_fee_usd: trade.gasUseEstimateUSD ?? undefined,
transaction_hash: hash, transaction_hash: hash,
transaction_deadline_seconds: getDurationUntilTimestampSeconds(transactionDeadlineSecondsSinceEpoch), transaction_deadline_seconds: getDurationUntilTimestampSeconds(transactionDeadlineSecondsSinceEpoch),
token_in_address: getTokenAddress(trade.inputAmount.currency), token_in_address: getTokenAddress(trade.inputAmount.currency),
@ -112,7 +112,7 @@ export default function SwapModalFooter({
fiatValueInput, fiatValueInput,
fiatValueOutput, fiatValueOutput,
}: { }: {
trade: InterfaceTrade<Currency, Currency, TradeType> trade: InterfaceTrade
hash: string | undefined hash: string | undefined
allowedSlippage: Percent allowedSlippage: Percent
onConfirm: () => void onConfirm: () => void

@ -1,7 +1,7 @@
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { sendAnalyticsEvent } from '@uniswap/analytics' import { sendAnalyticsEvent } from '@uniswap/analytics'
import { SwapEventName, SwapPriceUpdateUserResponse } from '@uniswap/analytics-events' 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 { useUSDPrice } from 'hooks/useUSDPrice'
import { getPriceUpdateBasisPoints } from 'lib/utils/analytics' import { getPriceUpdateBasisPoints } from 'lib/utils/analytics'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
@ -42,7 +42,7 @@ const ArrowWrapper = styled.div`
` `
const formatAnalyticsEventProperties = ( const formatAnalyticsEventProperties = (
trade: InterfaceTrade<Currency, Currency, TradeType>, trade: InterfaceTrade,
priceUpdate: number | undefined, priceUpdate: number | undefined,
response: SwapPriceUpdateUserResponse response: SwapPriceUpdateUserResponse
) => ({ ) => ({
@ -65,7 +65,7 @@ export default function SwapModalHeader({
showAcceptChanges, showAcceptChanges,
onAcceptChanges, onAcceptChanges,
}: { }: {
trade: InterfaceTrade<Currency, Currency, TradeType> trade: InterfaceTrade
shouldLogModalCloseEvent: boolean shouldLogModalCloseEvent: boolean
setShouldLogModalCloseEvent: (shouldLog: boolean) => void setShouldLogModalCloseEvent: (shouldLog: boolean) => void
allowedSlippage: Percent allowedSlippage: Percent

@ -1,5 +1,4 @@
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { Currency, TradeType } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import Column from 'components/Column' import Column from 'components/Column'
import { LoadingRows } from 'components/Loader/styled' import { LoadingRows } from 'components/Loader/styled'
@ -12,13 +11,7 @@ import getRoutingDiagramEntries from 'utils/getRoutingDiagramEntries'
import RouterLabel from './RouterLabel' import RouterLabel from './RouterLabel'
export default function SwapRoute({ export default function SwapRoute({ trade, syncing }: { trade: InterfaceTrade; syncing: boolean }) {
trade,
syncing,
}: {
trade: InterfaceTrade<Currency, Currency, TradeType>
syncing: boolean
}) {
const { chainId } = useWeb3React() const { chainId } = useWeb3React()
const autoRouterSupported = useAutoRouterSupported() const autoRouterSupported = useAutoRouterSupported()
@ -28,9 +21,9 @@ export default function SwapRoute({
// TODO(WEB-3303) // TODO(WEB-3303)
// Can `trade.gasUseEstimateUSD` be defined when `chainId` is not in `SUPPORTED_GAS_ESTIMATE_CHAIN_IDS`? // 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 && chainId && SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId)
? trade.gasUseEstimateUSD.toFixed(2) === '0.00' ? trade.gasUseEstimateUSD === '0.00'
? '<$0.01' ? '<$0.01'
: '$' + trade.gasUseEstimateUSD.toFixed(2) : '$' + trade.gasUseEstimateUSD
: undefined : undefined
return ( return (

@ -32,17 +32,17 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
justify-content: space-between; justify-content: space-between;
} }
.c5 { .c8 {
width: -webkit-fit-content; width: -webkit-fit-content;
width: -moz-fit-content; width: -moz-fit-content;
width: fit-content; width: fit-content;
} }
.c7 { .c6 {
color: #7780A0; color: #7780A0;
} }
.c8 { .c7 {
color: #0D111C; color: #0D111C;
} }
@ -67,7 +67,7 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
gap: 12px; gap: 12px;
} }
.c6 { .c5 {
display: inline-block; display: inline-block;
height: inherit; height: inherit;
} }
@ -82,14 +82,34 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
class="c2 c3 c4" class="c2 c3 c4"
> >
<div <div
class="c2 c3 c5" class="c5"
>
<div
class="c6"
> >
<div> <div>
<div
class="c6 css-zhpkf8"
>
Network fee
</div>
</div>
</div>
<div <div
class="c7 css-zhpkf8" class="c7 css-zhpkf8"
>
~$1.00
</div>
</div>
<div
class="c2 c3 c4"
>
<div
class="c2 c3 c8"
>
<div
class="c5"
>
<div>
<div
class="c6 css-zhpkf8"
> >
Minimum output Minimum output
</div> </div>
@ -97,7 +117,7 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
</div> </div>
</div> </div>
<div <div
class="c8 css-zhpkf8" class="c7 css-zhpkf8"
> >
0.00000000000000098 DEF 0.00000000000000098 DEF
</div> </div>
@ -106,14 +126,14 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
class="c2 c3 c4" class="c2 c3 c4"
> >
<div <div
class="c2 c3 c5" class="c2 c3 c8"
> >
<div <div
class="c6" class="c5"
> >
<div> <div>
<div <div
class="c7 css-zhpkf8" class="c6 css-zhpkf8"
> >
Expected output Expected output
</div> </div>
@ -121,7 +141,7 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
</div> </div>
</div> </div>
<div <div
class="c8 css-zhpkf8" class="c7 css-zhpkf8"
> >
0.000000000000001 DEF 0.000000000000001 DEF
</div> </div>
@ -133,16 +153,16 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
class="c2 c3 c4" class="c2 c3 c4"
> >
<div <div
class="c7 css-zhpkf8" class="c6 css-zhpkf8"
> >
Order routing Order routing
</div> </div>
<div <div
class="c6" class="c5"
> >
<div> <div>
<div <div
class="c8 css-zhpkf8" class="c7 css-zhpkf8"
> >
Uniswap API Uniswap API
</div> </div>

@ -42,11 +42,11 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
color: #0D111C; color: #0D111C;
} }
.c15 { .c12 {
color: #7780A0; color: #7780A0;
} }
.c13 { .c16 {
width: 100%; width: 100%;
height: 1px; height: 1px;
background-color: #D2D9EE; background-color: #D2D9EE;
@ -66,7 +66,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
justify-content: flex-start; justify-content: flex-start;
} }
.c12 { .c15 {
display: -webkit-box; display: -webkit-box;
display: -webkit-flex; display: -webkit-flex;
display: -ms-flexbox; display: -ms-flexbox;
@ -89,11 +89,20 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
transition: opacity 0.2s ease-in-out; transition: opacity 0.2s ease-in-out;
} }
.c14 { .c10 {
display: inline-block; display: inline-block;
height: inherit; height: inherit;
} }
.c11 {
margin-right: 4px;
height: 18px;
}
.c11 > * {
stroke: #98A1C0;
}
.c8 { .c8 {
background-color: transparent; background-color: transparent;
border: none; border: none;
@ -135,7 +144,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
cursor: pointer; cursor: pointer;
} }
.c10 { .c13 {
-webkit-transform: none; -webkit-transform: none;
-ms-transform: none; -ms-transform: none;
transform: none; transform: none;
@ -144,7 +153,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
transition: transform 0.1s linear; transition: transform 0.1s linear;
} }
.c11 { .c14 {
padding-top: 12px; padding-top: 12px;
} }
@ -184,8 +193,32 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
<div <div
class="c2 c3 c6" class="c2 c3 c6"
> >
<svg <div
class="c10" class="c10"
>
<div>
<div
class="c7"
>
<div
class="c2 c3 c6"
>
<svg
class="c11"
>
gas-icon.svg
</svg>
<div
class="c12 css-zhpkf8"
>
$1.00
</div>
</div>
</div>
</div>
</div>
<svg
class="c13"
fill="none" fill="none"
height="24" height="24"
stroke="#98A1C0" stroke="#98A1C0"
@ -207,15 +240,35 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
> >
<div> <div>
<div <div
class="c11" class="c14"
data-testid="advanced-swap-details" data-testid="advanced-swap-details"
> >
<div <div
class="c12" class="c15"
> >
<div <div
class="c13" class="c16"
/> />
<div
class="c2 c3 c4"
>
<div
class="c10"
>
<div>
<div
class="c12 css-zhpkf8"
>
Network fee
</div>
</div>
</div>
<div
class="c9 css-zhpkf8"
>
~$1.00
</div>
</div>
<div <div
class="c2 c3 c4" class="c2 c3 c4"
> >
@ -223,11 +276,11 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
class="c2 c3 c6" class="c2 c3 c6"
> >
<div <div
class="c14" class="c10"
> >
<div> <div>
<div <div
class="c15 css-zhpkf8" class="c12 css-zhpkf8"
> >
Minimum output Minimum output
</div> </div>
@ -247,11 +300,11 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
class="c2 c3 c6" class="c2 c3 c6"
> >
<div <div
class="c14" class="c10"
> >
<div> <div>
<div <div
class="c15 css-zhpkf8" class="c12 css-zhpkf8"
> >
Expected output Expected output
</div> </div>
@ -265,18 +318,18 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
</div> </div>
</div> </div>
<div <div
class="c13" class="c16"
/> />
<div <div
class="c2 c3 c4" class="c2 c3 c4"
> >
<div <div
class="c15 css-zhpkf8" class="c12 css-zhpkf8"
> >
Order routing Order routing
</div> </div>
<div <div
class="c14" class="c10"
> >
<div> <div>
<div <div

@ -81,7 +81,7 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
margin: -0px; margin: -0px;
} }
.c22 { .c24 {
width: -webkit-fit-content; width: -webkit-fit-content;
width: -moz-fit-content; width: -moz-fit-content;
width: fit-content; width: fit-content;
@ -91,7 +91,7 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
color: #0D111C; color: #0D111C;
} }
.c24 { .c23 {
color: #7780A0; color: #7780A0;
} }
@ -167,7 +167,7 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
background-size: 400%; background-size: 400%;
} }
.c23 { .c22 {
display: inline-block; display: inline-block;
height: inherit; height: inherit;
} }
@ -430,14 +430,34 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
class="c5 c6 c7" class="c5 c6 c7"
> >
<div <div
class="c5 c6 c22" class="c22"
>
<div
class="c23"
> >
<div> <div>
<div <div
class="c24 css-zhpkf8" class="c23 css-zhpkf8"
>
Network fee
</div>
</div>
</div>
<div
class="c18 css-zhpkf8"
>
~$1.00
</div>
</div>
<div
class="c5 c6 c7"
>
<div
class="c5 c6 c24"
>
<div
class="c22"
>
<div>
<div
class="c23 css-zhpkf8"
> >
Minimum output Minimum output
</div> </div>
@ -454,14 +474,14 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
class="c5 c6 c7" class="c5 c6 c7"
> >
<div <div
class="c5 c6 c22" class="c5 c6 c24"
> >
<div <div
class="c23" class="c22"
> >
<div> <div>
<div <div
class="c24 css-zhpkf8" class="c23 css-zhpkf8"
> >
Expected output Expected output
</div> </div>
@ -481,12 +501,12 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
class="c5 c6 c7" class="c5 c6 c7"
> >
<div <div
class="c24 css-zhpkf8" class="c23 css-zhpkf8"
> >
Order routing Order routing
</div> </div>
<div <div
class="c23" class="c22"
> >
<div> <div>
<div <div
@ -504,7 +524,7 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
style="padding: .75rem 1rem;" style="padding: .75rem 1rem;"
> >
<div <div
class="c24 css-k51stg" class="c23 css-k51stg"
style="width: 100%;" style="width: 100%;"
> >
Output is estimated. You will receive at least 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;" style="padding: 12px 0px 0px 0px;"
> >
<div <div
class="c24 css-8mokm4" class="c23 css-8mokm4"
> >
Output will be sent to Output will be sent to
<b <b

@ -113,3 +113,7 @@ export const L2_CHAIN_IDS = [
] as const ] as const
export type SupportedL2ChainId = typeof L2_CHAIN_IDS[number] export type SupportedL2ChainId = typeof L2_CHAIN_IDS[number]
export function isPolygonChain(chainId: number): chainId is SupportedChainId.POLYGON | SupportedChainId.POLYGON_MUMBAI {
return chainId === SupportedChainId.POLYGON || chainId === SupportedChainId.POLYGON_MUMBAI
}

@ -11,7 +11,7 @@ import { useMemo } from 'react'
import { InterfaceTrade } from 'state/routing/types' import { InterfaceTrade } from 'state/routing/types'
import useGasPrice from './useGasPrice' import useGasPrice from './useGasPrice'
import useStablecoinPrice, { useStablecoinValue } from './useStablecoinPrice' import useStablecoinPrice, { useStablecoinAmountFromFiatValue, useStablecoinValue } from './useStablecoinPrice'
const DEFAULT_AUTO_SLIPPAGE = new Percent(1, 1000) // .10% const DEFAULT_AUTO_SLIPPAGE = new Percent(1, 1000) // .10%
@ -72,15 +72,14 @@ const MAX_AUTO_SLIPPAGE_TOLERANCE = new Percent(5, 100) // 5%
/** /**
* Returns slippage tolerance based on values from current trade, gas estimates from api, and active network. * Returns slippage tolerance based on values from current trade, gas estimates from api, and active network.
*/ */
export default function useAutoSlippageTolerance( export default function useAutoSlippageTolerance(trade?: InterfaceTrade): Percent {
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
): Percent {
const { chainId } = useWeb3React() const { chainId } = useWeb3React()
const onL2 = chainId && L2_CHAIN_IDS.includes(chainId) const onL2 = chainId && L2_CHAIN_IDS.includes(chainId)
const outputDollarValue = useStablecoinValue(trade?.outputAmount) const outputDollarValue = useStablecoinValue(trade?.outputAmount)
const nativeGasPrice = useGasPrice() const nativeGasPrice = useGasPrice()
const gasEstimate = guesstimateGas(trade) const gasEstimate = guesstimateGas(trade)
const gasEstimateUSD = useStablecoinAmountFromFiatValue(trade?.gasUseEstimateUSD) ?? null
const nativeCurrency = useNativeCurrency(chainId) const nativeCurrency = useNativeCurrency(chainId)
const nativeCurrencyPrice = useStablecoinPrice((trade && nativeCurrency) ?? undefined) 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 // NOTE - dont use gas estimate for L2s yet - need to verify accuracy
// if not, use local heuristic // if not, use local heuristic
const dollarCostToUse = const dollarCostToUse =
chainId && SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) && trade?.gasUseEstimateUSD chainId && SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) && gasEstimateUSD ? gasEstimateUSD : dollarGasCost
? trade.gasUseEstimateUSD
: dollarGasCost
if (outputDollarValue && dollarCostToUse) { if (outputDollarValue && dollarCostToUse) {
// optimize for highest possible slippage without getting MEV'd // optimize for highest possible slippage without getting MEV'd
@ -121,5 +118,15 @@ export default function useAutoSlippageTolerance(
} }
return DEFAULT_AUTO_SLIPPAGE return DEFAULT_AUTO_SLIPPAGE
}, [trade, onL2, nativeGasPrice, gasEstimate, nativeCurrency, nativeCurrencyPrice, chainId, outputDollarValue]) }, [
trade,
onL2,
nativeGasPrice,
gasEstimate,
nativeCurrency,
nativeCurrencyPrice,
chainId,
gasEstimateUSD,
outputDollarValue,
])
} }

@ -83,15 +83,6 @@ describe('#useBestV3Trade ExactIn', () => {
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, undefined) expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, undefined)
expect(result.current).toEqual({ state: TradeState.VALID, trade: 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', () => { describe('when routing api is in error state', () => {
@ -167,15 +158,6 @@ describe('#useBestV3Trade ExactOut', () => {
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined) expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
expect(result.current).toEqual({ state: TradeState.VALID, trade: 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', () => { describe('when routing api is in error state', () => {

@ -23,7 +23,7 @@ export function useBestTrade(
otherCurrency?: Currency otherCurrency?: Currency
): { ): {
state: TradeState state: TradeState
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined trade?: InterfaceTrade
} { } {
const { chainId } = useWeb3React() const { chainId } = useWeb3React()
const autoRouterSupported = useAutoRouterSupported() const autoRouterSupported = useAutoRouterSupported()

@ -5,7 +5,7 @@ import { SupportedChainId } from 'constants/chains'
import JSBI from 'jsbi' import JSBI from 'jsbi'
import { useSingleContractWithCallData } from 'lib/hooks/multicall' import { useSingleContractWithCallData } from 'lib/hooks/multicall'
import { useMemo } from 'react' 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 { isCelo } from '../constants/tokens'
import { useAllV3Routes } from './useAllV3Routes' import { useAllV3Routes } from './useAllV3Routes'
@ -33,7 +33,7 @@ export function useClientSideV3Trade<TTradeType extends TradeType>(
tradeType: TTradeType, tradeType: TTradeType,
amountSpecified?: CurrencyAmount<Currency>, amountSpecified?: CurrencyAmount<Currency>,
otherCurrency?: Currency otherCurrency?: Currency
): { state: TradeState; trade: InterfaceTrade<Currency, Currency, TTradeType> | undefined } { ): { state: TradeState; trade: InterfaceTrade | undefined } {
const [currencyIn, currencyOut] = const [currencyIn, currencyOut] =
tradeType === TradeType.EXACT_INPUT tradeType === TradeType.EXACT_INPUT
? [amountSpecified?.currency, otherCurrency] ? [amountSpecified?.currency, otherCurrency]
@ -135,7 +135,7 @@ export function useClientSideV3Trade<TTradeType extends TradeType>(
return { return {
state: TradeState.VALID, state: TradeState.VALID,
trade: new InterfaceTrade({ trade: new ClassicTrade({
v2Routes: [], v2Routes: [],
v3Routes: [ v3Routes: [
{ {

@ -41,8 +41,8 @@ function useETHValue(currencyAmount?: CurrencyAmount<Currency>): {
} }
} }
if (!trade || !currencyAmount?.currency || !isGqlSupportedChain(chainId)) { if (!trade || state === TradeState.LOADING || !currencyAmount?.currency || !isGqlSupportedChain(chainId)) {
return { data: undefined, isLoading: state === TradeState.LOADING || state === TradeState.SYNCING } return { data: undefined, isLoading: state === TradeState.LOADING }
} }
const { numerator, denominator } = trade.routes[0].midPrice const { numerator, denominator } = trade.routes[0].midPrice

@ -3,8 +3,10 @@ import { BigintIsh, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core'
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { AlphaRouter, AlphaRouterConfig, ChainId } from '@uniswap/smart-order-router' import { AlphaRouter, AlphaRouterConfig, ChainId } from '@uniswap/smart-order-router'
import { SupportedChainId } from 'constants/chains' import { SupportedChainId } from 'constants/chains'
import { nativeOnChain } from 'constants/tokens'
import JSBI from 'jsbi' 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' import { transformSwapRouteToGetQuoteResult } from 'utils/transformSwapRouteToGetQuoteResult'
export function toSupportedChainId(chainId: ChainId): SupportedChainId | undefined { export function toSupportedChainId(chainId: ChainId): SupportedChainId | undefined {
@ -19,50 +21,41 @@ export function isSupportedChainId(chainId: ChainId | undefined): boolean {
async function getQuote( async function getQuote(
{ {
type, tradeType,
tokenIn, tokenIn,
tokenOut, tokenOut,
amount: amountRaw, amount: amountRaw,
}: { }: {
type: 'exactIn' | 'exactOut' tradeType: TradeType
tokenIn: { address: string; chainId: number; decimals: number; symbol?: string } tokenIn: { address: string; chainId: number; decimals: number; symbol?: string }
tokenOut: { address: string; chainId: number; decimals: number; symbol?: string } tokenOut: { address: string; chainId: number; decimals: number; symbol?: string }
amount: BigintIsh amount: BigintIsh
}, },
router: AlphaRouter, router: AlphaRouter,
config: Partial<AlphaRouterConfig> routerConfig: Partial<AlphaRouterConfig>
): Promise<{ data: GetQuoteResult; error?: unknown }> { ): Promise<QuoteResult> {
const currencyIn = new Token(tokenIn.chainId, tokenIn.address, tokenIn.decimals, tokenIn.symbol) const tokenInIsNative = Object.values(SwapRouterNativeAssets).includes(tokenIn.address as SwapRouterNativeAssets)
const currencyOut = new Token(tokenOut.chainId, tokenOut.address, tokenOut.decimals, tokenOut.symbol) 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)) 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( if (!swapRoute) {
amount, return { state: QuoteState.NOT_FOUND }
quoteCurrency,
type === 'exactIn' ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT,
/*swapConfig=*/ undefined,
config
)
if (!swapRoute) throw new Error('Failed to generate client side quote')
return { data: transformSwapRouteToGetQuoteResult(type, amount, swapRoute) }
} }
interface QuoteArguments { return transformSwapRouteToGetQuoteResult(tradeType, amount, swapRoute)
tokenInAddress: string
tokenInChainId: ChainId
tokenInDecimals: number
tokenInSymbol?: string
tokenOutAddress: string
tokenOutChainId: ChainId
tokenOutDecimals: number
tokenOutSymbol?: string
amount: string
type: 'exactIn' | 'exactOut'
} }
export async function getClientSideQuote( export async function getClientSideQuote(
@ -76,14 +69,14 @@ export async function getClientSideQuote(
tokenOutDecimals, tokenOutDecimals,
tokenOutSymbol, tokenOutSymbol,
amount, amount,
type, tradeType,
}: QuoteArguments, }: GetQuoteArgs,
router: AlphaRouter, router: AlphaRouter,
config: Partial<AlphaRouterConfig> config: Partial<AlphaRouterConfig>
) { ) {
return getQuote( return getQuote(
{ {
type, tradeType,
tokenIn: { tokenIn: {
address: tokenInAddress, address: tokenInAddress,
chainId: tokenInChainId, chainId: tokenInChainId,

@ -1,6 +1,7 @@
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { useMemo } from 'react' import { useMemo } from 'react'
import { INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference } from 'state/routing/slice' 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 * Returns query arguments for the Routing API query or undefined if the
@ -26,16 +27,16 @@ export function useRoutingAPIArguments({
? undefined ? undefined
: { : {
amount: amount.quotient.toString(), amount: amount.quotient.toString(),
tokenInAddress: tokenIn.wrapped.address, tokenInAddress: currencyAddressForSwapQuote(tokenIn),
tokenInChainId: tokenIn.wrapped.chainId, tokenInChainId: tokenIn.wrapped.chainId,
tokenInDecimals: tokenIn.wrapped.decimals, tokenInDecimals: tokenIn.wrapped.decimals,
tokenInSymbol: tokenIn.wrapped.symbol, tokenInSymbol: tokenIn.wrapped.symbol,
tokenOutAddress: tokenOut.wrapped.address, tokenOutAddress: currencyAddressForSwapQuote(tokenOut),
tokenOutChainId: tokenOut.wrapped.chainId, tokenOutChainId: tokenOut.wrapped.chainId,
tokenOutDecimals: tokenOut.wrapped.decimals, tokenOutDecimals: tokenOut.wrapped.decimals,
tokenOutSymbol: tokenOut.wrapped.symbol, tokenOutSymbol: tokenOut.wrapped.symbol,
routerPreference, routerPreference,
type: (tradeType === TradeType.EXACT_INPUT ? 'exactIn' : 'exactOut') as 'exactIn' | 'exactOut', tradeType,
}, },
[amount, routerPreference, tokenIn, tokenOut, tradeType] [amount, routerPreference, tokenIn, tokenOut, tradeType]
) )

@ -39,7 +39,7 @@ export const formatSwapSignedAnalyticsEventProperties = ({
fiatValues, fiatValues,
txHash, txHash,
}: { }: {
trade: InterfaceTrade<Currency, Currency, TradeType> | Trade<Currency, Currency, TradeType> trade: InterfaceTrade | Trade<Currency, Currency, TradeType>
fiatValues: { amountIn: number | undefined; amountOut: number | undefined } fiatValues: { amountIn: number | undefined; amountOut: number | undefined }
txHash: string txHash: string
}) => ({ }) => ({
@ -61,7 +61,7 @@ export const formatSwapSignedAnalyticsEventProperties = ({
export const formatSwapQuoteReceivedEventProperties = ( export const formatSwapQuoteReceivedEventProperties = (
trade: Trade<Currency, Currency, TradeType>, trade: Trade<Currency, Currency, TradeType>,
gasUseEstimateUSD?: CurrencyAmount<Token>, gasUseEstimateUSD?: string,
fetchingSwapQuoteStartTime?: Date fetchingSwapQuoteStartTime?: Date
) => { ) => {
return { return {
@ -70,7 +70,7 @@ export const formatSwapQuoteReceivedEventProperties = (
token_in_address: getTokenAddress(trade.inputAmount.currency), token_in_address: getTokenAddress(trade.inputAmount.currency),
token_out_address: getTokenAddress(trade.outputAmount.currency), token_out_address: getTokenAddress(trade.outputAmount.currency),
price_impact_basis_points: trade ? formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)) : undefined, 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: chain_id:
trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId
? trade.inputAmount.currency.chainId ? trade.inputAmount.currency.chainId

@ -3,7 +3,7 @@ import { formatEther, parseEther } from '@ethersproject/units'
import { t, Trans } from '@lingui/macro' import { t, Trans } from '@lingui/macro'
import { sendAnalyticsEvent, TraceEvent } from '@uniswap/analytics' import { sendAnalyticsEvent, TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, NFTEventName } from '@uniswap/analytics-events' 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 { useWeb3React } from '@web3-react/core'
import { useToggleAccountDrawer } from 'components/AccountDrawer' import { useToggleAccountDrawer } from 'components/AccountDrawer'
import Column from 'components/Column' import Column from 'components/Column'
@ -208,7 +208,7 @@ const InputCurrencyValue = ({
totalEthPrice: BigNumber totalEthPrice: BigNumber
activeCurrency: Currency | undefined | null activeCurrency: Currency | undefined | null
tradeState: TradeState tradeState: TradeState
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined trade: InterfaceTrade | undefined
}) => { }) => {
if (!usingPayWithAnyToken) { if (!usingPayWithAnyToken) {
return ( return (
@ -219,7 +219,7 @@ const InputCurrencyValue = ({
) )
} }
if (tradeState === TradeState.LOADING) { if (tradeState === TradeState.LOADING && !trade) {
return ( return (
<ThemedText.BodyPrimary color="textTertiary" lineHeight="20px" fontWeight="500"> <ThemedText.BodyPrimary color="textTertiary" lineHeight="20px" fontWeight="500">
<Trans>Fetching price...</Trans> <Trans>Fetching price...</Trans>
@ -228,7 +228,7 @@ const InputCurrencyValue = ({
} }
return ( return (
<ValueText color={tradeState === TradeState.SYNCING ? 'textTertiary' : 'textPrimary'}> <ValueText color={tradeState === TradeState.LOADING ? 'textTertiary' : 'textPrimary'}>
{ethNumberStandardFormatter(trade?.inputAmount.toExact())} {ethNumberStandardFormatter(trade?.inputAmount.toExact())}
</ValueText> </ValueText>
) )

@ -9,7 +9,7 @@ export default function useDerivedPayWithAnyTokenSwapInfo(
parsedOutputAmount?: CurrencyAmount<NativeCurrency | Token> parsedOutputAmount?: CurrencyAmount<NativeCurrency | Token>
): { ): {
state: TradeState state: TradeState
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined trade: InterfaceTrade | undefined
maximumAmountIn: CurrencyAmount<Token> | undefined maximumAmountIn: CurrencyAmount<Token> | undefined
allowedSlippage: Percent allowedSlippage: Percent
} { } {

@ -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 { PermitInput, TokenTradeRoutesInput, TokenTradeType } from 'graphql/data/__generated__/types-and-hooks'
import { Allowance } from 'hooks/usePermit2Allowance' import { Allowance } from 'hooks/usePermit2Allowance'
import { buildAllTradeRouteInputs } from 'nft/utils/tokenRoutes' import { buildAllTradeRouteInputs } from 'nft/utils/tokenRoutes'
@ -8,7 +8,7 @@ import { InterfaceTrade } from 'state/routing/types'
import { useTokenInput } from './useTokenInput' import { useTokenInput } from './useTokenInput'
export default function usePayWithAnyTokenSwap( export default function usePayWithAnyTokenSwap(
trade?: InterfaceTrade<Currency, Currency, TradeType> | undefined, trade?: InterfaceTrade | undefined,
allowance?: Allowance, allowance?: Allowance,
allowedSlippage?: Percent allowedSlippage?: Percent
) { ) {

@ -1,4 +1,4 @@
import { Currency, Percent, TradeType } from '@uniswap/sdk-core' import { Percent } from '@uniswap/sdk-core'
import { useMemo } from 'react' import { useMemo } from 'react'
import { InterfaceTrade } from 'state/routing/types' import { InterfaceTrade } from 'state/routing/types'
import { useTheme } from 'styled-components/macro' import { useTheme } from 'styled-components/macro'
@ -14,7 +14,7 @@ interface PriceImpactSeverity {
color: string color: string
} }
export function usePriceImpact(trade?: InterfaceTrade<Currency, Currency, TradeType>): PriceImpact | undefined { export function usePriceImpact(trade?: InterfaceTrade): PriceImpact | undefined {
const theme = useTheme() const theme = useTheme()
return useMemo(() => { return useMemo(() => {

@ -1,5 +1,5 @@
import { IRoute, Protocol } from '@uniswap/router-sdk' 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 { Pair } from '@uniswap/v2-sdk'
import { Pool } from '@uniswap/v3-sdk' import { Pool } from '@uniswap/v3-sdk'
import { TokenAmountInput, TokenTradeRouteInput, TradePoolInput } from 'graphql/data/__generated__/types-and-hooks' 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<Currency, Currency, TradeType>): { export function buildAllTradeRouteInputs(trade: InterfaceTrade): {
mixedTokenTradeRouteInputs: TokenTradeRouteInput[] | undefined mixedTokenTradeRouteInputs: TokenTradeRouteInput[] | undefined
v2TokenTradeRouteInputs: TokenTradeRouteInput[] | undefined v2TokenTradeRouteInputs: TokenTradeRouteInput[] | undefined
v3TokenTradeRouteInputs: TokenTradeRouteInput[] | undefined v3TokenTradeRouteInputs: TokenTradeRouteInput[] | undefined

@ -8,8 +8,7 @@ import {
InterfaceSectionName, InterfaceSectionName,
SwapEventName, SwapEventName,
} from '@uniswap/analytics-events' } from '@uniswap/analytics-events'
import { Trade } from '@uniswap/router-sdk' import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core'
import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk' import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { useToggleAccountDrawer } from 'components/AccountDrawer' import { useToggleAccountDrawer } from 'components/AccountDrawer'
@ -115,11 +114,11 @@ const OutputSwapSection = styled(SwapSection)`
` `
function getIsValidSwapQuote( function getIsValidSwapQuote(
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined, trade: InterfaceTrade | undefined,
tradeState: TradeState, tradeState: TradeState,
swapInputError?: ReactNode swapInputError?: ReactNode
): boolean { ): boolean {
return !!swapInputError && !!trade && (tradeState === TradeState.VALID || tradeState === TradeState.SYNCING) return Boolean(swapInputError && trade && tradeState === TradeState.VALID)
} }
function largerPercentValue(a?: Percent, b?: Percent) { function largerPercentValue(a?: Percent, b?: Percent) {
@ -293,7 +292,7 @@ export function Swap({
const fiatValueOutput = useUSDPrice(parsedAmounts[Field.OUTPUT]) const fiatValueOutput = useUSDPrice(parsedAmounts[Field.OUTPUT])
const [routeNotFound, routeIsLoading, routeIsSyncing] = useMemo( 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] [trade, tradeState]
) )
@ -336,7 +335,7 @@ export function Swap({
// modal and loading // modal and loading
const [{ showConfirm, tradeToConfirm, swapErrorMessage, attemptingTxn, txHash }, setSwapState] = useState<{ const [{ showConfirm, tradeToConfirm, swapErrorMessage, attemptingTxn, txHash }, setSwapState] = useState<{
showConfirm: boolean showConfirm: boolean
tradeToConfirm: Trade<Currency, Currency, TradeType> | undefined tradeToConfirm: InterfaceTrade | undefined
attemptingTxn: boolean attemptingTxn: boolean
swapErrorMessage: string | undefined swapErrorMessage: string | undefined
txHash: string | undefined txHash: string | undefined

@ -14,7 +14,15 @@ const store = configureStore({
reducer, reducer,
enhancers: (defaultEnhancers) => defaultEnhancers.concat(sentryEnhancer), enhancers: (defaultEnhancers) => defaultEnhancers.concat(sentryEnhancer),
middleware: (getDefaultMiddleware) => 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(routingApi.middleware)
.concat(save({ states: PERSISTED_KEYS, debounce: 1000 })), .concat(save({ states: PERSISTED_KEYS, debounce: 1000 })),
preloadedState: load({ states: PERSISTED_KEYS, disableWarnings: isTestEnv() }), preloadedState: load({ states: PERSISTED_KEYS, disableWarnings: isTestEnv() }),

@ -1,5 +1,6 @@
import { createApi, fetchBaseQuery, FetchBaseQueryError } from '@reduxjs/toolkit/query/react' import { createApi, fetchBaseQuery, FetchBaseQueryError } from '@reduxjs/toolkit/query/react'
import { Protocol } from '@uniswap/router-sdk' import { Protocol } from '@uniswap/router-sdk'
import { TradeType } from '@uniswap/sdk-core'
import { AlphaRouter, ChainId } from '@uniswap/smart-order-router' import { AlphaRouter, ChainId } from '@uniswap/smart-order-router'
import { RPC_PROVIDERS } from 'constants/providers' import { RPC_PROVIDERS } from 'constants/providers'
import { getClientSideQuote, toSupportedChainId } from 'lib/hooks/routing/clientSideSmartOrderRouter' import { getClientSideQuote, toSupportedChainId } from 'lib/hooks/routing/clientSideSmartOrderRouter'
@ -7,7 +8,8 @@ import ms from 'ms.macro'
import qs from 'qs' import qs from 'qs'
import { trace } from 'tracing/trace' import { trace } from 'tracing/trace'
import { GetQuoteResult } from './types' import { QuoteData, TradeResult } from './types'
import { isExactInput, transformRoutesToTrade } from './utils'
export enum RouterPreference { export enum RouterPreference {
AUTO = 'auto', AUTO = 'auto',
@ -69,7 +71,7 @@ const PRICE_PARAMS = {
distributionPercent: 100, distributionPercent: 100,
} }
interface GetQuoteArgs { export interface GetQuoteArgs {
tokenInAddress: string tokenInAddress: string
tokenInChainId: ChainId tokenInChainId: ChainId
tokenInDecimals: number tokenInDecimals: number
@ -80,7 +82,12 @@ interface GetQuoteArgs {
tokenOutSymbol?: string tokenOutSymbol?: string
amount: string amount: string
routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE
type: 'exactIn' | 'exactOut' tradeType: TradeType
}
enum QuoteState {
SUCCESS = 'Success',
NOT_FOUND = 'Not found',
} }
export const routingApi = createApi({ export const routingApi = createApi({
@ -89,7 +96,7 @@ export const routingApi = createApi({
baseUrl: 'https://api.uniswap.org/v1/', baseUrl: 'https://api.uniswap.org/v1/',
}), }),
endpoints: (build) => ({ endpoints: (build) => ({
getQuote: build.query<GetQuoteResult, GetQuoteArgs>({ getQuote: build.query<TradeResult, GetQuoteArgs>({
async onQueryStarted(args: GetQuoteArgs, { queryFulfilled }) { async onQueryStarted(args: GetQuoteArgs, { queryFulfilled }) {
trace( trace(
'quote', 'quote',
@ -119,11 +126,14 @@ export const routingApi = createApi({
) )
}, },
async queryFn(args, _api, _extraOptions, fetch) { async queryFn(args, _api, _extraOptions, fetch) {
const { tokenInAddress, tokenInChainId, tokenOutAddress, tokenOutChainId, amount, routerPreference, type } = if (
args args.routerPreference === RouterPreference.API ||
args.routerPreference === RouterPreference.AUTO ||
args.routerPreference === INTERNAL_ROUTER_PREFERENCE_PRICE
) {
try { try {
if (routerPreference === RouterPreference.API || routerPreference === RouterPreference.AUTO) { const { tokenInAddress, tokenInChainId, tokenOutAddress, tokenOutChainId, amount, tradeType } = args
const type = isExactInput(tradeType) ? 'exactIn' : 'exactOut'
const query = qs.stringify({ const query = qs.stringify({
...API_QUERY_PARAMS, ...API_QUERY_PARAMS,
tokenInAddress, tokenInAddress,
@ -133,21 +143,40 @@ export const routingApi = createApi({
amount, amount,
type, type,
}) })
return (await fetch(`quote?${query}`)) as { data: GetQuoteResult } | { error: FetchBaseQueryError } const response = await fetch(`quote?${query}`)
} else { if (response.error) {
const router = getRouter(args.tokenInChainId) try {
return await getClientSideQuote( // cast as any here because we do a runtime check on it being an object before indexing into .errorCode
args, const errorData = response.error.data as any
router, // NO_ROUTE should be treated as a valid response to prevent retries.
// TODO(zzmp): Use PRICE_PARAMS for RouterPreference.PRICE. if (typeof errorData === 'object' && errorData?.errorCode === 'NO_ROUTE') {
// This change is intentionally being deferred to first see what effect router caching has. return { data: { state: QuoteState.NOT_FOUND } }
CLIENT_PARAMS }
} 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. try {
// deprecate 'legacy' v2/v3 routers first. const router = getRouter(args.tokenInChainId)
return { error: { status: 'CUSTOM_ERROR', error: error.toString() } } 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`, keepUnusedDataFor: ms`10s`,

@ -8,7 +8,6 @@ export enum TradeState {
INVALID, INVALID,
NO_ROUTE_FOUND, NO_ROUTE_FOUND,
VALID, VALID,
SYNCING,
} }
// from https://github.com/Uniswap/routing-api/blob/main/lib/handlers/schema.ts // from https://github.com/Uniswap/routing-api/blob/main/lib/handlers/schema.ts
@ -49,7 +48,7 @@ export type V2PoolInRoute = {
address?: string address?: string
} }
export interface GetQuoteResult { export interface QuoteData {
quoteId?: string quoteId?: string
blockNumber: string blockNumber: string
amount: string amount: string
@ -68,12 +67,12 @@ export interface GetQuoteResult {
routeString: string routeString: string
} }
export class InterfaceTrade< export class ClassicTrade<
TInput extends Currency, TInput extends Currency,
TOutput extends Currency, TOutput extends Currency,
TTradeType extends TradeType TTradeType extends TradeType
> extends Trade<TInput, TOutput, TTradeType> { > extends Trade<TInput, TOutput, TTradeType> {
gasUseEstimateUSD: CurrencyAmount<Token> | null | undefined gasUseEstimateUSD: string | null | undefined
blockNumber: string | null | undefined blockNumber: string | null | undefined
constructor({ constructor({
@ -81,8 +80,8 @@ export class InterfaceTrade<
blockNumber, blockNumber,
...routes ...routes
}: { }: {
gasUseEstimateUSD?: CurrencyAmount<Token> | undefined | null gasUseEstimateUSD?: string | null
blockNumber?: string | null | undefined blockNumber?: string | null
v2Routes: { v2Routes: {
routev2: V2Route<TInput, TOutput> routev2: V2Route<TInput, TOutput>
inputAmount: CurrencyAmount<TInput> inputAmount: CurrencyAmount<TInput>
@ -105,3 +104,42 @@ export class InterfaceTrade<
this.gasUseEstimateUSD = gasUseEstimateUSD this.gasUseEstimateUSD = gasUseEstimateUSD
} }
} }
export type InterfaceTrade = ClassicTrade<Currency, Currency, TradeType>
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',
}

@ -3,14 +3,16 @@ import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { IMetric, MetricLoggerUnit, setGlobalMetric } from '@uniswap/smart-order-router' import { IMetric, MetricLoggerUnit, setGlobalMetric } from '@uniswap/smart-order-router'
import { sendTiming } from 'components/analytics' import { sendTiming } from 'components/analytics'
import { AVERAGE_L1_BLOCK_TIME } from 'constants/chainInfo' import { AVERAGE_L1_BLOCK_TIME } from 'constants/chainInfo'
import { useStablecoinAmountFromFiatValue } from 'hooks/useStablecoinPrice'
import { useRoutingAPIArguments } from 'lib/hooks/routing/useRoutingAPIArguments' import { useRoutingAPIArguments } from 'lib/hooks/routing/useRoutingAPIArguments'
import ms from 'ms.macro' import ms from 'ms.macro'
import { useMemo } from 'react' import { useMemo } from 'react'
import { INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference, useGetQuoteQuery } from 'state/routing/slice' import { INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference, useGetQuoteQuery } from 'state/routing/slice'
import { InterfaceTrade, TradeState } from './types' import { InterfaceTrade, QuoteState, TradeState } from './types'
import { computeRoutes, transformRoutesToTrade } from './utils'
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 * Returns the best trade by invoking the routing api or the smart order router on the client
@ -25,7 +27,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE
): { ): {
state: TradeState state: TradeState
trade: InterfaceTrade<Currency, Currency, TTradeType> | undefined trade?: InterfaceTrade
} { } {
const [currencyIn, currencyOut]: [Currency | undefined, Currency | undefined] = useMemo( const [currencyIn, currencyOut]: [Currency | undefined, Currency | undefined] = useMemo(
() => () =>
@ -44,10 +46,9 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
}) })
const { const {
isLoading,
isError, isError,
data: quoteResult, data: tradeResult,
currentData, currentData: currentTradeResult,
} = useGetQuoteQuery(queryArgs ?? skipToken, { } = useGetQuoteQuery(queryArgs ?? skipToken, {
// Price-fetching is informational and costly, so it's done less frequently. // Price-fetching is informational and costly, so it's done less frequently.
pollingInterval: routerPreference === INTERNAL_ROUTER_PREFERENCE_PRICE ? ms`1m` : AVERAGE_L1_BLOCK_TIME, pollingInterval: routerPreference === INTERNAL_ROUTER_PREFERENCE_PRICE ? ms`1m` : AVERAGE_L1_BLOCK_TIME,
@ -55,72 +56,23 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
refetchOnMountOrArgChange: 2 * 60, refetchOnMountOrArgChange: 2 * 60,
}) })
const route = useMemo( const isCurrent = currentTradeResult === tradeResult
() => 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
return useMemo(() => { 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 { return {
state: TradeState.INVALID, state: isCurrent ? TradeState.VALID : TradeState.LOADING,
trade: undefined, trade: tradeResult.trade,
} }
} }
}, [amountSpecified, isCurrent, isError, queryArgs, tradeResult])
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,
])
} }
// only want to enable this when app hook called // only want to enable this when app hook called

@ -1,35 +1,27 @@
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' import { computeRoutes } from './utils'
const USDC = new Token(1, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC') const USDC = new Token(1, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC')
const DAI = new Token(1, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 6, 'DAI') const DAI = new Token(1, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 6, 'DAI')
const MKR = new Token(1, '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2', 6, 'MKR') const MKR = new Token(1, '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2', 6, 'MKR')
const ETH = nativeOnChain(1) const ETH = nativeOnChain(SupportedChainId.MAINNET)
// helper function to make amounts more readable // helper function to make amounts more readable
const amount = (raw: TemplateStringsArray) => (parseInt(raw[0]) * 1e6).toString() const amount = (raw: TemplateStringsArray) => (parseInt(raw[0]) * 1e6).toString()
describe('#useRoute', () => { 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', () => { it('handles empty edges and nodes', () => {
const result = computeRoutes(USDC, DAI, TradeType.EXACT_INPUT, { const result = computeRoutes(false, false, [])
route: [],
})
expect(result).toEqual([]) expect(result).toEqual([])
}) })
it('handles a single route trade from DAI to USDC from v3', () => { it('handles a single route trade from DAI to USDC from v3', () => {
const result = computeRoutes(DAI, USDC, TradeType.EXACT_INPUT, { const result = computeRoutes(false, false, [
route: [
[ [
{ {
type: 'v3-pool', type: 'v3-pool',
@ -44,8 +36,7 @@ describe('#useRoute', () => {
tokenOut: USDC, tokenOut: USDC,
}, },
], ],
], ])
})
const r = result?.[0] const r = result?.[0]
@ -60,8 +51,7 @@ describe('#useRoute', () => {
}) })
it('handles a single route trade from DAI to USDC from v2', () => { it('handles a single route trade from DAI to USDC from v2', () => {
const result = computeRoutes(DAI, USDC, TradeType.EXACT_INPUT, { const result = computeRoutes(false, false, [
route: [
[ [
{ {
type: 'v2-pool', type: 'v2-pool',
@ -80,8 +70,7 @@ describe('#useRoute', () => {
}, },
}, },
], ],
], ])
})
const r = result?.[0] const r = result?.[0]
@ -96,8 +85,7 @@ describe('#useRoute', () => {
}) })
it('handles a multi-route trade from DAI to USDC', () => { it('handles a multi-route trade from DAI to USDC', () => {
const result = computeRoutes(DAI, USDC, TradeType.EXACT_OUTPUT, { const result = computeRoutes(false, false, [
route: [
[ [
{ {
type: 'v2-pool', type: 'v2-pool',
@ -142,8 +130,7 @@ describe('#useRoute', () => {
tickCurrent: '-69633', tickCurrent: '-69633',
}, },
], ],
], ])
})
expect(result).toBeDefined() expect(result).toBeDefined()
expect(result?.length).toBe(2) expect(result?.length).toBe(2)
@ -165,8 +152,7 @@ describe('#useRoute', () => {
}) })
it('handles a single route trade with same token pair, different fee tiers', () => { it('handles a single route trade with same token pair, different fee tiers', () => {
const result = computeRoutes(DAI, USDC, TradeType.EXACT_INPUT, { const result = computeRoutes(false, false, [
route: [
[ [
{ {
type: 'v3-pool', type: 'v3-pool',
@ -195,8 +181,7 @@ describe('#useRoute', () => {
tickCurrent: '-69633', tickCurrent: '-69633',
}, },
], ],
], ])
})
expect(result).toBeDefined() expect(result).toBeDefined()
expect(result?.length).toBe(2) expect(result?.length).toBe(2)
@ -206,12 +191,53 @@ describe('#useRoute', () => {
expect(result?.[0].inputAmount.toSignificant()).toBe('1') 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', () => { describe('with ETH', () => {
it('outputs native ETH as input currency', () => { it('outputs native ETH as input currency', () => {
const WETH = ETH.wrapped const WETH = ETH.wrapped
const result = computeRoutes(ETH, USDC, TradeType.EXACT_OUTPUT, { const result = computeRoutes(true, false, [
route: [
[ [
{ {
type: 'v3-pool', type: 'v3-pool',
@ -226,8 +252,7 @@ describe('#useRoute', () => {
tokenOut: USDC, tokenOut: USDC,
}, },
], ],
], ])
})
expect(result).toBeDefined() expect(result).toBeDefined()
expect(result?.length).toBe(1) expect(result?.length).toBe(1)
@ -239,8 +264,7 @@ describe('#useRoute', () => {
it('outputs native ETH as output currency', () => { it('outputs native ETH as output currency', () => {
const WETH = new Token(1, ETH.wrapped.address, 18, 'WETH') const WETH = new Token(1, ETH.wrapped.address, 18, 'WETH')
const result = computeRoutes(USDC, ETH, TradeType.EXACT_OUTPUT, { const result = computeRoutes(false, true, [
route: [
[ [
{ {
type: 'v3-pool', type: 'v3-pool',
@ -255,8 +279,7 @@ describe('#useRoute', () => {
tokenOut: WETH, tokenOut: WETH,
}, },
], ],
], ])
})
expect(result?.length).toBe(1) expect(result?.length).toBe(1)
expect(result?.[0].routev3?.input).toStrictEqual(USDC) expect(result?.[0].routev3?.input).toStrictEqual(USDC)
@ -268,8 +291,7 @@ describe('#useRoute', () => {
it('outputs native ETH as input currency for v2 routes', () => { it('outputs native ETH as input currency for v2 routes', () => {
const WETH = ETH.wrapped const WETH = ETH.wrapped
const result = computeRoutes(ETH, USDC, TradeType.EXACT_OUTPUT, { const result = computeRoutes(true, false, [
route: [
[ [
{ {
type: 'v2-pool', type: 'v2-pool',
@ -288,8 +310,7 @@ describe('#useRoute', () => {
}, },
}, },
], ],
], ])
})
expect(result).toBeDefined() expect(result).toBeDefined()
expect(result?.length).toBe(1) expect(result?.length).toBe(1)
@ -301,8 +322,7 @@ describe('#useRoute', () => {
it('outputs native ETH as output currency for v2 routes', () => { it('outputs native ETH as output currency for v2 routes', () => {
const WETH = new Token(1, ETH.wrapped.address, 18, 'WETH') const WETH = new Token(1, ETH.wrapped.address, 18, 'WETH')
const result = computeRoutes(USDC, ETH, TradeType.EXACT_OUTPUT, { const result = computeRoutes(false, true, [
route: [
[ [
{ {
type: 'v2-pool', type: 'v2-pool',
@ -321,8 +341,7 @@ describe('#useRoute', () => {
}, },
}, },
], ],
], ])
})
expect(result?.length).toBe(1) expect(result?.length).toBe(1)
expect(result?.[0].routev2?.input).toStrictEqual(USDC) expect(result?.[0].routev2?.input).toStrictEqual(USDC)

@ -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 { Currency, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core'
import { Pair, Route as V2Route } from '@uniswap/v2-sdk' import { Pair, Route as V2Route } from '@uniswap/v2-sdk'
import { FeeAmount, Pool, Route as V3Route } from '@uniswap/v3-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 * Transforms a Routing API quote into an array of routes that can be used to
* a `Trade`. * create a `Trade`.
*/ */
export function computeRoutes( export function computeRoutes(
currencyIn: Currency | undefined, tokenInIsNative: boolean,
currencyOut: Currency | undefined, tokenOutIsNative: boolean,
tradeType: TradeType, routes: QuoteData['route']
quoteResult: Pick<GetQuoteResult, 'route'> | undefined ):
) { | {
if (!quoteResult || !quoteResult.route || !currencyIn || !currencyOut) return undefined routev3: V3Route<Currency, Currency> | null
routev2: V2Route<Currency, Currency> | null
mixedRoute: MixedRouteSDK<Currency, Currency> | null
inputAmount: CurrencyAmount<Currency>
outputAmount: CurrencyAmount<Currency>
}[]
| 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 parsedCurrencyIn = tokenInIsNative ? nativeOnChain(tokenIn.chainId) : parseToken(tokenIn)
const parsedTokenOut = parseToken(quoteResult.route[0][quoteResult.route[0].length - 1].tokenOut) const parsedCurrencyOut = tokenOutIsNative ? nativeOnChain(tokenOut.chainId) : parseToken(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
try { try {
return quoteResult.route.map((route) => { return routes.map((route) => {
if (route.length === 0) { if (route.length === 0) {
throw new Error('Expected route to have at least one pair or pool') 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') throw new Error('Expected both amountIn and amountOut to be present')
} }
const routeProtocol = getRouteProtocol(route) const isOnlyV2 = isVersionedRoute<V2PoolInRoute>(PoolType.V2Pool, route)
const isOnlyV3 = isVersionedRoute<V3PoolInRoute>(PoolType.V3Pool, route)
return { return {
routev3: routev3: isOnlyV3 ? new V3Route(route.map(parsePool), parsedCurrencyIn, parsedCurrencyOut) : null,
routeProtocol === Protocol.V3 routev2: isOnlyV2 ? new V2Route(route.map(parsePair), parsedCurrencyIn, parsedCurrencyOut) : null,
? new V3Route(route.map(genericPoolPairParser) as Pool[], currencyIn, currencyOut)
: null,
routev2:
routeProtocol === Protocol.V2
? new V2Route(route.map(genericPoolPairParser) as Pair[], currencyIn, currencyOut)
: null,
mixedRoute: mixedRoute:
routeProtocol === Protocol.MIXED !isOnlyV3 && !isOnlyV2
? new MixedRouteSDK(route.map(genericPoolPairParser), currencyIn, currencyOut) ? new MixedRouteSDK(route.map(parsePoolOrPair), parsedCurrencyIn, parsedCurrencyOut)
: null, : null,
inputAmount: CurrencyAmount.fromRawAmount(currencyIn, rawAmountIn), inputAmount: CurrencyAmount.fromRawAmount(parsedCurrencyIn, rawAmountIn),
outputAmount: CurrencyAmount.fromRawAmount(currencyOut, rawAmountOut), outputAmount: CurrencyAmount.fromRawAmount(parsedCurrencyOut, rawAmountOut),
} }
}) })
} catch (e) { } catch (e) {
// `Route` constructor may throw if inputs/outputs are temporarily out of sync console.error('Error computing routes', e)
// (RTK-Query always returns the latest data which may not be the right inputs/outputs) return
// This is not fatal and will fix itself in future render cycles
console.error(e)
return undefined
} }
} }
export function transformRoutesToTrade<TTradeType extends TradeType>( const parsePoolOrPair = (pool: V3PoolInRoute | V2PoolInRoute): Pool | Pair => {
route: ReturnType<typeof computeRoutes>, return pool.type === PoolType.V3Pool ? parsePool(pool) : parsePair(pool)
tradeType: TTradeType, }
blockNumber?: string | null,
gasUseEstimateUSD?: CurrencyAmount<Token> | null function isVersionedRoute<T extends V2PoolInRoute | V3PoolInRoute>(
): InterfaceTrade<Currency, Currency, TTradeType> { type: T['type'],
return new InterfaceTrade({ 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: v2Routes:
route routes
?.filter((r): r is typeof route[0] & { routev2: NonNullable<typeof route[0]['routev2']> } => r.routev2 !== null)
.map(({ routev2, inputAmount, outputAmount }) => ({ routev2, inputAmount, outputAmount })) ?? [],
v3Routes:
route
?.filter((r): r is typeof route[0] & { routev3: NonNullable<typeof route[0]['routev3']> } => r.routev3 !== null)
.map(({ routev3, inputAmount, outputAmount }) => ({ routev3, inputAmount, outputAmount })) ?? [],
mixedRoutes:
route
?.filter( ?.filter(
(r): r is typeof route[0] & { mixedRoute: NonNullable<typeof route[0]['mixedRoute']> } => (r): r is typeof routes[0] & { routev2: NonNullable<typeof routes[0]['routev2']> } => r.routev2 !== null
)
.map(({ routev2, inputAmount, outputAmount }) => ({
routev2,
inputAmount,
outputAmount,
})) ?? [],
v3Routes:
routes
?.filter(
(r): r is typeof routes[0] & { routev3: NonNullable<typeof routes[0]['routev3']> } => r.routev3 !== null
)
.map(({ routev3, inputAmount, outputAmount }) => ({
routev3,
inputAmount,
outputAmount,
})) ?? [],
mixedRoutes:
routes
?.filter(
(r): r is typeof routes[0] & { mixedRoute: NonNullable<typeof routes[0]['mixedRoute']> } =>
r.mixedRoute !== null r.mixedRoute !== null
) )
.map(({ mixedRoute, inputAmount, outputAmount }) => ({ mixedRoute, inputAmount, outputAmount })) ?? [], .map(({ mixedRoute, inputAmount, outputAmount }) => ({
mixedRoute,
inputAmount,
outputAmount,
})) ?? [],
tradeType, tradeType,
gasUseEstimateUSD, gasUseEstimateUSD: parseFloat(gasUseEstimateUSD).toFixed(2).toString(),
blockNumber, blockNumber,
}) })
return { state: QuoteState.SUCCESS, trade }
} }
const parseToken = ({ address, chainId, decimals, symbol }: GetQuoteResult['route'][0][0]['tokenIn']): Token => { function parseToken({ address, chainId, decimals, symbol }: QuoteData['route'][0][0]['tokenIn']): Token {
return new Token(chainId, address, parseInt(decimals.toString()), symbol) return new Token(chainId, address, parseInt(decimals.toString()), symbol)
} }
const parsePool = ({ fee, sqrtRatioX96, liquidity, tickCurrent, tokenIn, tokenOut }: V3PoolInRoute): Pool => function parsePool({ fee, sqrtRatioX96, liquidity, tickCurrent, tokenIn, tokenOut }: V3PoolInRoute): Pool {
new Pool( return new Pool(
parseToken(tokenIn), parseToken(tokenIn),
parseToken(tokenOut), parseToken(tokenOut),
parseInt(fee) as FeeAmount, parseInt(fee) as FeeAmount,
@ -106,6 +146,7 @@ const parsePool = ({ fee, sqrtRatioX96, liquidity, tickCurrent, tokenIn, tokenOu
liquidity, liquidity,
parseInt(tickCurrent) parseInt(tickCurrent)
) )
}
const parsePair = ({ reserve0, reserve1 }: V2PoolInRoute): Pair => const parsePair = ({ reserve0, reserve1 }: V2PoolInRoute): Pair =>
new Pair( new Pair(
@ -113,12 +154,15 @@ const parsePair = ({ reserve0, reserve1 }: V2PoolInRoute): Pair =>
CurrencyAmount.fromRawAmount(parseToken(reserve1.token), reserve1.quotient) CurrencyAmount.fromRawAmount(parseToken(reserve1.token), reserve1.quotient)
) )
const genericPoolPairParser = (pool: V3PoolInRoute | V2PoolInRoute): Pool | Pair => { // TODO(WEB-2050): Convert other instances of tradeType comparison to use this utility function
return pool.type === 'v3-pool' ? parsePool(pool) : parsePair(pool) export function isExactInput(tradeType: TradeType): boolean {
return tradeType === TradeType.EXACT_INPUT
} }
function getRouteProtocol(route: (V3PoolInRoute | V2PoolInRoute)[]): Protocol { export function currencyAddressForSwapQuote(currency: Currency): string {
if (route.every((pool) => pool.type === 'v2-pool')) return Protocol.V2 if (currency.isNative) {
if (route.every((pool) => pool.type === 'v3-pool')) return Protocol.V3 return isPolygonChain(currency.chainId) ? SwapRouterNativeAssets.MATIC : SwapRouterNativeAssets.ETH
return Protocol.MIXED }
return currency.address
} }

@ -81,7 +81,7 @@ export function useDerivedSwapInfo(
parsedAmount: CurrencyAmount<Currency> | undefined parsedAmount: CurrencyAmount<Currency> | undefined
inputError?: ReactNode inputError?: ReactNode
trade: { trade: {
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined trade?: InterfaceTrade
state: TradeState state: TradeState
} }
allowedSlippage: Percent allowedSlippage: Percent

@ -2,7 +2,7 @@ import { CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
import { V3Route } from '@uniswap/smart-order-router' import { V3Route } from '@uniswap/smart-order-router'
import { FeeAmount, Pool } from '@uniswap/v3-sdk' import { FeeAmount, Pool } from '@uniswap/v3-sdk'
import JSBI from 'jsbi' 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_1 = new Token(1, '0x0000000000000000000000000000000000000001', 18, 'ABC', 'Abc')
export const TEST_TOKEN_2 = new Token(1, '0x0000000000000000000000000000000000000002', 18, 'DEF', 'Def') 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) => export const toCurrencyAmount = (token: Token, amount: number) =>
CurrencyAmount.fromRawAmount(token, JSBI.BigInt(amount)) CurrencyAmount.fromRawAmount(token, JSBI.BigInt(amount))
export const TEST_TRADE_EXACT_INPUT = new InterfaceTrade({ export const TEST_TRADE_EXACT_INPUT = new ClassicTrade({
v3Routes: [ v3Routes: [
{ {
routev3: new V3Route([TEST_POOL_12], TEST_TOKEN_1, TEST_TOKEN_2), 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: [], v2Routes: [],
tradeType: TradeType.EXACT_INPUT, tradeType: TradeType.EXACT_INPUT,
gasUseEstimateUSD: '1.00',
}) })
export const TEST_TRADE_EXACT_OUTPUT = new InterfaceTrade({ export const TEST_TRADE_EXACT_OUTPUT = new ClassicTrade({
v3Routes: [ v3Routes: [
{ {
routev3: new V3Route([TEST_POOL_13], TEST_TOKEN_1, TEST_TOKEN_3), routev3: new V3Route([TEST_POOL_13], TEST_TOKEN_1, TEST_TOKEN_3),

@ -15,9 +15,7 @@ const V2_DEFAULT_FEE_TIER = 3000
/** /**
* Loops through all routes on a trade and returns an array of diagram entries. * Loops through all routes on a trade and returns an array of diagram entries.
*/ */
export default function getRoutingDiagramEntries( export default function getRoutingDiagramEntries(trade: InterfaceTrade): RoutingDiagramEntry[] {
trade: InterfaceTrade<Currency, Currency, TradeType>
): RoutingDiagramEntry[] {
return trade.swaps.map(({ route: { path: tokenPath, pools, protocol }, inputAmount, outputAmount }) => { return trade.swaps.map(({ route: { path: tokenPath, pools, protocol }, inputAmount, outputAmount }) => {
const portion = const portion =
trade.tradeType === TradeType.EXACT_INPUT trade.tradeType === TradeType.EXACT_INPUT

@ -1,12 +1,13 @@
import { Protocol } from '@uniswap/router-sdk' 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 { routeAmountsToString, SwapRoute } from '@uniswap/smart-order-router'
import { Pool } from '@uniswap/v3-sdk' 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) // from routing-api (https://github.com/Uniswap/routing-api/blob/main/lib/handlers/quote/quote.ts#L243-L311)
export function transformSwapRouteToGetQuoteResult( export function transformSwapRouteToGetQuoteResult(
type: 'exactIn' | 'exactOut', tradeType: TradeType,
amount: CurrencyAmount<Currency>, amount: CurrencyAmount<Currency>,
{ {
quote, quote,
@ -19,7 +20,7 @@ export function transformSwapRouteToGetQuoteResult(
methodParameters, methodParameters,
blockNumber, blockNumber,
}: SwapRoute }: SwapRoute
): GetQuoteResult { ): QuoteResult {
const routeResponse: Array<(V3PoolInRoute | V2PoolInRoute)[]> = [] const routeResponse: Array<(V3PoolInRoute | V2PoolInRoute)[]> = []
for (const subRoute of route) { for (const subRoute of route) {
@ -34,12 +35,12 @@ export function transformSwapRouteToGetQuoteResult(
let edgeAmountIn = undefined let edgeAmountIn = undefined
if (i === 0) { 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 let edgeAmountOut = undefined
if (i === pools.length - 1) { 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) { if (nextPool instanceof Pool) {
@ -109,7 +110,7 @@ export function transformSwapRouteToGetQuoteResult(
routeResponse.push(curRoute) routeResponse.push(curRoute)
} }
const result: GetQuoteResult = { const result: QuoteData = {
methodParameters, methodParameters,
blockNumber: blockNumber.toString(), blockNumber: blockNumber.toString(),
amount: amount.quotient.toString(), amount: amount.quotient.toString(),
@ -127,5 +128,5 @@ export function transformSwapRouteToGetQuoteResult(
routeString: routeAmountsToString(route), routeString: routeAmountsToString(route),
} }
return result return { state: QuoteState.SUCCESS, data: result }
} }