feat: Quick routes (#7348)

* wip, added PreviewTrade and now amending request arg type

* updates

* update logic to progress to swap review screen

* add token tax info to preview trades

* add loading component

* add feature flag and fix analytics and perf stuff

* update debounce amount

* add latencyMs measure

* change types

* add inline comments

* actually pass in feature flags

* dep array

* fix snapshot and unit tests

* fix unit tests

* update font color for loading text

* remove all chains feature flag

* remove from feature flag modal

* dont flicker review modal when allowance is loading

* remove comment

* add snapshot tests

* triple equals

* add comment

* change cast
This commit is contained in:
Tina 2023-09-29 12:20:10 -04:00 committed by GitHub
parent e6362212c6
commit c7a8e9e5a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1413 additions and 169 deletions

@ -8,6 +8,7 @@ import { useInfoLiveViewsFlag } from 'featureFlags/flags/infoLiveViews'
import { useInfoPoolPageFlag } from 'featureFlags/flags/infoPoolPage' import { useInfoPoolPageFlag } from 'featureFlags/flags/infoPoolPage'
import { useInfoTDPFlag } from 'featureFlags/flags/infoTDP' import { useInfoTDPFlag } from 'featureFlags/flags/infoTDP'
import { useMultichainUXFlag } from 'featureFlags/flags/multichainUx' import { useMultichainUXFlag } from 'featureFlags/flags/multichainUx'
import { useQuickRouteMainnetFlag } from 'featureFlags/flags/quickRouteMainnet'
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc' import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
import { useUniswapXDefaultEnabledFlag } from 'featureFlags/flags/uniswapXDefault' import { useUniswapXDefaultEnabledFlag } from 'featureFlags/flags/uniswapXDefault'
import { useUniswapXEthOutputFlag } from 'featureFlags/flags/uniswapXEthOutput' import { useUniswapXEthOutputFlag } from 'featureFlags/flags/uniswapXEthOutput'
@ -254,6 +255,14 @@ export default function FeatureFlagModal() {
featureFlag={FeatureFlag.fotAdjustedmentsEnabled} featureFlag={FeatureFlag.fotAdjustedmentsEnabled}
label="Enable fee-on-transfer UI and slippage adjustments" label="Enable fee-on-transfer UI and slippage adjustments"
/> />
<FeatureFlagGroup name="Quick routes">
<FeatureFlagOption
variant={BaseVariant}
value={useQuickRouteMainnetFlag()}
featureFlag={FeatureFlag.quickRouteMainnet}
label="Enable quick routes for Mainnet"
/>
</FeatureFlagGroup>
<FeatureFlagGroup name="UniswapX Flags"> <FeatureFlagGroup name="UniswapX Flags">
<FeatureFlagOption <FeatureFlagOption
variant={BaseVariant} variant={BaseVariant}

@ -0,0 +1,62 @@
import styled, { keyframes } from 'styled-components'
const StyledPollingDot = styled.div`
width: 8px;
height: 8px;
min-height: 8px;
min-width: 8px;
border-radius: 50%;
position: relative;
background-color: ${({ theme }) => theme.surface3};
transition: 250ms ease background-color;
`
const StyledPolling = styled.div`
display: flex;
height: 16px;
width: 16px;
margin-right: 2px;
margin-left: 2px;
align-items: center;
color: ${({ theme }) => theme.neutral1};
transition: 250ms ease color;
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
display: none;
`}
`
const rotate360 = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`
const Spinner = styled.div`
animation: ${rotate360} 1s cubic-bezier(0.83, 0, 0.17, 1) infinite;
transform: translateZ(0);
border-top: 1px solid transparent;
border-right: 1px solid transparent;
border-bottom: 1px solid transparent;
border-left: 2px solid ${({ theme }) => theme.neutral1};
background: transparent;
width: 14px;
height: 14px;
border-radius: 50%;
position: relative;
transition: 250ms ease border-color;
left: -3px;
top: -3px;
`
export default function SpinningLoader() {
return (
<StyledPolling>
<StyledPollingDot>
<Spinner />
</StyledPollingDot>
</StyledPolling>
)
}

@ -34,7 +34,7 @@ export const LoadingRows = styled.div`
export const loadingOpacityMixin = css<{ $loading: boolean }>` export const loadingOpacityMixin = css<{ $loading: boolean }>`
filter: ${({ $loading }) => ($loading ? 'grayscale(1)' : 'none')}; filter: ${({ $loading }) => ($loading ? 'grayscale(1)' : 'none')};
opacity: ${({ $loading }) => ($loading ? '0.4' : '1')}; opacity: ${({ $loading }) => ($loading ? '0.6' : '1')};
transition: ${({ $loading, theme }) => transition: ${({ $loading, theme }) =>
$loading ? 'none' : `opacity ${theme.transition.duration.medium} ${theme.transition.timing.inOut}`}; $loading ? 'none' : `opacity ${theme.transition.duration.medium} ${theme.transition.timing.inOut}`};
` `

@ -1,10 +1,10 @@
import { InterfaceTrade, QuoteMethod } from 'state/routing/types' import { QuoteMethod, SubmittableTrade } from 'state/routing/types'
import { isUniswapXTrade } from 'state/routing/utils' import { isUniswapXTrade } from 'state/routing/utils'
import { ThemedText } from 'theme/components' import { ThemedText } from 'theme/components'
import UniswapXRouterLabel from './UniswapXRouterLabel' import UniswapXRouterLabel from './UniswapXRouterLabel'
export default function RouterLabel({ trade }: { trade: InterfaceTrade }) { export default function RouterLabel({ trade }: { trade: SubmittableTrade }) {
if (isUniswapXTrade(trade)) { if (isUniswapXTrade(trade)) {
return ( return (
<UniswapXRouterLabel> <UniswapXRouterLabel>

@ -8,7 +8,7 @@ import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
import { ZERO_PERCENT } from 'constants/misc' import { ZERO_PERCENT } from 'constants/misc'
import useNativeCurrency from 'lib/hooks/useNativeCurrency' import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { ClassicTrade, InterfaceTrade } from 'state/routing/types' import { ClassicTrade, InterfaceTrade } from 'state/routing/types'
import { getTransactionCount, isClassicTrade } from 'state/routing/utils' import { getTransactionCount, isClassicTrade, isSubmittableTrade } from 'state/routing/utils'
import { ExternalLink, Separator, ThemedText } from 'theme/components' import { ExternalLink, Separator, ThemedText } from 'theme/components'
import { NumberType, useFormatter } from 'utils/formatNumbers' import { NumberType, useFormatter } from 'utils/formatNumbers'
@ -49,7 +49,7 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }:
const txCount = getTransactionCount(trade) const txCount = getTransactionCount(trade)
const { formatCurrencyAmount, formatNumber, formatPriceImpact } = useFormatter() const { formatCurrencyAmount, formatNumber, formatPriceImpact } = useFormatter()
const supportsGasEstimate = chainId && SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) const supportsGasEstimate = chainId && SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) && isSubmittableTrade(trade)
return ( return (
<Column gap="md"> <Column gap="md">
@ -150,37 +150,39 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }:
</TextWithLoadingPlaceholder> </TextWithLoadingPlaceholder>
</RowBetween> </RowBetween>
<Separator /> <Separator />
<RowBetween> {isSubmittableTrade(trade) && (
<ThemedText.BodySmall color="neutral2"> <RowBetween>
<Trans>Order routing</Trans> <ThemedText.BodySmall color="neutral2">
</ThemedText.BodySmall> <Trans>Order routing</Trans>
{isClassicTrade(trade) ? ( </ThemedText.BodySmall>
<MouseoverTooltip {isClassicTrade(trade) ? (
size={TooltipSize.Large} <MouseoverTooltip
text={<SwapRoute data-testid="swap-route-info" trade={trade} syncing={syncing} />} size={TooltipSize.Large}
onOpen={() => { text={<SwapRoute data-testid="swap-route-info" trade={trade} syncing={syncing} />}
sendAnalyticsEvent(SwapEventName.SWAP_AUTOROUTER_VISUALIZATION_EXPANDED, { onOpen={() => {
element: InterfaceElementName.AUTOROUTER_VISUALIZATION_ROW, sendAnalyticsEvent(SwapEventName.SWAP_AUTOROUTER_VISUALIZATION_EXPANDED, {
}) element: InterfaceElementName.AUTOROUTER_VISUALIZATION_ROW,
}} })
> }}
<RouterLabel trade={trade} /> >
</MouseoverTooltip> <RouterLabel trade={trade} />
) : ( </MouseoverTooltip>
<MouseoverTooltip ) : (
size={TooltipSize.Small} <MouseoverTooltip
text={<GasBreakdownTooltip trade={trade} hideFees />} size={TooltipSize.Small}
placement="right" text={<GasBreakdownTooltip trade={trade} hideFees />}
onOpen={() => { placement="right"
sendAnalyticsEvent(SwapEventName.SWAP_AUTOROUTER_VISUALIZATION_EXPANDED, { onOpen={() => {
element: InterfaceElementName.AUTOROUTER_VISUALIZATION_ROW, sendAnalyticsEvent(SwapEventName.SWAP_AUTOROUTER_VISUALIZATION_EXPANDED, {
}) element: InterfaceElementName.AUTOROUTER_VISUALIZATION_ROW,
}} })
> }}
<RouterLabel trade={trade} /> >
</MouseoverTooltip> <RouterLabel trade={trade} />
)} </MouseoverTooltip>
</RowBetween> )}
</RowBetween>
)}
</Column> </Column>
) )
} }

@ -22,6 +22,7 @@ import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { getPriceUpdateBasisPoints } from 'lib/utils/analytics' import { getPriceUpdateBasisPoints } from 'lib/utils/analytics'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { InterfaceTrade, TradeFillType } from 'state/routing/types' import { InterfaceTrade, TradeFillType } from 'state/routing/types'
import { isPreviewTrade } from 'state/routing/utils'
import { Field } from 'state/swap/actions' import { Field } from 'state/swap/actions'
import { useIsTransactionConfirmed, useSwapTransactionStatus } from 'state/transactions/hooks' import { useIsTransactionConfirmed, useSwapTransactionStatus } from 'state/transactions/hooks'
import styled from 'styled-components' import styled from 'styled-components'
@ -363,7 +364,8 @@ export default function ConfirmSwapModal({
trade={trade} trade={trade}
swapResult={swapResult} swapResult={swapResult}
allowedSlippage={allowedSlippage} allowedSlippage={allowedSlippage}
disabledConfirm={showAcceptChanges} isLoading={isPreviewTrade(trade)}
disabledConfirm={showAcceptChanges || isPreviewTrade(trade) || allowance.state === AllowanceState.LOADING}
fiatValueInput={fiatValueInput} fiatValueInput={fiatValueInput}
fiatValueOutput={fiatValueOutput} fiatValueOutput={fiatValueOutput}
showAcceptChanges={showAcceptChanges} showAcceptChanges={showAcceptChanges}

@ -3,7 +3,7 @@ import { AutoColumn } from 'components/Column'
import UniswapXRouterLabel, { UniswapXGradient } from 'components/RouterLabel/UniswapXRouterLabel' import UniswapXRouterLabel, { UniswapXGradient } from 'components/RouterLabel/UniswapXRouterLabel'
import Row from 'components/Row' import Row from 'components/Row'
import { ReactNode } from 'react' import { ReactNode } from 'react'
import { InterfaceTrade } from 'state/routing/types' import { SubmittableTrade } from 'state/routing/types'
import { isClassicTrade, isUniswapXTrade } from 'state/routing/utils' import { isClassicTrade, isUniswapXTrade } from 'state/routing/utils'
import styled from 'styled-components' import styled from 'styled-components'
import { Divider, ExternalLink, ThemedText } from 'theme/components' import { Divider, ExternalLink, ThemedText } from 'theme/components'
@ -56,7 +56,7 @@ export function GasBreakdownTooltip({
hideFees = false, hideFees = false,
hideUniswapXDescription = false, hideUniswapXDescription = false,
}: { }: {
trade: InterfaceTrade trade: SubmittableTrade
hideFees?: boolean hideFees?: boolean
hideUniswapXDescription?: boolean hideUniswapXDescription?: boolean
}) { }) {

@ -7,7 +7,7 @@ import { UniswapXRouterIcon } from 'components/RouterLabel/UniswapXRouterLabel'
import Row, { RowFixed } from 'components/Row' import Row, { RowFixed } from 'components/Row'
import { MouseoverTooltip, TooltipSize } from 'components/Tooltip' import { MouseoverTooltip, TooltipSize } from 'components/Tooltip'
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains' import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
import { InterfaceTrade } from 'state/routing/types' import { SubmittableTrade } from 'state/routing/types'
import { isUniswapXTrade } from 'state/routing/utils' import { isUniswapXTrade } from 'state/routing/utils'
import styled from 'styled-components' import styled from 'styled-components'
import { ThemedText } from 'theme/components' import { ThemedText } from 'theme/components'
@ -24,7 +24,7 @@ const StyledGasIcon = styled(Gas)`
} }
` `
export default function GasEstimateTooltip({ trade, loading }: { trade?: InterfaceTrade; loading: boolean }) { export default function GasEstimateTooltip({ trade, loading }: { trade?: SubmittableTrade; loading: boolean }) {
const { chainId } = useWeb3React() const { chainId } = useWeb3React()
const { formatNumber } = useFormatter() const { formatNumber } = useFormatter()

@ -10,7 +10,8 @@ import { formatCommonPropertiesForTrade } from 'lib/utils/analytics'
import { useState } from 'react' import { useState } from 'react'
import { ChevronDown } from 'react-feather' import { ChevronDown } from 'react-feather'
import { InterfaceTrade } from 'state/routing/types' import { InterfaceTrade } from 'state/routing/types'
import styled, { keyframes, useTheme } from 'styled-components' import { isSubmittableTrade } from 'state/routing/utils'
import styled, { useTheme } from 'styled-components'
import { ThemedText } from 'theme/components' import { ThemedText } from 'theme/components'
import { AdvancedSwapDetails } from './AdvancedSwapDetails' import { AdvancedSwapDetails } from './AdvancedSwapDetails'
@ -28,58 +29,6 @@ const RotatingArrow = styled(ChevronDown)<{ open?: boolean }>`
transition: transform 0.1s linear; transition: transform 0.1s linear;
` `
const StyledPolling = styled.div`
display: flex;
height: 16px;
width: 16px;
margin-right: 2px;
margin-left: 2px;
align-items: center;
color: ${({ theme }) => theme.neutral1};
transition: 250ms ease color;
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
display: none;
`}
`
const StyledPollingDot = styled.div`
width: 8px;
height: 8px;
min-height: 8px;
min-width: 8px;
border-radius: 50%;
position: relative;
background-color: ${({ theme }) => theme.surface3};
transition: 250ms ease background-color;
`
const rotate360 = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`
const Spinner = styled.div`
animation: ${rotate360} 1s cubic-bezier(0.83, 0, 0.17, 1) infinite;
transform: translateZ(0);
border-top: 1px solid transparent;
border-right: 1px solid transparent;
border-bottom: 1px solid transparent;
border-left: 2px solid ${({ theme }) => theme.neutral1};
background: transparent;
width: 14px;
height: 14px;
border-radius: 50%;
position: relative;
transition: 250ms ease border-color;
left: -3px;
top: -3px;
`
const SwapDetailsWrapper = styled.div` const SwapDetailsWrapper = styled.div`
padding-top: ${({ theme }) => theme.grids.md}; padding-top: ${({ theme }) => theme.grids.md};
` `
@ -121,13 +70,6 @@ export default function SwapDetailsDropdown({ trade, syncing, loading, allowedSl
open={showDetails} open={showDetails}
> >
<RowFixed> <RowFixed>
{Boolean(loading || syncing) && (
<StyledPolling>
<StyledPollingDot>
<Spinner />
</StyledPollingDot>
</StyledPolling>
)}
{trade ? ( {trade ? (
<LoadingOpacityContainer $loading={syncing} data-testid="trade-price-container"> <LoadingOpacityContainer $loading={syncing} data-testid="trade-price-container">
<TradePrice price={trade.executionPrice} /> <TradePrice price={trade.executionPrice} />
@ -139,7 +81,9 @@ export default function SwapDetailsDropdown({ trade, syncing, loading, allowedSl
) : null} ) : null}
</RowFixed> </RowFixed>
<RowFixed gap="xs"> <RowFixed gap="xs">
{!showDetails && <GasEstimateTooltip trade={trade} loading={syncing || loading} />} {!showDetails && isSubmittableTrade(trade) && (
<GasEstimateTooltip trade={trade} loading={syncing || loading} />
)}
<RotatingArrow stroke={trade ? theme.neutral3 : theme.surface2} open={Boolean(trade && showDetails)} /> <RotatingArrow stroke={trade ? theme.neutral3 : theme.surface2} open={Boolean(trade && showDetails)} />
</RowFixed> </RowFixed>
</StyledHeaderRow> </StyledHeaderRow>

@ -1,4 +1,5 @@
import { import {
PREVIEW_EXACT_IN_TRADE,
TEST_ALLOWED_SLIPPAGE, TEST_ALLOWED_SLIPPAGE,
TEST_TOKEN_1, TEST_TOKEN_1,
TEST_TOKEN_2, TEST_TOKEN_2,
@ -15,6 +16,7 @@ describe('SwapModalFooter.tsx', () => {
it('matches base snapshot, test trade exact input', () => { it('matches base snapshot, test trade exact input', () => {
const { asFragment } = render( const { asFragment } = render(
<SwapModalFooter <SwapModalFooter
isLoading={false}
trade={TEST_TRADE_EXACT_INPUT} trade={TEST_TRADE_EXACT_INPUT}
allowedSlippage={TEST_ALLOWED_SLIPPAGE} allowedSlippage={TEST_ALLOWED_SLIPPAGE}
swapResult={undefined} swapResult={undefined}
@ -50,6 +52,7 @@ describe('SwapModalFooter.tsx', () => {
const mockAcceptChanges = jest.fn() const mockAcceptChanges = jest.fn()
render( render(
<SwapModalFooter <SwapModalFooter
isLoading={false}
trade={TEST_TRADE_EXACT_INPUT} trade={TEST_TRADE_EXACT_INPUT}
allowedSlippage={TEST_ALLOWED_SLIPPAGE} allowedSlippage={TEST_ALLOWED_SLIPPAGE}
swapResult={undefined} swapResult={undefined}
@ -77,6 +80,7 @@ describe('SwapModalFooter.tsx', () => {
it('test trade exact output, no recipient', () => { it('test trade exact output, no recipient', () => {
render( render(
<SwapModalFooter <SwapModalFooter
isLoading={false}
trade={TEST_TRADE_EXACT_OUTPUT} trade={TEST_TRADE_EXACT_OUTPUT}
allowedSlippage={TEST_ALLOWED_SLIPPAGE} allowedSlippage={TEST_ALLOWED_SLIPPAGE}
swapResult={undefined} swapResult={undefined}
@ -109,6 +113,7 @@ describe('SwapModalFooter.tsx', () => {
it('test trade fee on input token transfer', () => { it('test trade fee on input token transfer', () => {
render( render(
<SwapModalFooter <SwapModalFooter
isLoading={false}
trade={TEST_TRADE_FEE_ON_SELL} trade={TEST_TRADE_FEE_ON_SELL}
allowedSlippage={TEST_ALLOWED_SLIPPAGE} allowedSlippage={TEST_ALLOWED_SLIPPAGE}
swapResult={undefined} swapResult={undefined}
@ -138,6 +143,7 @@ describe('SwapModalFooter.tsx', () => {
it('test trade fee on output token transfer', () => { it('test trade fee on output token transfer', () => {
render( render(
<SwapModalFooter <SwapModalFooter
isLoading={false}
trade={TEST_TRADE_FEE_ON_BUY} trade={TEST_TRADE_FEE_ON_BUY}
allowedSlippage={TEST_ALLOWED_SLIPPAGE} allowedSlippage={TEST_ALLOWED_SLIPPAGE}
swapResult={undefined} swapResult={undefined}
@ -163,4 +169,30 @@ describe('SwapModalFooter.tsx', () => {
).toBeInTheDocument() ).toBeInTheDocument()
expect(screen.getByText(`${TEST_TOKEN_2.symbol} fee`)).toBeInTheDocument() expect(screen.getByText(`${TEST_TOKEN_2.symbol} fee`)).toBeInTheDocument()
}) })
it('renders a preview trade while disabling submission', () => {
const { asFragment } = render(
<SwapModalFooter
isLoading
trade={PREVIEW_EXACT_IN_TRADE}
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
swapResult={undefined}
onConfirm={jest.fn()}
swapErrorMessage={undefined}
disabledConfirm
fiatValueInput={{
data: undefined,
isLoading: false,
}}
fiatValueOutput={{
data: undefined,
isLoading: false,
}}
showAcceptChanges={false}
onAcceptChanges={jest.fn()}
/>
)
expect(asFragment()).toMatchSnapshot()
expect(screen.getByText('Finalizing quote...')).toBeInTheDocument()
})
}) })

@ -4,6 +4,7 @@ import { Percent, TradeType } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { TraceEvent } from 'analytics' import { TraceEvent } from 'analytics'
import Column from 'components/Column' import Column from 'components/Column'
import SpinningLoader from 'components/Loader/SpinningLoader'
import { MouseoverTooltip, TooltipSize } from 'components/Tooltip' import { MouseoverTooltip, TooltipSize } from 'components/Tooltip'
import { ZERO_PERCENT } from 'constants/misc' import { ZERO_PERCENT } from 'constants/misc'
import { SwapResult } from 'hooks/useSwapCallback' import { SwapResult } from 'hooks/useSwapCallback'
@ -11,8 +12,8 @@ import useTransactionDeadline from 'hooks/useTransactionDeadline'
import useNativeCurrency from 'lib/hooks/useNativeCurrency' import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { ReactNode } from 'react' import { ReactNode } from 'react'
import { AlertTriangle } from 'react-feather' import { AlertTriangle } from 'react-feather'
import { ClassicTrade, InterfaceTrade, RouterPreference } from 'state/routing/types' import { ClassicTrade, InterfaceTrade, PreviewTrade, RouterPreference } from 'state/routing/types'
import { getTransactionCount, isClassicTrade } from 'state/routing/utils' import { getTransactionCount, isClassicTrade, isPreviewTrade, isSubmittableTrade } from 'state/routing/utils'
import { useRouterPreference, useUserSlippageTolerance } from 'state/user/hooks' import { useRouterPreference, useUserSlippageTolerance } from 'state/user/hooks'
import styled, { DefaultTheme, useTheme } from 'styled-components' import styled, { DefaultTheme, useTheme } from 'styled-components'
import { ExternalLink, ThemedText } from 'theme/components' import { ExternalLink, ThemedText } from 'theme/components'
@ -60,6 +61,7 @@ export default function SwapModalFooter({
fiatValueOutput, fiatValueOutput,
showAcceptChanges, showAcceptChanges,
onAcceptChanges, onAcceptChanges,
isLoading,
}: { }: {
trade: InterfaceTrade trade: InterfaceTrade
swapResult?: SwapResult swapResult?: SwapResult
@ -71,6 +73,7 @@ export default function SwapModalFooter({
fiatValueOutput: { data?: number; isLoading: boolean } fiatValueOutput: { data?: number; isLoading: boolean }
showAcceptChanges: boolean showAcceptChanges: boolean
onAcceptChanges: () => void onAcceptChanges: () => void
isLoading: boolean
}) { }) {
const transactionDeadlineSecondsSinceEpoch = useTransactionDeadline()?.toNumber() // in seconds since epoch const transactionDeadlineSecondsSinceEpoch = useTransactionDeadline()?.toNumber() // in seconds since epoch
const isAutoSlippage = useUserSlippageTolerance()[0] === 'auto' const isAutoSlippage = useUserSlippageTolerance()[0] === 'auto'
@ -113,17 +116,23 @@ export default function SwapModalFooter({
<Plural value={txCount} one="Network fee" other="Network fees" /> <Plural value={txCount} one="Network fee" other="Network fees" />
</Label> </Label>
</MouseoverTooltip> </MouseoverTooltip>
<MouseoverTooltip placement="right" size={TooltipSize.Small} text={<GasBreakdownTooltip trade={trade} />}> <MouseoverTooltip
placement="right"
size={TooltipSize.Small}
text={isSubmittableTrade(trade) ? <GasBreakdownTooltip trade={trade} /> : undefined}
>
<DetailRowValue> <DetailRowValue>
{formatNumber({ {isSubmittableTrade(trade)
input: trade.totalGasUseEstimateUSD, ? formatNumber({
type: NumberType.FiatGasPrice, input: trade.totalGasUseEstimateUSD,
})} type: NumberType.FiatGasPrice,
})
: '-'}
</DetailRowValue> </DetailRowValue>
</MouseoverTooltip> </MouseoverTooltip>
</Row> </Row>
</ThemedText.BodySmall> </ThemedText.BodySmall>
{isClassicTrade(trade) && ( {(isClassicTrade(trade) || isPreviewTrade(trade)) && (
<> <>
<TokenTaxLineItem trade={trade} type="input" /> <TokenTaxLineItem trade={trade} type="input" />
<TokenTaxLineItem trade={trade} type="output" /> <TokenTaxLineItem trade={trade} type="output" />
@ -134,8 +143,10 @@ export default function SwapModalFooter({
<Trans>Price impact</Trans> <Trans>Price impact</Trans>
</Label> </Label>
</MouseoverTooltip> </MouseoverTooltip>
<DetailRowValue warningColor={getPriceImpactColor(trade.priceImpact)}> <DetailRowValue
{trade.priceImpact ? formatPriceImpact(trade.priceImpact) : '-'} warningColor={isClassicTrade(trade) ? getPriceImpactColor(trade.priceImpact) : undefined}
>
{isClassicTrade(trade) && trade.priceImpact ? formatPriceImpact(trade.priceImpact) : '-'}
</DetailRowValue> </DetailRowValue>
</Row> </Row>
</ThemedText.BodySmall> </ThemedText.BodySmall>
@ -219,9 +230,18 @@ export default function SwapModalFooter({
$borderRadius="12px" $borderRadius="12px"
id={InterfaceElementName.CONFIRM_SWAP_BUTTON} id={InterfaceElementName.CONFIRM_SWAP_BUTTON}
> >
<ThemedText.HeadlineSmall color="deprecated_accentTextLightPrimary"> {isLoading ? (
<Trans>Confirm swap</Trans> <ThemedText.HeadlineSmall color="neutral2">
</ThemedText.HeadlineSmall> <Row>
<SpinningLoader />
<Trans>Finalizing quote...</Trans>
</Row>
</ThemedText.HeadlineSmall>
) : (
<ThemedText.HeadlineSmall color="deprecated_accentTextLightPrimary">
<Trans>Confirm swap</Trans>
</ThemedText.HeadlineSmall>
)}
</ConfirmButton> </ConfirmButton>
</TraceEvent> </TraceEvent>
@ -232,7 +252,7 @@ export default function SwapModalFooter({
) )
} }
function TokenTaxLineItem({ trade, type }: { trade: ClassicTrade; type: 'input' | 'output' }) { function TokenTaxLineItem({ trade, type }: { trade: ClassicTrade | PreviewTrade; type: 'input' | 'output' }) {
const { formatPriceImpact } = useFormatter() const { formatPriceImpact } = useFormatter()
const [currency, percentage] = const [currency, percentage] =

@ -1,7 +1,9 @@
import { import {
ETH_MAINNET, ETH_MAINNET,
PREVIEW_EXACT_IN_TRADE,
TEST_ALLOWED_SLIPPAGE, TEST_ALLOWED_SLIPPAGE,
TEST_DUTCH_TRADE_ETH_INPUT, TEST_DUTCH_TRADE_ETH_INPUT,
TEST_TOKEN_2,
TEST_TRADE_EXACT_INPUT, TEST_TRADE_EXACT_INPUT,
TEST_TRADE_EXACT_OUTPUT, TEST_TRADE_EXACT_OUTPUT,
} from 'test-utils/constants' } from 'test-utils/constants'
@ -44,4 +46,15 @@ describe('SwapModalHeader.tsx', () => {
expect(screen.getByTestId('INPUT-amount')).toHaveTextContent(`<0.00001 ABC`) expect(screen.getByTestId('INPUT-amount')).toHaveTextContent(`<0.00001 ABC`)
expect(screen.getByTestId('OUTPUT-amount')).toHaveTextContent(`<0.00001 GHI`) expect(screen.getByTestId('OUTPUT-amount')).toHaveTextContent(`<0.00001 GHI`)
}) })
it('renders preview trades with loading states', () => {
const { asFragment } = render(
<SwapModalHeader
inputCurrency={TEST_TOKEN_2}
trade={PREVIEW_EXACT_IN_TRADE}
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
/>
)
expect(asFragment()).toMatchSnapshot()
})
}) })

@ -3,6 +3,7 @@ import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import Column, { AutoColumn } from 'components/Column' import Column, { AutoColumn } from 'components/Column'
import { useUSDPrice } from 'hooks/useUSDPrice' import { useUSDPrice } from 'hooks/useUSDPrice'
import { InterfaceTrade } from 'state/routing/types' import { InterfaceTrade } from 'state/routing/types'
import { isPreviewTrade } from 'state/routing/utils'
import { Field } from 'state/swap/actions' import { Field } from 'state/swap/actions'
import styled from 'styled-components' import styled from 'styled-components'
import { Divider, ThemedText } from 'theme/components' import { Divider, ThemedText } from 'theme/components'
@ -38,6 +39,7 @@ export default function SwapModalHeader({
amount={trade.inputAmount} amount={trade.inputAmount}
currency={inputCurrency ?? trade.inputAmount.currency} currency={inputCurrency ?? trade.inputAmount.currency}
usdAmount={fiatValueInput.data} usdAmount={fiatValueInput.data}
isLoading={isPreviewTrade(trade) && trade.tradeType === TradeType.EXACT_OUTPUT}
/> />
<SwapModalHeaderAmount <SwapModalHeaderAmount
field={Field.OUTPUT} field={Field.OUTPUT}
@ -45,6 +47,7 @@ export default function SwapModalHeader({
amount={trade.postTaxOutputAmount} amount={trade.postTaxOutputAmount}
currency={trade.outputAmount.currency} currency={trade.outputAmount.currency}
usdAmount={fiatValueOutput.data} usdAmount={fiatValueOutput.data}
isLoading={isPreviewTrade(trade) && trade.tradeType === TradeType.EXACT_INPUT}
tooltipText={ tooltipText={
trade.tradeType === TradeType.EXACT_INPUT ? ( trade.tradeType === TradeType.EXACT_INPUT ? (
<ThemedText.BodySmall> <ThemedText.BodySmall>

@ -33,6 +33,7 @@ const ResponsiveHeadline = ({ children, ...textProps }: PropsWithChildren<TextPr
} }
interface AmountProps { interface AmountProps {
isLoading: boolean
field: Field field: Field
tooltipText?: ReactNode tooltipText?: ReactNode
label: ReactNode label: ReactNode
@ -44,7 +45,15 @@ interface AmountProps {
currency: Currency currency: Currency
} }
export function SwapModalHeaderAmount({ tooltipText, label, amount, usdAmount, field, currency }: AmountProps) { export function SwapModalHeaderAmount({
tooltipText,
label,
amount,
usdAmount,
field,
currency,
isLoading,
}: AmountProps) {
const { formatNumber, formatReviewSwapCurrencyAmount } = useFormatter() const { formatNumber, formatReviewSwapCurrencyAmount } = useFormatter()
return ( return (
@ -56,7 +65,7 @@ export function SwapModalHeaderAmount({ tooltipText, label, amount, usdAmount, f
</MouseoverTooltip> </MouseoverTooltip>
</ThemedText.BodySecondary> </ThemedText.BodySecondary>
<Column gap="xs"> <Column gap="xs">
<ResponsiveHeadline data-testid={`${field}-amount`}> <ResponsiveHeadline data-testid={`${field}-amount`} color={isLoading ? 'neutral2' : undefined}>
{formatReviewSwapCurrencyAmount(amount)} {currency?.symbol} {formatReviewSwapCurrencyAmount(amount)} {currency?.symbol}
</ResponsiveHeadline> </ResponsiveHeadline>
{usdAmount && ( {usdAmount && (

@ -343,3 +343,424 @@ exports[`SwapModalFooter.tsx matches base snapshot, test trade exact input 1`] =
</div> </div>
</DocumentFragment> </DocumentFragment>
`; `;
exports[`SwapModalFooter.tsx renders a preview trade while disabling submission 1`] = `
<DocumentFragment>
.c3 {
box-sizing: border-box;
margin: 0;
min-width: 0;
}
.c4 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: flex-start;
-webkit-box-align: flex-start;
-ms-flex-align: flex-start;
align-items: flex-start;
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
gap: 8px;
}
.c2 {
color: #222222;
}
.c0 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 12px;
}
.c7 {
display: inline-block;
height: inherit;
}
.c5 {
color: #7D7D7D;
margin-right: 8px;
}
.c8 {
cursor: help;
color: #7D7D7D;
margin-right: 8px;
}
.c1 {
padding: 0 8px;
}
.c6 {
text-align: right;
overflow-wrap: break-word;
}
@media (max-width:960px) {
}
<div
class="c0 c1"
>
<div
class="c2 css-142zc9n"
>
<div
class="c3 c4"
>
<div
class="c2 c5 css-142zc9n"
>
Exchange rate
</div>
<div
class="c2 c6 css-142zc9n"
>
1 DEF = 1.00 ABC
</div>
</div>
</div>
<div
class="c2 css-142zc9n"
>
<div
class="c3 c4"
>
<div
class="c7"
>
<div>
<div
class="c2 c8 css-142zc9n"
cursor="help"
>
Network fees
</div>
</div>
</div>
<div
class="c7"
>
<div>
<div
class="c2 c6 css-142zc9n"
>
-
</div>
</div>
</div>
</div>
</div>
<div
class="c2 css-142zc9n"
>
<div
class="c3 c4"
>
<div
class="c7"
>
<div>
<div
class="c2 c8 css-142zc9n"
cursor="help"
>
Price impact
</div>
</div>
</div>
<div
class="c2 c6 css-142zc9n"
>
-
</div>
</div>
</div>
<div
class="c2 css-142zc9n"
>
<div
class="c3 c4"
>
<div
class="c7"
>
<div>
<div
class="c2 c8 css-142zc9n"
cursor="help"
>
Minimum received
</div>
</div>
</div>
<div
class="c2 c6 css-142zc9n"
>
0.00000000000000098 DEF
</div>
</div>
</div>
</div>
.c0 {
box-sizing: border-box;
margin: 0;
min-width: 0;
}
.c3 {
box-sizing: border-box;
margin: 0;
min-width: 0;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
display: inline-block;
text-align: center;
line-height: inherit;
-webkit-text-decoration: none;
text-decoration: none;
font-size: inherit;
padding-left: 16px;
padding-right: 16px;
padding-top: 8px;
padding-bottom: 8px;
color: white;
background-color: primary;
border: 0;
border-radius: 4px;
}
.c1 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c2 {
-webkit-flex-wrap: wrap;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
}
.c2 > * {
margin: !important;
}
.c7 {
color: #7D7D7D;
}
.c9 {
width: 8px;
height: 8px;
min-height: 8px;
min-width: 8px;
border-radius: 50%;
position: relative;
background-color: #22222212;
-webkit-transition: 250ms ease background-color;
transition: 250ms ease background-color;
}
.c8 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
height: 16px;
width: 16px;
margin-right: 2px;
margin-left: 2px;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
color: #222222;
-webkit-transition: 250ms ease color;
transition: 250ms ease color;
}
.c10 {
-webkit-animation: fvtopB 1s cubic-bezier(0.83,0,0.17,1) infinite;
animation: fvtopB 1s cubic-bezier(0.83,0,0.17,1) infinite;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
border-top: 1px solid transparent;
border-right: 1px solid transparent;
border-bottom: 1px solid transparent;
border-left: 2px solid #222222;
background: transparent;
width: 14px;
height: 14px;
border-radius: 50%;
position: relative;
-webkit-transition: 250ms ease border-color;
transition: 250ms ease border-color;
left: -3px;
top: -3px;
}
.c4 {
padding: 16px;
width: 100%;
line-height: 24px;
font-weight: 535;
text-align: center;
border-radius: 16px;
outline: none;
border: 1px solid transparent;
color: #222222;
-webkit-text-decoration: none;
text-decoration: none;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-flex-wrap: nowrap;
-ms-flex-wrap: nowrap;
flex-wrap: nowrap;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
cursor: pointer;
position: relative;
z-index: 1;
will-change: transform;
-webkit-transition: -webkit-transform 450ms ease;
-webkit-transition: transform 450ms ease;
transition: transform 450ms ease;
-webkit-transform: perspective(1px) translateZ(0);
-ms-transform: perspective(1px) translateZ(0);
transform: perspective(1px) translateZ(0);
}
.c4:disabled {
opacity: 50%;
cursor: auto;
pointer-events: none;
}
.c4 > * {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.c4 > a {
-webkit-text-decoration: none;
text-decoration: none;
}
.c5 {
background-color: #FC72FF;
font-size: 20px;
font-weight: 535;
padding: 16px;
color: #FFFFFF;
}
.c5:focus {
box-shadow: 0 0 0 1pt #fb58ff;
background-color: #fb58ff;
}
.c5:hover {
background-color: #fb58ff;
}
.c5:active {
box-shadow: 0 0 0 1pt #fb3fff;
background-color: #fb3fff;
}
.c5:disabled {
background-color: #22222212;
color: #7D7D7D;
cursor: auto;
box-shadow: none;
border: 1px solid transparent;
outline: none;
}
.c6 {
height: 56px;
margin-top: 10px;
}
@media (max-width:960px) {
.c8 {
display: none;
}
}
<div
class="c0 c1 c2"
>
<button
class="c3 c4 c5 c6"
data-testid="confirm-swap-button"
disabled=""
id="confirm-swap-or-send"
>
<div
class="c7 css-1jyz67g"
>
<div
class="c0 c1"
>
<div
class="c8"
>
<div
class="c9"
>
<div
class="c10"
/>
</div>
</div>
Finalizing quote...
</div>
</div>
</button>
</div>
</DocumentFragment>
`;

@ -156,7 +156,7 @@ exports[`SwapModalHeader.tsx matches base snapshot, test trade exact input 1`] =
class="c5" class="c5"
> >
<div <div
class="c8 css-1geukv8" class="css-1geukv8"
data-testid="INPUT-amount" data-testid="INPUT-amount"
> >
&lt;0.00001 ABC &lt;0.00001 ABC
@ -204,7 +204,7 @@ exports[`SwapModalHeader.tsx matches base snapshot, test trade exact input 1`] =
class="c5" class="c5"
> >
<div <div
class="c8 css-1geukv8" class="css-1geukv8"
data-testid="OUTPUT-amount" data-testid="OUTPUT-amount"
> >
&lt;0.00001 DEF &lt;0.00001 DEF
@ -390,7 +390,7 @@ exports[`SwapModalHeader.tsx renders ETH input token for an ETH input UniswapX s
class="c5" class="c5"
> >
<div <div
class="c8 css-1geukv8" class="css-1geukv8"
data-testid="INPUT-amount" data-testid="INPUT-amount"
> >
&lt;0.00001 ETH &lt;0.00001 ETH
@ -438,7 +438,241 @@ exports[`SwapModalHeader.tsx renders ETH input token for an ETH input UniswapX s
class="c5" class="c5"
> >
<div <div
class="c8 css-1geukv8" class="css-1geukv8"
data-testid="OUTPUT-amount"
>
&lt;0.00001 DEF
</div>
</div>
</div>
<div
class="c10"
style="height: 36px; width: 36px;"
>
<div
class="c11"
>
<img
alt="DEF logo"
class="c12"
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x0000000000000000000000000000000000000002/logo.png"
/>
</div>
</div>
</div>
</div>
<div
class="c13 c14"
/>
</div>
</DocumentFragment>
`;
exports[`SwapModalHeader.tsx renders preview trades with loading states 1`] = `
<DocumentFragment>
.c3 {
box-sizing: border-box;
margin: 0;
min-width: 0;
}
.c4 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
gap: 12px;
}
.c6 {
color: #7D7D7D;
}
.c8 {
color: #222222;
}
.c13 {
width: 100%;
height: 1px;
border-width: 0;
margin: 0;
background-color: #22222212;
}
.c2 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 24px;
}
.c5 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 4px;
}
.c0 {
display: grid;
grid-auto-rows: auto;
grid-row-gap: 8px;
}
.c12 {
opacity: 0;
-webkit-transition: opacity 250ms ease-in;
transition: opacity 250ms ease-in;
width: 36px;
height: 36px;
border-radius: 50%;
}
.c11 {
width: 36px;
height: 36px;
background: #22222212;
-webkit-transition: background-color 250ms ease-in;
transition: background-color 250ms ease-in;
box-shadow: 0 0 1px white;
border-radius: 50%;
}
.c10 {
position: relative;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
.c7 {
display: inline-block;
height: inherit;
}
.c9 {
cursor: help;
color: #7D7D7D;
margin-right: 8px;
}
.c14 {
margin: 16px 2px 24px 2px;
}
.c1 {
margin-top: 16px;
}
<div
class="c0 c1"
>
<div
class="c2"
>
<div
class="c3 c4"
>
<div
class="c5"
>
<div
class="c6 css-1urox24"
>
<div
class="c7"
>
<div>
<div
class="c8 c9 css-142zc9n"
cursor="help"
>
You pay
</div>
</div>
</div>
</div>
<div
class="c5"
>
<div
class="css-1geukv8"
data-testid="INPUT-amount"
>
&lt;0.00001 DEF
</div>
</div>
</div>
<div
class="c10"
style="height: 36px; width: 36px;"
>
<div
class="c11"
>
<img
alt="DEF logo"
class="c12"
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x0000000000000000000000000000000000000002/logo.png"
/>
</div>
</div>
</div>
<div
class="c3 c4"
>
<div
class="c5"
>
<div
class="c6 css-1urox24"
>
<div
class="c7"
>
<div>
<div
class="c8 c9 css-142zc9n"
cursor="help"
>
You receive
</div>
</div>
</div>
</div>
<div
class="c5"
>
<div
class="c6 css-1geukv8"
data-testid="OUTPUT-amount" data-testid="OUTPUT-amount"
> >
&lt;0.00001 DEF &lt;0.00001 DEF
@ -624,7 +858,7 @@ exports[`SwapModalHeader.tsx test trade exact output, no recipient 1`] = `
class="c5" class="c5"
> >
<div <div
class="c8 css-1geukv8" class="css-1geukv8"
data-testid="INPUT-amount" data-testid="INPUT-amount"
> >
&lt;0.00001 ABC &lt;0.00001 ABC
@ -672,7 +906,7 @@ exports[`SwapModalHeader.tsx test trade exact output, no recipient 1`] = `
class="c5" class="c5"
> >
<div <div
class="c8 css-1geukv8" class="css-1geukv8"
data-testid="OUTPUT-amount" data-testid="OUTPUT-amount"
> >
&lt;0.00001 GHI &lt;0.00001 GHI

@ -0,0 +1,9 @@
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useQuickRouteMainnetFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.quickRouteMainnet)
}
export function useQuickRouteMainnetEnabled(): boolean {
return useQuickRouteMainnetFlag() === BaseVariant.Enabled
}

@ -20,6 +20,7 @@ export enum FeatureFlag {
infoPoolPage = 'info_pool_page', infoPoolPage = 'info_pool_page',
infoLiveViews = 'info_live_views', infoLiveViews = 'info_live_views',
uniswapXDefaultEnabled = 'uniswapx_default_enabled', uniswapXDefaultEnabled = 'uniswapx_default_enabled',
quickRouteMainnet = 'enable_quick_route_mainnet',
} }
interface FeatureFlagsContextType { interface FeatureFlagsContextType {

@ -2,6 +2,7 @@ import { renderHook } from '@testing-library/react'
import { CurrencyAmount, TradeType } from '@uniswap/sdk-core' import { CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { DAI, USDC_MAINNET } from 'constants/tokens' import { DAI, USDC_MAINNET } from 'constants/tokens'
import { RouterPreference, TradeState } from 'state/routing/types' import { RouterPreference, TradeState } from 'state/routing/types'
import { usePreviewTrade } from 'state/routing/usePreviewTrade'
import { useRouterPreference } from 'state/user/hooks' import { useRouterPreference } from 'state/user/hooks'
import { mocked } from 'test-utils/mocked' import { mocked } from 'test-utils/mocked'
@ -18,11 +19,13 @@ jest.mock('./useAutoRouterSupported')
jest.mock('./useDebounce') jest.mock('./useDebounce')
jest.mock('./useIsWindowVisible') jest.mock('./useIsWindowVisible')
jest.mock('state/routing/useRoutingAPITrade') jest.mock('state/routing/useRoutingAPITrade')
jest.mock('state/routing/usePreviewTrade')
jest.mock('state/user/hooks') jest.mock('state/user/hooks')
// helpers to set mock expectations // helpers to set mock expectations
const expectRouterMock = (state: TradeState) => { const expectRouterMock = (state: TradeState) => {
mocked(useRoutingAPITrade).mockReturnValue({ state, trade: undefined }) mocked(useRoutingAPITrade).mockReturnValue({ state, trade: undefined })
mocked(usePreviewTrade).mockReturnValue({ state, trade: undefined })
} }
beforeEach(() => { beforeEach(() => {
@ -42,11 +45,11 @@ describe('#useBestV3Trade ExactIn', () => {
const { result } = renderHook(() => useDebouncedTrade(TradeType.EXACT_INPUT, USDCAmount, DAI)) const { result } = renderHook(() => useDebouncedTrade(TradeType.EXACT_INPUT, USDCAmount, DAI))
expect(useRoutingAPITrade).toHaveBeenCalledWith( expect(useRoutingAPITrade).toHaveBeenCalledWith(
/* skipFetch = */ true,
TradeType.EXACT_INPUT, TradeType.EXACT_INPUT,
USDCAmount, USDCAmount,
DAI, DAI,
RouterPreference.CLIENT, RouterPreference.CLIENT,
/* skipFetch = */ true,
/* account = */ undefined, /* account = */ undefined,
/* inputTax = */ undefined, /* inputTax = */ undefined,
/* outputTax = */ undefined /* outputTax = */ undefined
@ -62,11 +65,11 @@ describe('#useDebouncedTrade ExactOut', () => {
const { result } = renderHook(() => useDebouncedTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET)) const { result } = renderHook(() => useDebouncedTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
expect(useRoutingAPITrade).toHaveBeenCalledWith( expect(useRoutingAPITrade).toHaveBeenCalledWith(
/* skipFetch = */ true,
TradeType.EXACT_OUTPUT, TradeType.EXACT_OUTPUT,
DAIAmount, DAIAmount,
USDC_MAINNET, USDC_MAINNET,
RouterPreference.CLIENT, RouterPreference.CLIENT,
/* skipFetch = */ true,
/* account = */ undefined, /* account = */ undefined,
/* inputTax = */ undefined, /* inputTax = */ undefined,
/* outputTax = */ undefined /* outputTax = */ undefined

@ -4,6 +4,7 @@ import { WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
import { DebounceSwapQuoteVariant, useDebounceSwapQuoteFlag } from 'featureFlags/flags/debounceSwapQuote' import { DebounceSwapQuoteVariant, useDebounceSwapQuoteFlag } from 'featureFlags/flags/debounceSwapQuote'
import { useMemo } from 'react' import { useMemo } from 'react'
import { ClassicTrade, InterfaceTrade, QuoteMethod, RouterPreference, TradeState } from 'state/routing/types' import { ClassicTrade, InterfaceTrade, QuoteMethod, RouterPreference, TradeState } from 'state/routing/types'
import { usePreviewTrade } from 'state/routing/usePreviewTrade'
import { useRoutingAPITrade } from 'state/routing/useRoutingAPITrade' import { useRoutingAPITrade } from 'state/routing/useRoutingAPITrade'
import { useRouterPreference } from 'state/user/hooks' import { useRouterPreference } from 'state/user/hooks'
@ -13,6 +14,7 @@ import useIsWindowVisible from './useIsWindowVisible'
// Prevents excessive quote requests between keystrokes. // Prevents excessive quote requests between keystrokes.
const DEBOUNCE_TIME = 350 const DEBOUNCE_TIME = 350
const DEBOUNCE_TIME_QUICKROUTE = 50
// Temporary until we remove the feature flag. // Temporary until we remove the feature flag.
const DEBOUNCE_TIME_INCREASED = 650 const DEBOUNCE_TIME_INCREASED = 650
@ -79,26 +81,44 @@ export function useDebouncedTrade(
const isDebouncing = const isDebouncing =
useDebounce(inputs, debouncedSwapQuoteFlagEnabled ? DEBOUNCE_TIME_INCREASED : DEBOUNCE_TIME) !== inputs useDebounce(inputs, debouncedSwapQuoteFlagEnabled ? DEBOUNCE_TIME_INCREASED : DEBOUNCE_TIME) !== inputs
const isPreviewTradeDebouncing = useDebounce(inputs, DEBOUNCE_TIME_QUICKROUTE) !== inputs
const isWrap = useMemo(() => { const isWrap = useMemo(() => {
if (!chainId || !amountSpecified || !otherCurrency) return false if (!chainId || !amountSpecified || !otherCurrency) return false
const weth = WRAPPED_NATIVE_CURRENCY[chainId] const weth = WRAPPED_NATIVE_CURRENCY[chainId]
return ( return Boolean(
(amountSpecified.currency.isNative && weth?.equals(otherCurrency)) || (amountSpecified.currency.isNative && weth?.equals(otherCurrency)) ||
(otherCurrency.isNative && weth?.equals(amountSpecified.currency)) (otherCurrency.isNative && weth?.equals(amountSpecified.currency))
) )
}, [amountSpecified, chainId, otherCurrency]) }, [amountSpecified, chainId, otherCurrency])
const skipFetch = isDebouncing || !autoRouterSupported || !isWindowVisible || isWrap
const [routerPreference] = useRouterPreference() const [routerPreference] = useRouterPreference()
return useRoutingAPITrade(
const skipBothFetches = !autoRouterSupported || !isWindowVisible || isWrap
const skipRoutingFetch = skipBothFetches || isDebouncing
const skipPreviewTradeFetch =
skipBothFetches || routerPreference === RouterPreference.CLIENT || isPreviewTradeDebouncing
const previewTradeResult = usePreviewTrade(
skipPreviewTradeFetch,
tradeType,
amountSpecified,
otherCurrency,
inputTax,
outputTax
)
const routingApiTradeResult = useRoutingAPITrade(
skipRoutingFetch,
tradeType, tradeType,
amountSpecified, amountSpecified,
otherCurrency, otherCurrency,
routerPreferenceOverride ?? routerPreference, routerPreferenceOverride ?? routerPreference,
skipFetch,
account, account,
inputTax, inputTax,
outputTax outputTax
) )
return previewTradeResult.currentTrade && !routingApiTradeResult.currentTrade
? previewTradeResult
: routingApiTradeResult
} }

@ -107,7 +107,7 @@ export default function usePermit2Allowance(
const shouldRequestApproval = !(isApproved || isApprovalLoading) const shouldRequestApproval = !(isApproved || isApprovalLoading)
// UniswapX trades do not need a permit signature step in between because the swap step _is_ the permit signature // UniswapX trades do not need a permit signature step in between because the swap step _is_ the permit signature
const shouldRequestSignature = tradeFillType !== TradeFillType.UniswapX && !(isPermitted || isSigned) const shouldRequestSignature = tradeFillType === TradeFillType.Classic && !(isPermitted || isSigned)
const addTransaction = useTransactionAdder() const addTransaction = useTransactionAdder()
const approveAndPermit = useCallback(async () => { const approveAndPermit = useCallback(async () => {

@ -36,7 +36,13 @@ export default function useStablecoinPrice(currency?: Currency): Price<Currency,
const amountOut = chainId ? STABLECOIN_AMOUNT_OUT[chainId] : undefined const amountOut = chainId ? STABLECOIN_AMOUNT_OUT[chainId] : undefined
const stablecoin = amountOut?.currency const stablecoin = amountOut?.currency
const { trade } = useRoutingAPITrade(TradeType.EXACT_OUTPUT, amountOut, currency, INTERNAL_ROUTER_PREFERENCE_PRICE) const { trade } = useRoutingAPITrade(
false /* skip */,
TradeType.EXACT_OUTPUT,
amountOut,
currency,
INTERNAL_ROUTER_PREFERENCE_PRICE
)
const price = useMemo(() => { const price = useMemo(() => {
if (!currency || !stablecoin) { if (!currency || !stablecoin) {
return undefined return undefined

@ -29,11 +29,11 @@ function useETHPrice(currency?: Currency): {
const amountOut = isSupported ? ETH_AMOUNT_OUT[chainId] : undefined const amountOut = isSupported ? ETH_AMOUNT_OUT[chainId] : undefined
const { trade, state } = useRoutingAPITrade( const { trade, state } = useRoutingAPITrade(
!isSupported /* skip */,
TradeType.EXACT_OUTPUT, TradeType.EXACT_OUTPUT,
amountOut, amountOut,
currency, currency,
INTERNAL_ROUTER_PREFERENCE_PRICE, INTERNAL_ROUTER_PREFERENCE_PRICE
!isSupported
) )
return useMemo(() => { return useMemo(() => {

@ -1,7 +1,7 @@
import { Currency, CurrencyAmount, Percent, Price, Token } from '@uniswap/sdk-core' import { Currency, CurrencyAmount, Percent, Price, Token } from '@uniswap/sdk-core'
import { NATIVE_CHAIN_ID } from 'constants/tokens' import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { InterfaceTrade, QuoteMethod } from 'state/routing/types' import { InterfaceTrade, QuoteMethod } from 'state/routing/types'
import { isClassicTrade, isUniswapXTrade } from 'state/routing/utils' import { isClassicTrade, isSubmittableTrade, isUniswapXTrade } from 'state/routing/utils'
import { computeRealizedPriceImpact } from 'utils/prices' import { computeRealizedPriceImpact } from 'utils/prices'
export const getDurationUntilTimestampSeconds = (futureTimestampInSecondsSinceEpoch?: number): number | undefined => { export const getDurationUntilTimestampSeconds = (futureTimestampInSecondsSinceEpoch?: number): number | undefined => {
@ -29,12 +29,18 @@ export const getPriceUpdateBasisPoints = (
return formatPercentInBasisPointsNumber(changePercentage) return formatPercentInBasisPointsNumber(changePercentage)
} }
function getEstimatedNetworkFee(trade: InterfaceTrade) {
if (isClassicTrade(trade)) return trade.gasUseEstimateUSD
if (isUniswapXTrade(trade)) return trade.classicGasUseEstimateUSD
return undefined
}
export function formatCommonPropertiesForTrade(trade: InterfaceTrade, allowedSlippage: Percent) { export function formatCommonPropertiesForTrade(trade: InterfaceTrade, allowedSlippage: Percent) {
return { return {
routing: trade.fillType, routing: trade.fillType,
type: trade.tradeType, type: trade.tradeType,
ura_quote_id: isUniswapXTrade(trade) ? trade.quoteId : undefined, ura_quote_id: isUniswapXTrade(trade) ? trade.quoteId : undefined,
ura_request_id: trade.requestId, ura_request_id: isSubmittableTrade(trade) ? trade.requestId : undefined,
ura_quote_block_number: isClassicTrade(trade) ? trade.blockNumber : undefined, ura_quote_block_number: isClassicTrade(trade) ? trade.blockNumber : undefined,
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),
@ -49,7 +55,7 @@ export function formatCommonPropertiesForTrade(trade: InterfaceTrade, allowedSli
trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId
? trade.inputAmount.currency.chainId ? trade.inputAmount.currency.chainId
: undefined, : undefined,
estimated_network_fee_usd: isClassicTrade(trade) ? trade.gasUseEstimateUSD : trade.classicGasUseEstimateUSD, estimated_network_fee_usd: getEstimatedNetworkFee(trade),
minimum_output_after_slippage: trade.minimumAmountOut(allowedSlippage).toSignificant(6), minimum_output_after_slippage: trade.minimumAmountOut(allowedSlippage).toSignificant(6),
allowed_slippage: formatPercentNumber(allowedSlippage), allowed_slippage: formatPercentNumber(allowedSlippage),
method: getQuoteMethod(trade), method: getQuoteMethod(trade),

@ -51,7 +51,7 @@ import { useLocation, useNavigate } from 'react-router-dom'
import { Text } from 'rebass' import { Text } from 'rebass'
import { useAppSelector } from 'state/hooks' import { useAppSelector } from 'state/hooks'
import { InterfaceTrade, TradeState } from 'state/routing/types' import { InterfaceTrade, TradeState } from 'state/routing/types'
import { isClassicTrade, isUniswapXTrade } from 'state/routing/utils' import { isClassicTrade, isPreviewTrade } from 'state/routing/utils'
import { Field, forceExactInput, replaceSwapState } from 'state/swap/actions' import { Field, forceExactInput, replaceSwapState } from 'state/swap/actions'
import { useDefaultsFromURLSearch, useDerivedSwapInfo, useSwapActionHandlers } from 'state/swap/hooks' import { useDefaultsFromURLSearch, useDerivedSwapInfo, useSwapActionHandlers } from 'state/swap/hooks'
import swapReducer, { initialState as initialSwapState, SwapState } from 'state/swap/reducer' import swapReducer, { initialState as initialSwapState, SwapState } from 'state/swap/reducer'
@ -118,12 +118,16 @@ const OutputSwapSection = styled(SwapSection)`
border-bottom: ${({ theme }) => `1px solid ${theme.surface1}`}; border-bottom: ${({ theme }) => `1px solid ${theme.surface1}`};
` `
function getIsValidSwapQuote( function getIsReviewableQuote(
trade: InterfaceTrade | undefined, trade: InterfaceTrade | undefined,
tradeState: TradeState, tradeState: TradeState,
swapInputError?: ReactNode swapInputError?: ReactNode
): boolean { ): boolean {
return Boolean(!swapInputError && trade && tradeState === TradeState.VALID) if (swapInputError) return false
// if the current quote is a preview quote, allow the user to progress to the Swap review screen
if (isPreviewTrade(trade)) return true
return Boolean(trade && tradeState === TradeState.VALID)
} }
function largerPercentValue(a?: Percent, b?: Percent) { function largerPercentValue(a?: Percent, b?: Percent) {
@ -518,7 +522,7 @@ export function Swap({
// warnings on the greater of fiat value price impact and execution price impact // warnings on the greater of fiat value price impact and execution price impact
const { priceImpactSeverity, largerPriceImpact } = useMemo(() => { const { priceImpactSeverity, largerPriceImpact } = useMemo(() => {
if (isUniswapXTrade(trade)) { if (!isClassicTrade(trade)) {
return { priceImpactSeverity: 0, largerPriceImpact: undefined } return { priceImpactSeverity: 0, largerPriceImpact: undefined }
} }
@ -607,7 +611,7 @@ export function Swap({
showCancel={true} showCancel={true}
/> />
<SwapHeader trade={trade} autoSlippage={autoSlippage} chainId={chainId} /> <SwapHeader trade={trade} autoSlippage={autoSlippage} chainId={chainId} />
{trade && showConfirm && allowance.state !== AllowanceState.LOADING && ( {trade && showConfirm && (
<ConfirmSwapModal <ConfirmSwapModal
trade={trade} trade={trade}
inputCurrency={inputCurrency} inputCurrency={inputCurrency}
@ -744,7 +748,7 @@ export function Swap({
<TraceEvent <TraceEvent
events={[BrowserEvent.onClick]} events={[BrowserEvent.onClick]}
name={InterfaceEventName.CONNECT_WALLET_BUTTON_CLICKED} name={InterfaceEventName.CONNECT_WALLET_BUTTON_CLICKED}
properties={{ received_swap_quote: getIsValidSwapQuote(trade, tradeState, swapInputError) }} properties={{ received_swap_quote: getIsReviewableQuote(trade, tradeState, swapInputError) }}
element={InterfaceElementName.CONNECT_WALLET_BUTTON} element={InterfaceElementName.CONNECT_WALLET_BUTTON}
> >
<ButtonLight onClick={toggleWalletDrawer} fontWeight={535} $borderRadius="16px"> <ButtonLight onClick={toggleWalletDrawer} fontWeight={535} $borderRadius="16px">
@ -803,7 +807,7 @@ export function Swap({
}} }}
id="swap-button" id="swap-button"
data-testid="swap-button" data-testid="swap-button"
disabled={!getIsValidSwapQuote(trade, tradeState, swapInputError)} disabled={!getIsReviewableQuote(trade, tradeState, swapInputError)}
error={!swapInputError && priceImpactSeverity > 2 && allowance.state === AllowanceState.ALLOWED} error={!swapInputError && priceImpactSeverity > 2 && allowance.state === AllowanceState.ALLOWED}
> >
<Text fontSize={20}> <Text fontSize={20}>

@ -94,6 +94,20 @@ jest.mock('state/routing/slice', () => {
} }
}) })
jest.mock('state/routing/quickRouteSlice', () => {
const quickRouteSlice = jest.requireActual('state/routing/quickRouteSlice')
return {
...quickRouteSlice,
// Prevents unit tests from logging errors from failed getQuote queries
useGetQuickRouteQuery: () => ({
isError: false,
data: undefined,
error: undefined,
currentData: undefined,
}),
}
})
// Mocks are configured to reset between tests (by CRA), so they must be set in a beforeEach. // Mocks are configured to reset between tests (by CRA), so they must be set in a beforeEach.
beforeEach(() => { beforeEach(() => {
// Mock window.getComputedStyle, because it is otherwise too computationally expensive to unit test. // Mock window.getComputedStyle, because it is otherwise too computationally expensive to unit test.

@ -5,6 +5,7 @@ import { persistStore } from 'redux-persist'
import { updateVersion } from './global/actions' import { updateVersion } from './global/actions'
import { sentryEnhancer } from './logging' import { sentryEnhancer } from './logging'
import reducer from './reducer' import reducer from './reducer'
import { quickRouteApi } from './routing/quickRouteSlice'
import { routingApi } from './routing/slice' import { routingApi } from './routing/slice'
export function createDefaultStore() { export function createDefaultStore() {
@ -18,7 +19,7 @@ export function createDefaultStore() {
// meta.arg and meta.baseQueryMeta are defaults. payload.trade is a nonserializable return value, but that's ok // 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) // because we are not adding it into any persisted store that requires serialization (e.g. localStorage)
ignoredActionPaths: ['meta.arg', 'meta.baseQueryMeta', 'payload.trade'], ignoredActionPaths: ['meta.arg', 'meta.baseQueryMeta', 'payload.trade'],
ignoredPaths: [routingApi.reducerPath], ignoredPaths: [routingApi.reducerPath, quickRouteApi.reducerPath],
ignoredActions: [ ignoredActions: [
// ignore the redux-persist actions // ignore the redux-persist actions
'persist/PERSIST', 'persist/PERSIST',
@ -27,7 +28,9 @@ export function createDefaultStore() {
'persist/FLUSH', 'persist/FLUSH',
], ],
}, },
}).concat(routingApi.middleware), })
.concat(routingApi.middleware)
.concat(quickRouteApi.middleware),
}) })
} }

@ -12,6 +12,7 @@ import logs from './logs/slice'
import { customCreateMigrate, migrations } from './migrations' import { customCreateMigrate, migrations } from './migrations'
import mint from './mint/reducer' import mint from './mint/reducer'
import mintV3 from './mint/v3/reducer' import mintV3 from './mint/v3/reducer'
import { quickRouteApi } from './routing/quickRouteSlice'
import { routingApi } from './routing/slice' import { routingApi } from './routing/slice'
import signatures from './signatures/reducer' import signatures from './signatures/reducer'
import transactions from './transactions/reducer' import transactions from './transactions/reducer'
@ -35,6 +36,7 @@ const appReducer = combineReducers({
multicall: multicall.reducer, multicall: multicall.reducer,
logs, logs,
[routingApi.reducerPath]: routingApi.reducer, [routingApi.reducerPath]: routingApi.reducer,
[quickRouteApi.reducerPath]: quickRouteApi.reducer,
...persistedReducers, ...persistedReducers,
}) })

@ -18,6 +18,7 @@ import { MintState } from './mint/reducer'
import { Field as FieldV3 } from './mint/v3/actions' import { Field as FieldV3 } from './mint/v3/actions'
import { FullRange, MintState as MintV3State } from './mint/v3/reducer' import { FullRange, MintState as MintV3State } from './mint/v3/reducer'
import { AppState } from './reducer' import { AppState } from './reducer'
import { quickRouteApi } from './routing/quickRouteSlice'
import { routingApi } from './routing/slice' import { routingApi } from './routing/slice'
import { RouterPreference } from './routing/types' import { RouterPreference } from './routing/types'
import { SignatureState } from './signatures/reducer' import { SignatureState } from './signatures/reducer'
@ -61,6 +62,7 @@ type ExpectedAppState = CombinedState<{
multicall: ReturnType<typeof multicall.reducer> multicall: ReturnType<typeof multicall.reducer>
logs: LogsState logs: LogsState
[routingApi.reducerPath]: ReturnType<typeof routingApi.reducer> [routingApi.reducerPath]: ReturnType<typeof routingApi.reducer>
[quickRouteApi.reducerPath]: ReturnType<typeof quickRouteApi.reducer>
}> }>
assert<Equals<AppState, ExpectedAppState>>() assert<Equals<AppState, ExpectedAppState>>()

@ -0,0 +1,112 @@
import { createApi, fetchBaseQuery, FetchBaseQueryError } from '@reduxjs/toolkit/query/react'
import { sendAnalyticsEvent } from 'analytics'
import ms from 'ms'
import { logSwapQuoteRequest } from 'tracing/swapFlowLoggers'
import { trace } from 'tracing/trace'
import { GetQuickQuoteArgs, PreviewTradeResult, QuickRouteResponse, QuoteState, RouterPreference } from './types'
import { isExactInput, transformQuickRouteToTrade } from './utils'
const UNISWAP_API_URL = process.env.REACT_APP_UNISWAP_API_URL
if (UNISWAP_API_URL === undefined) {
throw new Error(`UNISWAP_API_URL must be a defined environment variable`)
}
function getQuoteLatencyMeasure(mark: PerformanceMark): PerformanceMeasure {
performance.mark('quickroute-fetch-end')
return performance.measure('quickroute-fetch-latency', mark.name, 'quickroute-fetch-end')
}
export const quickRouteApi = createApi({
reducerPath: 'quickRouteApi',
baseQuery: fetchBaseQuery({
baseUrl: UNISWAP_API_URL,
}),
endpoints: (build) => ({
getQuickRoute: build.query<PreviewTradeResult, GetQuickQuoteArgs>({
async onQueryStarted(args: GetQuickQuoteArgs, { queryFulfilled }) {
trace(
'quickroute',
async ({ setTraceError, setTraceStatus }) => {
try {
await queryFulfilled
} catch (error: unknown) {
if (error && typeof error === 'object' && 'error' in error) {
const queryError = (error as Record<'error', FetchBaseQueryError>).error
if (typeof queryError.status === 'number') {
setTraceStatus(queryError.status)
}
setTraceError(queryError)
} else {
throw error
}
}
},
{
data: {
...args,
},
}
)
},
async queryFn(args, _api, _extraOptions, fetch) {
logSwapQuoteRequest(args.tokenInChainId, RouterPreference.API, true)
const quoteStartMark = performance.mark(`quickroute-fetch-start-${Date.now()}`)
const { tokenInAddress, tokenInChainId, tokenOutAddress, tokenOutChainId, amount, tradeType } = args
const type = isExactInput(tradeType) ? 'EXACT_IN' : 'EXACT_OUT'
const requestBody = {
tokenInChainId,
tokenInAddress,
tokenOutChainId,
tokenOutAddress,
amount,
tradeType: type,
}
const response = await fetch({
method: 'GET',
url: '/quickroute',
params: requestBody,
})
if (response.error) {
// 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 { errorCode?: string; detail?: string }
// NO_ROUTE should be treated as a valid response to prevent retries.
if (
typeof errorData === 'object' &&
(errorData?.errorCode === 'NO_ROUTE' || errorData?.detail === 'No quotes available')
) {
sendAnalyticsEvent('No quote received from quickroute API', {
requestBody,
response,
})
return {
data: { state: QuoteState.NOT_FOUND, latencyMs: getQuoteLatencyMeasure(quoteStartMark).duration },
}
} else {
return { error: response.error }
}
}
const quickRouteResponse = response.data as QuickRouteResponse
const previewTrade = transformQuickRouteToTrade(args, quickRouteResponse)
return {
data: {
state: QuoteState.SUCCESS,
trade: previewTrade,
latencyMs: getQuoteLatencyMeasure(quoteStartMark).duration,
},
}
},
keepUnusedDataFor: ms(`10s`),
extraOptions: {
maxRetries: 0,
},
}),
}),
})
export const { useGetQuickRouteQuery } = quickRouteApi
export const useGetQuickRouteQueryState = quickRouteApi.endpoints.getQuickRoute.useQueryState

@ -122,7 +122,7 @@ export const routingApi = createApi({
}, },
async queryFn(args, _api, _extraOptions, fetch) { async queryFn(args, _api, _extraOptions, fetch) {
let fellBack = false let fellBack = false
logSwapQuoteRequest(args.tokenInChainId, args.routerPreference) logSwapQuoteRequest(args.tokenInChainId, args.routerPreference, false)
const quoteStartMark = performance.mark(`quote-fetch-start-${Date.now()}`) const quoteStartMark = performance.mark(`quote-fetch-start-${Date.now()}`)
if (shouldUseAPIRouter(args)) { if (shouldUseAPIRouter(args)) {
fellBack = true fellBack = true
@ -150,7 +150,7 @@ export const routingApi = createApi({
if (response.error) { if (response.error) {
try { try {
// cast as any here because we do a runtime check on it being an object before indexing into .errorCode // 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 const errorData = response.error.data as { errorCode?: string; detail?: string }
// NO_ROUTE should be treated as a valid response to prevent retries. // NO_ROUTE should be treated as a valid response to prevent retries.
if ( if (
typeof errorData === 'object' && typeof errorData === 'object' &&

@ -1,19 +1,20 @@
import { MixedRouteSDK, ONE, Protocol, Trade } from '@uniswap/router-sdk' import { MixedRouteSDK, ONE, Protocol, Trade } from '@uniswap/router-sdk'
import { ChainId, Currency, CurrencyAmount, Fraction, Percent, Token, TradeType } from '@uniswap/sdk-core' import { ChainId, Currency, CurrencyAmount, Fraction, Percent, Price, Token, TradeType } from '@uniswap/sdk-core'
import { DutchOrderInfo, DutchOrderInfoJSON, DutchOrderTrade as IDutchOrderTrade } from '@uniswap/uniswapx-sdk' import { DutchOrderInfo, DutchOrderInfoJSON, DutchOrderTrade as IDutchOrderTrade } from '@uniswap/uniswapx-sdk'
import { Route as V2Route } from '@uniswap/v2-sdk' import { Route as V2Route } from '@uniswap/v2-sdk'
import { Route as V3Route } from '@uniswap/v3-sdk' import { Route as V3Route } from '@uniswap/v3-sdk'
export enum TradeState { export enum TradeState {
LOADING, LOADING = 'loading',
INVALID, INVALID = 'invalid',
STALE, STALE = 'stale',
NO_ROUTE_FOUND, NO_ROUTE_FOUND = 'no_route_found',
VALID, VALID = 'valid',
} }
export enum QuoteMethod { export enum QuoteMethod {
ROUTING_API = 'ROUTING_API', ROUTING_API = 'ROUTING_API',
QUICK_ROUTE = 'QUICK_ROUTE',
CLIENT_SIDE = 'CLIENT_SIDE', CLIENT_SIDE = 'CLIENT_SIDE',
CLIENT_SIDE_FALLBACK = 'CLIENT_SIDE_FALLBACK', // If client-side was used after the routing-api call failed. CLIENT_SIDE_FALLBACK = 'CLIENT_SIDE_FALLBACK', // If client-side was used after the routing-api call failed.
} }
@ -54,6 +55,20 @@ export interface GetQuoteArgs {
outputTax: Percent outputTax: Percent
} }
export type GetQuickQuoteArgs = {
amount: string
tokenInAddress: string
tokenInChainId: ChainId
tokenInDecimals: number
tokenInSymbol?: string
tokenOutAddress: string
tokenOutChainId: ChainId
tokenOutDecimals: number
tokenOutSymbol?: string
tradeType: TradeType
inputTax: Percent
outputTax: Percent
}
// 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
type TokenInRoute = Pick<Token, 'address' | 'chainId' | 'symbol' | 'decimals'> type TokenInRoute = Pick<Token, 'address' | 'chainId' | 'symbol' | 'decimals'>
@ -132,6 +147,26 @@ type URAClassicQuoteResponse = {
} }
export type URAQuoteResponse = URAClassicQuoteResponse | URADutchOrderQuoteResponse export type URAQuoteResponse = URAClassicQuoteResponse | URADutchOrderQuoteResponse
export type QuickRouteResponse = {
tokenIn: {
address: string
decimals: number
symbol: string
name: string
}
tokenOut: {
address: string
decimals: number
symbol: string
name: string
}
tradeType: 'EXACT_IN' | 'EXACT_OUT'
quote: {
amount: string
path: string
}
}
export function isClassicQuoteResponse(data: URAQuoteResponse): data is URAClassicQuoteResponse { export function isClassicQuoteResponse(data: URAQuoteResponse): data is URAClassicQuoteResponse {
return data.routing === URAQuoteType.CLASSIC return data.routing === URAQuoteType.CLASSIC
} }
@ -139,6 +174,7 @@ export function isClassicQuoteResponse(data: URAQuoteResponse): data is URAClass
export enum TradeFillType { export enum TradeFillType {
Classic = 'classic', // Uniswap V1, V2, and V3 trades with on-chain routes Classic = 'classic', // Uniswap V1, V2, and V3 trades with on-chain routes
UniswapX = 'uniswap_x', // off-chain trades, no routes UniswapX = 'uniswap_x', // off-chain trades, no routes
None = 'none', // for preview trades, cant be used for submission
} }
export type ApproveInfo = { needsApprove: true; approveGasEstimateUSD: number } | { needsApprove: false } export type ApproveInfo = { needsApprove: true; approveGasEstimateUSD: number } | { needsApprove: false }
@ -302,7 +338,97 @@ export class DutchOrderTrade extends IDutchOrderTrade<Currency, Currency, TradeT
} }
} }
export type InterfaceTrade = ClassicTrade | DutchOrderTrade export class PreviewTrade {
public readonly fillType = TradeFillType.None
public readonly quoteMethod = QuoteMethod.QUICK_ROUTE
public readonly tradeType: TradeType
public readonly inputAmount: CurrencyAmount<Currency>
public readonly outputAmount: CurrencyAmount<Currency>
inputTax: Percent
outputTax: Percent
constructor({
inputAmount,
outputAmount,
tradeType,
inputTax,
outputTax,
}: {
inputAmount: CurrencyAmount<Currency>
outputAmount: CurrencyAmount<Currency>
tradeType: TradeType
inputTax: Percent
outputTax: Percent
}) {
this.inputAmount = inputAmount
this.outputAmount = outputAmount
this.tradeType = tradeType
this.inputTax = inputTax
this.outputTax = outputTax
}
public get totalTaxRate(): Percent {
return this.inputTax.add(this.outputTax)
}
public get postTaxOutputAmount() {
// Ideally we should calculate the final output amount by ammending the inputAmount based on the input tax and then applying the output tax,
// but this isn't currently possible because V2Trade reconstructs the total inputAmount based on the swap routes
// TODO(WEB-2761): Amend V2Trade objects in the v2-sdk to have a separate field for post-input tax routes
return this.outputAmount.multiply(new Fraction(ONE).subtract(this.totalTaxRate))
}
// below methods are copied from router-sdk
// Trade https://github.com/Uniswap/router-sdk/blob/main/src/entities/trade.ts#L10
public minimumAmountOut(slippageTolerance: Percent, amountOut = this.outputAmount): CurrencyAmount<Currency> {
if (this.tradeType === TradeType.EXACT_OUTPUT) {
return amountOut
} else {
const slippageAdjustedAmountOut = new Fraction(ONE)
.add(slippageTolerance)
.invert()
.multiply(amountOut.quotient).quotient
return CurrencyAmount.fromRawAmount(amountOut.currency, slippageAdjustedAmountOut)
}
}
public maximumAmountIn(slippageTolerance: Percent, amountIn = this.inputAmount): CurrencyAmount<Currency> {
if (this.tradeType === TradeType.EXACT_INPUT) {
return amountIn
} else {
const slippageAdjustedAmountIn = new Fraction(ONE).add(slippageTolerance).multiply(amountIn.quotient).quotient
return CurrencyAmount.fromRawAmount(amountIn.currency, slippageAdjustedAmountIn)
}
}
private _executionPrice: Price<Currency, Currency> | undefined
/**
* The price expressed in terms of output amount/input amount.
*/
public get executionPrice(): Price<Currency, Currency> {
return (
this._executionPrice ??
(this._executionPrice = new Price(
this.inputAmount.currency,
this.outputAmount.currency,
this.inputAmount.quotient,
this.outputAmount.quotient
))
)
}
public worstExecutionPrice(slippageTolerance: Percent): Price<Currency, Currency> {
return new Price(
this.inputAmount.currency,
this.outputAmount.currency,
this.maximumAmountIn(slippageTolerance).quotient,
this.minimumAmountOut(slippageTolerance).quotient
)
}
}
export type SubmittableTrade = ClassicTrade | DutchOrderTrade
export type InterfaceTrade = SubmittableTrade | PreviewTrade
export enum QuoteState { export enum QuoteState {
SUCCESS = 'Success', SUCCESS = 'Success',
@ -327,7 +453,19 @@ export type TradeResult =
} }
| { | {
state: QuoteState.SUCCESS state: QuoteState.SUCCESS
trade: InterfaceTrade trade: SubmittableTrade
latencyMs?: number
}
export type PreviewTradeResult =
| {
state: QuoteState.NOT_FOUND
trade?: undefined
latencyMs?: number
}
| {
state: QuoteState.SUCCESS
trade: PreviewTrade
latencyMs?: number latencyMs?: number
} }

@ -0,0 +1,128 @@
import { skipToken } from '@reduxjs/toolkit/query/react'
import { ChainId, Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core'
import { ZERO_PERCENT } from 'constants/misc'
import { useQuickRouteMainnetEnabled } from 'featureFlags/flags/quickRouteMainnet'
import { useMemo } from 'react'
import { useGetQuickRouteQuery, useGetQuickRouteQueryState } from './quickRouteSlice'
import { GetQuickQuoteArgs, PreviewTrade, QuoteState, TradeState } from './types'
import { currencyAddressForSwapQuote } from './utils'
const TRADE_NOT_FOUND = { state: TradeState.NO_ROUTE_FOUND, trade: undefined } as const
const TRADE_LOADING = { state: TradeState.LOADING, trade: undefined } as const
function useQuickRouteArguments({
tokenIn,
tokenOut,
amount,
tradeType,
inputTax,
outputTax,
}: {
tokenIn?: Currency
tokenOut?: Currency
amount?: CurrencyAmount<Currency>
tradeType: TradeType
inputTax: Percent
outputTax: Percent
}): GetQuickQuoteArgs | typeof skipToken {
const enabledMainnet = useQuickRouteMainnetEnabled()
return useMemo(() => {
if (!tokenIn || !tokenOut || !amount) return skipToken
if (!enabledMainnet || tokenIn.chainId !== ChainId.MAINNET) return skipToken
return {
amount: amount.quotient.toString(),
tokenInAddress: currencyAddressForSwapQuote(tokenIn),
tokenInChainId: tokenIn.chainId,
tokenInDecimals: tokenIn.wrapped.decimals,
tokenInSymbol: tokenIn.wrapped.symbol,
tokenOutAddress: currencyAddressForSwapQuote(tokenOut),
tokenOutChainId: tokenOut.wrapped.chainId,
tokenOutDecimals: tokenOut.wrapped.decimals,
tokenOutSymbol: tokenOut.wrapped.symbol,
tradeType,
inputTax,
outputTax,
}
}, [amount, enabledMainnet, inputTax, outputTax, tokenIn, tokenOut, tradeType])
}
export function usePreviewTrade(
skipFetch = false,
tradeType: TradeType,
amountSpecified: CurrencyAmount<Currency> | undefined,
otherCurrency: Currency | undefined,
inputTax = ZERO_PERCENT,
outputTax = ZERO_PERCENT
): {
state: TradeState
trade?: PreviewTrade
currentTrade?: PreviewTrade
swapQuoteLatency?: number
} {
const [currencyIn, currencyOut]: [Currency | undefined, Currency | undefined] = useMemo(
() =>
tradeType === TradeType.EXACT_INPUT
? [amountSpecified?.currency, otherCurrency]
: [otherCurrency, amountSpecified?.currency],
[amountSpecified, otherCurrency, tradeType]
)
const queryArgs = useQuickRouteArguments({
tokenIn: currencyIn,
tokenOut: currencyOut,
amount: amountSpecified,
tradeType,
inputTax,
outputTax,
})
const { isError, data: tradeResult, error, currentData } = useGetQuickRouteQueryState(queryArgs)
useGetQuickRouteQuery(skipFetch ? skipToken : queryArgs, {
// If latest quote from cache was fetched > 2m ago, instantly repoll for another instead of waiting for next poll period
refetchOnMountOrArgChange: 2 * 60,
})
const isFetching = currentData !== tradeResult || !currentData
return useMemo(() => {
if (amountSpecified && queryArgs === skipToken) {
return {
state: TradeState.STALE,
trade: tradeResult?.trade,
currentTrade: currentData?.trade,
swapQuoteLatency: tradeResult?.latencyMs,
}
} else if (!amountSpecified || isError || queryArgs === skipToken) {
return {
state: TradeState.INVALID,
trade: undefined,
currentTrade: currentData?.trade,
error: JSON.stringify(error),
}
} else if (tradeResult?.state === QuoteState.NOT_FOUND && !isFetching) {
return TRADE_NOT_FOUND
} else if (!tradeResult?.trade) {
return TRADE_LOADING
} else {
return {
state: isFetching ? TradeState.LOADING : TradeState.VALID,
trade: tradeResult.trade,
currentTrade: currentData?.trade,
swapQuoteLatency: tradeResult.latencyMs,
}
}
}, [
amountSpecified,
error,
isError,
isFetching,
queryArgs,
tradeResult?.latencyMs,
tradeResult?.state,
tradeResult?.trade,
currentData?.trade,
])
}

@ -9,44 +9,46 @@ import { useMemo } from 'react'
import { useGetQuoteQuery, useGetQuoteQueryState } from './slice' import { useGetQuoteQuery, useGetQuoteQueryState } from './slice'
import { import {
ClassicTrade, ClassicTrade,
InterfaceTrade,
INTERNAL_ROUTER_PREFERENCE_PRICE, INTERNAL_ROUTER_PREFERENCE_PRICE,
QuoteMethod, QuoteMethod,
QuoteState, QuoteState,
RouterPreference, RouterPreference,
SubmittableTrade,
TradeState, TradeState,
} from './types' } from './types'
const TRADE_NOT_FOUND = { state: TradeState.NO_ROUTE_FOUND, trade: undefined } as const const TRADE_NOT_FOUND = { state: TradeState.NO_ROUTE_FOUND, trade: undefined, currentData: undefined } as const
const TRADE_LOADING = { state: TradeState.LOADING, trade: undefined } as const const TRADE_LOADING = { state: TradeState.LOADING, trade: undefined, currentData: undefined } as const
export function useRoutingAPITrade<TTradeType extends TradeType>( export function useRoutingAPITrade<TTradeType extends TradeType>(
skipFetch: boolean,
tradeType: TTradeType, tradeType: TTradeType,
amountSpecified: CurrencyAmount<Currency> | undefined, amountSpecified: CurrencyAmount<Currency> | undefined,
otherCurrency: Currency | undefined, otherCurrency: Currency | undefined,
routerPreference: typeof INTERNAL_ROUTER_PREFERENCE_PRICE, routerPreference: typeof INTERNAL_ROUTER_PREFERENCE_PRICE,
skipFetch?: boolean,
account?: string, account?: string,
inputTax?: Percent, inputTax?: Percent,
outputTax?: Percent outputTax?: Percent
): { ): {
state: TradeState state: TradeState
trade?: ClassicTrade trade?: ClassicTrade
currentTrade?: ClassicTrade
swapQuoteLatency?: number swapQuoteLatency?: number
} }
export function useRoutingAPITrade<TTradeType extends TradeType>( export function useRoutingAPITrade<TTradeType extends TradeType>(
skipFetch: boolean,
tradeType: TTradeType, tradeType: TTradeType,
amountSpecified: CurrencyAmount<Currency> | undefined, amountSpecified: CurrencyAmount<Currency> | undefined,
otherCurrency: Currency | undefined, otherCurrency: Currency | undefined,
routerPreference: RouterPreference, routerPreference: RouterPreference,
skipFetch?: boolean,
account?: string, account?: string,
inputTax?: Percent, inputTax?: Percent,
outputTax?: Percent outputTax?: Percent
): { ): {
state: TradeState state: TradeState
trade?: InterfaceTrade trade?: SubmittableTrade
currentTrade?: SubmittableTrade
swapQuoteLatency?: number swapQuoteLatency?: number
} }
@ -57,17 +59,18 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
* @param otherCurrency the desired output/payment currency * @param otherCurrency the desired output/payment currency
*/ */
export function useRoutingAPITrade<TTradeType extends TradeType>( export function useRoutingAPITrade<TTradeType extends TradeType>(
skipFetch = false,
tradeType: TTradeType, tradeType: TTradeType,
amountSpecified: CurrencyAmount<Currency> | undefined, amountSpecified: CurrencyAmount<Currency> | undefined,
otherCurrency: Currency | undefined, otherCurrency: Currency | undefined,
routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE, routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE,
skipFetch = false,
account?: string, account?: string,
inputTax = ZERO_PERCENT, inputTax = ZERO_PERCENT,
outputTax = ZERO_PERCENT outputTax = ZERO_PERCENT
): { ): {
state: TradeState state: TradeState
trade?: InterfaceTrade trade?: SubmittableTrade
currentTrade?: SubmittableTrade
method?: QuoteMethod method?: QuoteMethod
swapQuoteLatency?: number swapQuoteLatency?: number
} { } {
@ -97,6 +100,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
// If latest quote from cache was fetched > 2m ago, instantly repoll for another instead of waiting for next poll period // If latest quote from cache was fetched > 2m ago, instantly repoll for another instead of waiting for next poll period
refetchOnMountOrArgChange: 2 * 60, refetchOnMountOrArgChange: 2 * 60,
}) })
const isFetching = currentData !== tradeResult || !currentData const isFetching = currentData !== tradeResult || !currentData
return useMemo(() => { return useMemo(() => {
@ -104,12 +108,14 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
return { return {
state: TradeState.STALE, state: TradeState.STALE,
trade: tradeResult?.trade, trade: tradeResult?.trade,
currentTrade: currentData?.trade,
swapQuoteLatency: tradeResult?.latencyMs, swapQuoteLatency: tradeResult?.latencyMs,
} }
} else if (!amountSpecified || isError || queryArgs === skipToken) { } else if (!amountSpecified || isError || queryArgs === skipToken) {
return { return {
state: TradeState.INVALID, state: TradeState.INVALID,
trade: undefined, trade: undefined,
currentTrade: currentData?.trade,
error: JSON.stringify(error), error: JSON.stringify(error),
} }
} else if (tradeResult?.state === QuoteState.NOT_FOUND && !isFetching) { } else if (tradeResult?.state === QuoteState.NOT_FOUND && !isFetching) {
@ -120,6 +126,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
return { return {
state: isFetching ? TradeState.LOADING : TradeState.VALID, state: isFetching ? TradeState.LOADING : TradeState.VALID,
trade: tradeResult?.trade, trade: tradeResult?.trade,
currentTrade: currentData?.trade,
swapQuoteLatency: tradeResult?.latencyMs, swapQuoteLatency: tradeResult?.latencyMs,
} }
} }
@ -132,5 +139,6 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
tradeResult?.latencyMs, tradeResult?.latencyMs,
tradeResult?.state, tradeResult?.state,
tradeResult?.trade, tradeResult?.trade,
currentData?.trade,
]) ])
} }

@ -12,13 +12,17 @@ import {
ClassicQuoteData, ClassicQuoteData,
ClassicTrade, ClassicTrade,
DutchOrderTrade, DutchOrderTrade,
GetQuickQuoteArgs,
GetQuoteArgs, GetQuoteArgs,
InterfaceTrade, InterfaceTrade,
isClassicQuoteResponse, isClassicQuoteResponse,
PoolType, PoolType,
PreviewTrade,
QuickRouteResponse,
QuoteMethod, QuoteMethod,
QuoteState, QuoteState,
RouterPreference, RouterPreference,
SubmittableTrade,
SwapRouterNativeAssets, SwapRouterNativeAssets,
TradeFillType, TradeFillType,
TradeResult, TradeResult,
@ -114,7 +118,7 @@ function toDutchOrderInfo(orderInfoJSON: DutchOrderInfoJSON): DutchOrderInfo {
// Prepares the currencies used for the actual Swap (either UniswapX or Universal Router) // Prepares the currencies used for the actual Swap (either UniswapX or Universal Router)
// May not match `currencyIn` that the user selected because for ETH inputs in UniswapX, the actual // May not match `currencyIn` that the user selected because for ETH inputs in UniswapX, the actual
// swap will use WETH. // swap will use WETH.
function getTradeCurrencies(args: GetQuoteArgs, isUniswapXTrade: boolean): [Currency, Currency] { function getTradeCurrencies(args: GetQuoteArgs | GetQuickQuoteArgs, isUniswapXTrade: boolean): [Currency, Currency] {
const { const {
tokenInAddress, tokenInAddress,
tokenInChainId, tokenInChainId,
@ -168,6 +172,17 @@ function getClassicTradeDetails(
} }
} }
export function transformQuickRouteToTrade(args: GetQuickQuoteArgs, data: QuickRouteResponse): PreviewTrade {
const { amount, tradeType, inputTax, outputTax } = args
const [currencyIn, currencyOut] = getTradeCurrencies(args, false)
const [rawAmountIn, rawAmountOut] =
data.tradeType === 'EXACT_IN' ? [amount, data.quote.amount] : [data.quote.amount, amount]
const inputAmount = CurrencyAmount.fromRawAmount(currencyIn, rawAmountIn)
const outputAmount = CurrencyAmount.fromRawAmount(currencyOut, rawAmountOut)
return new PreviewTrade({ inputAmount, outputAmount, tradeType, inputTax, outputTax })
}
export async function transformRoutesToTrade( export async function transformRoutesToTrade(
args: GetQuoteArgs, args: GetQuoteArgs,
data: URAQuoteResponse, data: URAQuoteResponse,
@ -313,6 +328,14 @@ export function isClassicTrade(trade?: InterfaceTrade): trade is ClassicTrade {
return trade?.fillType === TradeFillType.Classic return trade?.fillType === TradeFillType.Classic
} }
export function isPreviewTrade(trade?: InterfaceTrade): trade is PreviewTrade {
return trade?.fillType === TradeFillType.None
}
export function isSubmittableTrade(trade?: InterfaceTrade): trade is SubmittableTrade {
return trade?.fillType === TradeFillType.Classic || trade?.fillType === TradeFillType.UniswapX
}
export function isUniswapXTrade(trade?: InterfaceTrade): trade is DutchOrderTrade { export function isUniswapXTrade(trade?: InterfaceTrade): trade is DutchOrderTrade {
return trade?.fillType === TradeFillType.UniswapX return trade?.fillType === TradeFillType.UniswapX
} }
@ -322,6 +345,8 @@ export function shouldUseAPIRouter(args: GetQuoteArgs): boolean {
} }
export function getTransactionCount(trade: InterfaceTrade): number { export function getTransactionCount(trade: InterfaceTrade): number {
if (!isSubmittableTrade(trade)) return 0
let count = 0 let count = 0
if (trade.approveInfo.needsApprove) { if (trade.approveInfo.needsApprove) {
count++ // approval step, which can happen in both classic and uniswapx count++ // approval step, which can happen in both classic and uniswapx

@ -7,7 +7,7 @@ import { ZERO_PERCENT } from 'constants/misc'
import { nativeOnChain } from 'constants/tokens' import { nativeOnChain } from 'constants/tokens'
import { BigNumber } from 'ethers/lib/ethers' import { BigNumber } from 'ethers/lib/ethers'
import JSBI from 'jsbi' import JSBI from 'jsbi'
import { ClassicTrade, DutchOrderTrade, QuoteMethod } from 'state/routing/types' import { ClassicTrade, DutchOrderTrade, PreviewTrade, QuoteMethod } 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')
@ -160,3 +160,11 @@ export const TEST_TRADE_FEE_ON_BUY = new ClassicTrade({
inputTax: ZERO_PERCENT, inputTax: ZERO_PERCENT,
outputTax: new Percent(3, 100), outputTax: new Percent(3, 100),
}) })
export const PREVIEW_EXACT_IN_TRADE = new PreviewTrade({
inputAmount: toCurrencyAmount(TEST_TOKEN_1, 1000),
outputAmount: toCurrencyAmount(TEST_TOKEN_2, 1000),
tradeType: TradeType.EXACT_INPUT,
inputTax: new Percent(0, 100),
outputTax: new Percent(0, 100),
})

@ -63,6 +63,7 @@ describe('swapFlowLoggers', () => {
expect(sendAnalyticsEvent).toHaveBeenCalledWith(SwapEventName.SWAP_QUOTE_FETCH, { expect(sendAnalyticsEvent).toHaveBeenCalledWith(SwapEventName.SWAP_QUOTE_FETCH, {
chainId: mockChainId, chainId: mockChainId,
isQuickRoute: false,
time_to_first_quote_request: 100, time_to_first_quote_request: 100,
time_to_first_quote_request_since_first_input: 100, time_to_first_quote_request_since_first_input: 100,
}) })
@ -75,6 +76,7 @@ describe('swapFlowLoggers', () => {
expect(sendAnalyticsEvent).toHaveBeenCalledWith(SwapEventName.SWAP_QUOTE_FETCH, { expect(sendAnalyticsEvent).toHaveBeenCalledWith(SwapEventName.SWAP_QUOTE_FETCH, {
chainId: mockChainId, chainId: mockChainId,
isQuickRoute: false,
}) })
}) })
}) })

@ -34,7 +34,8 @@ export function maybeLogFirstSwapAction(analyticsContext: ITraceContext) {
export function logSwapQuoteRequest( export function logSwapQuoteRequest(
chainId: number, chainId: number,
routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE,
isQuickRoute?: boolean
) { ) {
let performanceMetrics = {} let performanceMetrics = {}
if (routerPreference !== INTERNAL_ROUTER_PREFERENCE_PRICE) { if (routerPreference !== INTERNAL_ROUTER_PREFERENCE_PRICE) {
@ -50,6 +51,7 @@ export function logSwapQuoteRequest(
} }
sendAnalyticsEvent(SwapEventName.SWAP_QUOTE_FETCH, { sendAnalyticsEvent(SwapEventName.SWAP_QUOTE_FETCH, {
chainId, chainId,
isQuickRoute: isQuickRoute ?? false,
...performanceMetrics, ...performanceMetrics,
}) })
} }