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 {
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(<AdvancedSwapDetails trade={TEST_TRADE_EXACT_OUTPUT} allowedSlippage={TEST_ALLOWED_SLIPPAGE} />)
await act(() => userEvent.hover(screen.getByText(/Maximum input/i)))
expect(await screen.getByText(/The minimum amount you are guaranteed to receive./i)).toBeVisible()

@ -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<Currency, Currency, TradeType>
trade: InterfaceTrade
allowedSlippage: Percent
syncing?: boolean
}
@ -60,7 +60,7 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }:
</ThemedText.BodySmall>
</MouseoverTooltip>
<TextWithLoadingPlaceholder syncing={syncing} width={50}>
<ThemedText.BodySmall>~${trade.gasUseEstimateUSD.toFixed(2)}</ThemedText.BodySmall>
<ThemedText.BodySmall>~${trade.gasUseEstimateUSD}</ThemedText.BodySmall>
</TextWithLoadingPlaceholder>
</RowBetween>
)}

@ -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<Currency, Currency, TradeType> | undefined
originalTrade: Trade<Currency, Currency, TradeType> | undefined
trade: InterfaceTrade | undefined
originalTrade: InterfaceTrade | undefined
attemptingTxn: boolean
txHash: string | undefined
recipient: string | null

@ -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<Currency, Currency, TradeType> // 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 (

@ -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(
<SwapDetailsDropdown
trade={TEST_TRADE_EXACT_INPUT}

@ -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 { useWeb3React } from '@web3-react/core'
import AnimatedDropdown from 'components/AnimatedDropdown'
import Column from 'components/Column'
@ -92,7 +92,7 @@ const Wrapper = styled(Column)`
`
interface SwapDetailsInlineProps {
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
trade: InterfaceTrade | undefined
syncing: boolean
loading: boolean
allowedSlippage: Percent

@ -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<Currency, Currency, TradeType>
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<Currency, Currency, TradeType>
trade: InterfaceTrade
hash: string | undefined
allowedSlippage: Percent
onConfirm: () => void

@ -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<Currency, Currency, TradeType>,
trade: InterfaceTrade,
priceUpdate: number | undefined,
response: SwapPriceUpdateUserResponse
) => ({
@ -65,7 +65,7 @@ export default function SwapModalHeader({
showAcceptChanges,
onAcceptChanges,
}: {
trade: InterfaceTrade<Currency, Currency, TradeType>
trade: InterfaceTrade
shouldLogModalCloseEvent: boolean
setShouldLogModalCloseEvent: (shouldLog: boolean) => void
allowedSlippage: Percent

@ -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<Currency, Currency, TradeType>
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 (

@ -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"
>
<div
class="c2 c3 c5"
class="c5"
>
<div>
<div
class="c6 css-zhpkf8"
>
Network fee
</div>
</div>
</div>
<div
class="c7 css-zhpkf8"
>
~$1.00
</div>
</div>
<div
class="c2 c3 c4"
>
<div
class="c2 c3 c8"
>
<div
class="c6"
class="c5"
>
<div>
<div
class="c7 css-zhpkf8"
class="c6 css-zhpkf8"
>
Minimum output
</div>
@ -97,7 +117,7 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
</div>
</div>
<div
class="c8 css-zhpkf8"
class="c7 css-zhpkf8"
>
0.00000000000000098 DEF
</div>
@ -106,14 +126,14 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
class="c2 c3 c4"
>
<div
class="c2 c3 c5"
class="c2 c3 c8"
>
<div
class="c6"
class="c5"
>
<div>
<div
class="c7 css-zhpkf8"
class="c6 css-zhpkf8"
>
Expected output
</div>
@ -121,7 +141,7 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
</div>
</div>
<div
class="c8 css-zhpkf8"
class="c7 css-zhpkf8"
>
0.000000000000001 DEF
</div>
@ -133,16 +153,16 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
class="c2 c3 c4"
>
<div
class="c7 css-zhpkf8"
class="c6 css-zhpkf8"
>
Order routing
</div>
<div
class="c6"
class="c5"
>
<div>
<div
class="c8 css-zhpkf8"
class="c7 css-zhpkf8"
>
Uniswap API
</div>

@ -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`] = `
<div
class="c2 c3 c6"
>
<svg
<div
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"
height="24"
stroke="#98A1C0"
@ -207,15 +240,35 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
>
<div>
<div
class="c11"
class="c14"
data-testid="advanced-swap-details"
>
<div
class="c12"
class="c15"
>
<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
class="c2 c3 c4"
>
@ -223,11 +276,11 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
class="c2 c3 c6"
>
<div
class="c14"
class="c10"
>
<div>
<div
class="c15 css-zhpkf8"
class="c12 css-zhpkf8"
>
Minimum output
</div>
@ -247,11 +300,11 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
class="c2 c3 c6"
>
<div
class="c14"
class="c10"
>
<div>
<div
class="c15 css-zhpkf8"
class="c12 css-zhpkf8"
>
Expected output
</div>
@ -265,18 +318,18 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
</div>
</div>
<div
class="c13"
class="c16"
/>
<div
class="c2 c3 c4"
>
<div
class="c15 css-zhpkf8"
class="c12 css-zhpkf8"
>
Order routing
</div>
<div
class="c14"
class="c10"
>
<div>
<div

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

@ -113,3 +113,7 @@ export const L2_CHAIN_IDS = [
] as const
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 useGasPrice from './useGasPrice'
import useStablecoinPrice, { useStablecoinValue } from './useStablecoinPrice'
import useStablecoinPrice, { useStablecoinAmountFromFiatValue, useStablecoinValue } from './useStablecoinPrice'
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.
*/
export default function useAutoSlippageTolerance(
trade: InterfaceTrade<Currency, Currency, TradeType> | 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,
])
}

@ -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', () => {

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

@ -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<TTradeType extends TradeType>(
tradeType: TTradeType,
amountSpecified?: CurrencyAmount<Currency>,
otherCurrency?: Currency
): { state: TradeState; trade: InterfaceTrade<Currency, Currency, TTradeType> | undefined } {
): { state: TradeState; trade: InterfaceTrade | undefined } {
const [currencyIn, currencyOut] =
tradeType === TradeType.EXACT_INPUT
? [amountSpecified?.currency, otherCurrency]
@ -135,7 +135,7 @@ export function useClientSideV3Trade<TTradeType extends TradeType>(
return {
state: TradeState.VALID,
trade: new InterfaceTrade({
trade: new ClassicTrade({
v2Routes: [],
v3Routes: [
{

@ -41,8 +41,8 @@ function useETHValue(currencyAmount?: CurrencyAmount<Currency>): {
}
}
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

@ -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<AlphaRouterConfig>
): 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<AlphaRouterConfig>
): Promise<QuoteResult> {
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<AlphaRouterConfig>
) {
return getQuote(
{
type,
tradeType,
tokenIn: {
address: tokenInAddress,
chainId: tokenInChainId,

@ -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]
)

@ -39,7 +39,7 @@ export const formatSwapSignedAnalyticsEventProperties = ({
fiatValues,
txHash,
}: {
trade: InterfaceTrade<Currency, Currency, TradeType> | Trade<Currency, Currency, TradeType>
trade: InterfaceTrade | Trade<Currency, Currency, TradeType>
fiatValues: { amountIn: number | undefined; amountOut: number | undefined }
txHash: string
}) => ({
@ -61,7 +61,7 @@ export const formatSwapSignedAnalyticsEventProperties = ({
export const formatSwapQuoteReceivedEventProperties = (
trade: Trade<Currency, Currency, TradeType>,
gasUseEstimateUSD?: CurrencyAmount<Token>,
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

@ -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<Currency, Currency, TradeType> | undefined
trade: InterfaceTrade | undefined
}) => {
if (!usingPayWithAnyToken) {
return (
@ -219,7 +219,7 @@ const InputCurrencyValue = ({
)
}
if (tradeState === TradeState.LOADING) {
if (tradeState === TradeState.LOADING && !trade) {
return (
<ThemedText.BodyPrimary color="textTertiary" lineHeight="20px" fontWeight="500">
<Trans>Fetching price...</Trans>
@ -228,7 +228,7 @@ const InputCurrencyValue = ({
}
return (
<ValueText color={tradeState === TradeState.SYNCING ? 'textTertiary' : 'textPrimary'}>
<ValueText color={tradeState === TradeState.LOADING ? 'textTertiary' : 'textPrimary'}>
{ethNumberStandardFormatter(trade?.inputAmount.toExact())}
</ValueText>
)

@ -9,7 +9,7 @@ export default function useDerivedPayWithAnyTokenSwapInfo(
parsedOutputAmount?: CurrencyAmount<NativeCurrency | Token>
): {
state: TradeState
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
trade: InterfaceTrade | undefined
maximumAmountIn: CurrencyAmount<Token> | undefined
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 { 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<Currency, Currency, TradeType> | undefined,
trade?: InterfaceTrade | undefined,
allowance?: Allowance,
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 { InterfaceTrade } from 'state/routing/types'
import { useTheme } from 'styled-components/macro'
@ -14,7 +14,7 @@ interface PriceImpactSeverity {
color: string
}
export function usePriceImpact(trade?: InterfaceTrade<Currency, Currency, TradeType>): PriceImpact | undefined {
export function usePriceImpact(trade?: InterfaceTrade): PriceImpact | undefined {
const theme = useTheme()
return useMemo(() => {

@ -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<Currency, Currency, TradeType>): {
export function buildAllTradeRouteInputs(trade: InterfaceTrade): {
mixedTokenTradeRouteInputs: TokenTradeRouteInput[] | undefined
v2TokenTradeRouteInputs: TokenTradeRouteInput[] | undefined
v3TokenTradeRouteInputs: TokenTradeRouteInput[] | undefined

@ -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<Currency, Currency, TradeType> | 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<Currency, Currency, TradeType> | undefined
tradeToConfirm: InterfaceTrade | undefined
attemptingTxn: boolean
swapErrorMessage: string | undefined
txHash: string | undefined

@ -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() }),

@ -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<GetQuoteResult, GetQuoteArgs>({
getQuote: build.query<TradeResult, GetQuoteArgs>({
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`,

@ -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<TInput, TOutput, TTradeType> {
gasUseEstimateUSD: CurrencyAmount<Token> | null | undefined
gasUseEstimateUSD: string | null | undefined
blockNumber: string | null | undefined
constructor({
@ -81,8 +80,8 @@ export class InterfaceTrade<
blockNumber,
...routes
}: {
gasUseEstimateUSD?: CurrencyAmount<Token> | undefined | null
blockNumber?: string | null | undefined
gasUseEstimateUSD?: string | null
blockNumber?: string | null
v2Routes: {
routev2: V2Route<TInput, TOutput>
inputAmount: CurrencyAmount<TInput>
@ -105,3 +104,42 @@ export class InterfaceTrade<
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 { 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<TTradeType extends TradeType>(
routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE
): {
state: TradeState
trade: InterfaceTrade<Currency, Currency, TTradeType> | undefined
trade?: InterfaceTrade
} {
const [currencyIn, currencyOut]: [Currency | undefined, Currency | undefined] = useMemo(
() =>
@ -44,10 +46,9 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
})
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<TTradeType extends TradeType>(
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

@ -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)

@ -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<GetQuoteResult, 'route'> | undefined
) {
if (!quoteResult || !quoteResult.route || !currencyIn || !currencyOut) return undefined
tokenInIsNative: boolean,
tokenOutIsNative: boolean,
routes: QuoteData['route']
):
| {
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 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<V2PoolInRoute>(PoolType.V2Pool, route)
const isOnlyV3 = isVersionedRoute<V3PoolInRoute>(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<TTradeType extends TradeType>(
route: ReturnType<typeof computeRoutes>,
tradeType: TTradeType,
blockNumber?: string | null,
gasUseEstimateUSD?: CurrencyAmount<Token> | null
): InterfaceTrade<Currency, Currency, TTradeType> {
return new InterfaceTrade({
v2Routes:
route
?.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(
(r): r is typeof route[0] & { mixedRoute: NonNullable<typeof route[0]['mixedRoute']> } =>
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<T extends V2PoolInRoute | V3PoolInRoute>(
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<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
)
.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
}

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

@ -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),

@ -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<Currency, Currency, TradeType>
): 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

@ -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<Currency>,
{
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 }
}