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:
parent
e6362212c6
commit
c7a8e9e5a7
@ -8,6 +8,7 @@ import { useInfoLiveViewsFlag } from 'featureFlags/flags/infoLiveViews'
|
||||
import { useInfoPoolPageFlag } from 'featureFlags/flags/infoPoolPage'
|
||||
import { useInfoTDPFlag } from 'featureFlags/flags/infoTDP'
|
||||
import { useMultichainUXFlag } from 'featureFlags/flags/multichainUx'
|
||||
import { useQuickRouteMainnetFlag } from 'featureFlags/flags/quickRouteMainnet'
|
||||
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
|
||||
import { useUniswapXDefaultEnabledFlag } from 'featureFlags/flags/uniswapXDefault'
|
||||
import { useUniswapXEthOutputFlag } from 'featureFlags/flags/uniswapXEthOutput'
|
||||
@ -254,6 +255,14 @@ export default function FeatureFlagModal() {
|
||||
featureFlag={FeatureFlag.fotAdjustedmentsEnabled}
|
||||
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">
|
||||
<FeatureFlagOption
|
||||
variant={BaseVariant}
|
||||
|
62
src/components/Loader/SpinningLoader.tsx
Normal file
62
src/components/Loader/SpinningLoader.tsx
Normal file
@ -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 }>`
|
||||
filter: ${({ $loading }) => ($loading ? 'grayscale(1)' : 'none')};
|
||||
opacity: ${({ $loading }) => ($loading ? '0.4' : '1')};
|
||||
opacity: ${({ $loading }) => ($loading ? '0.6' : '1')};
|
||||
transition: ${({ $loading, theme }) =>
|
||||
$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 { ThemedText } from 'theme/components'
|
||||
|
||||
import UniswapXRouterLabel from './UniswapXRouterLabel'
|
||||
|
||||
export default function RouterLabel({ trade }: { trade: InterfaceTrade }) {
|
||||
export default function RouterLabel({ trade }: { trade: SubmittableTrade }) {
|
||||
if (isUniswapXTrade(trade)) {
|
||||
return (
|
||||
<UniswapXRouterLabel>
|
||||
|
@ -8,7 +8,7 @@ import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
|
||||
import { ZERO_PERCENT } from 'constants/misc'
|
||||
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
|
||||
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 { NumberType, useFormatter } from 'utils/formatNumbers'
|
||||
|
||||
@ -49,7 +49,7 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }:
|
||||
const txCount = getTransactionCount(trade)
|
||||
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 (
|
||||
<Column gap="md">
|
||||
@ -150,37 +150,39 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }:
|
||||
</TextWithLoadingPlaceholder>
|
||||
</RowBetween>
|
||||
<Separator />
|
||||
<RowBetween>
|
||||
<ThemedText.BodySmall color="neutral2">
|
||||
<Trans>Order routing</Trans>
|
||||
</ThemedText.BodySmall>
|
||||
{isClassicTrade(trade) ? (
|
||||
<MouseoverTooltip
|
||||
size={TooltipSize.Large}
|
||||
text={<SwapRoute data-testid="swap-route-info" trade={trade} syncing={syncing} />}
|
||||
onOpen={() => {
|
||||
sendAnalyticsEvent(SwapEventName.SWAP_AUTOROUTER_VISUALIZATION_EXPANDED, {
|
||||
element: InterfaceElementName.AUTOROUTER_VISUALIZATION_ROW,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<RouterLabel trade={trade} />
|
||||
</MouseoverTooltip>
|
||||
) : (
|
||||
<MouseoverTooltip
|
||||
size={TooltipSize.Small}
|
||||
text={<GasBreakdownTooltip trade={trade} hideFees />}
|
||||
placement="right"
|
||||
onOpen={() => {
|
||||
sendAnalyticsEvent(SwapEventName.SWAP_AUTOROUTER_VISUALIZATION_EXPANDED, {
|
||||
element: InterfaceElementName.AUTOROUTER_VISUALIZATION_ROW,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<RouterLabel trade={trade} />
|
||||
</MouseoverTooltip>
|
||||
)}
|
||||
</RowBetween>
|
||||
{isSubmittableTrade(trade) && (
|
||||
<RowBetween>
|
||||
<ThemedText.BodySmall color="neutral2">
|
||||
<Trans>Order routing</Trans>
|
||||
</ThemedText.BodySmall>
|
||||
{isClassicTrade(trade) ? (
|
||||
<MouseoverTooltip
|
||||
size={TooltipSize.Large}
|
||||
text={<SwapRoute data-testid="swap-route-info" trade={trade} syncing={syncing} />}
|
||||
onOpen={() => {
|
||||
sendAnalyticsEvent(SwapEventName.SWAP_AUTOROUTER_VISUALIZATION_EXPANDED, {
|
||||
element: InterfaceElementName.AUTOROUTER_VISUALIZATION_ROW,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<RouterLabel trade={trade} />
|
||||
</MouseoverTooltip>
|
||||
) : (
|
||||
<MouseoverTooltip
|
||||
size={TooltipSize.Small}
|
||||
text={<GasBreakdownTooltip trade={trade} hideFees />}
|
||||
placement="right"
|
||||
onOpen={() => {
|
||||
sendAnalyticsEvent(SwapEventName.SWAP_AUTOROUTER_VISUALIZATION_EXPANDED, {
|
||||
element: InterfaceElementName.AUTOROUTER_VISUALIZATION_ROW,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<RouterLabel trade={trade} />
|
||||
</MouseoverTooltip>
|
||||
)}
|
||||
</RowBetween>
|
||||
)}
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import useNativeCurrency from 'lib/hooks/useNativeCurrency'
|
||||
import { getPriceUpdateBasisPoints } from 'lib/utils/analytics'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { InterfaceTrade, TradeFillType } from 'state/routing/types'
|
||||
import { isPreviewTrade } from 'state/routing/utils'
|
||||
import { Field } from 'state/swap/actions'
|
||||
import { useIsTransactionConfirmed, useSwapTransactionStatus } from 'state/transactions/hooks'
|
||||
import styled from 'styled-components'
|
||||
@ -363,7 +364,8 @@ export default function ConfirmSwapModal({
|
||||
trade={trade}
|
||||
swapResult={swapResult}
|
||||
allowedSlippage={allowedSlippage}
|
||||
disabledConfirm={showAcceptChanges}
|
||||
isLoading={isPreviewTrade(trade)}
|
||||
disabledConfirm={showAcceptChanges || isPreviewTrade(trade) || allowance.state === AllowanceState.LOADING}
|
||||
fiatValueInput={fiatValueInput}
|
||||
fiatValueOutput={fiatValueOutput}
|
||||
showAcceptChanges={showAcceptChanges}
|
||||
|
@ -3,7 +3,7 @@ import { AutoColumn } from 'components/Column'
|
||||
import UniswapXRouterLabel, { UniswapXGradient } from 'components/RouterLabel/UniswapXRouterLabel'
|
||||
import Row from 'components/Row'
|
||||
import { ReactNode } from 'react'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
import { SubmittableTrade } from 'state/routing/types'
|
||||
import { isClassicTrade, isUniswapXTrade } from 'state/routing/utils'
|
||||
import styled from 'styled-components'
|
||||
import { Divider, ExternalLink, ThemedText } from 'theme/components'
|
||||
@ -56,7 +56,7 @@ export function GasBreakdownTooltip({
|
||||
hideFees = false,
|
||||
hideUniswapXDescription = false,
|
||||
}: {
|
||||
trade: InterfaceTrade
|
||||
trade: SubmittableTrade
|
||||
hideFees?: boolean
|
||||
hideUniswapXDescription?: boolean
|
||||
}) {
|
||||
|
@ -7,7 +7,7 @@ import { UniswapXRouterIcon } from 'components/RouterLabel/UniswapXRouterLabel'
|
||||
import Row, { RowFixed } from 'components/Row'
|
||||
import { MouseoverTooltip, TooltipSize } from 'components/Tooltip'
|
||||
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 styled from 'styled-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 { formatNumber } = useFormatter()
|
||||
|
||||
|
@ -10,7 +10,8 @@ import { formatCommonPropertiesForTrade } from 'lib/utils/analytics'
|
||||
import { useState } from 'react'
|
||||
import { ChevronDown } from 'react-feather'
|
||||
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 { AdvancedSwapDetails } from './AdvancedSwapDetails'
|
||||
@ -28,58 +29,6 @@ const RotatingArrow = styled(ChevronDown)<{ open?: boolean }>`
|
||||
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`
|
||||
padding-top: ${({ theme }) => theme.grids.md};
|
||||
`
|
||||
@ -121,13 +70,6 @@ export default function SwapDetailsDropdown({ trade, syncing, loading, allowedSl
|
||||
open={showDetails}
|
||||
>
|
||||
<RowFixed>
|
||||
{Boolean(loading || syncing) && (
|
||||
<StyledPolling>
|
||||
<StyledPollingDot>
|
||||
<Spinner />
|
||||
</StyledPollingDot>
|
||||
</StyledPolling>
|
||||
)}
|
||||
{trade ? (
|
||||
<LoadingOpacityContainer $loading={syncing} data-testid="trade-price-container">
|
||||
<TradePrice price={trade.executionPrice} />
|
||||
@ -139,7 +81,9 @@ export default function SwapDetailsDropdown({ trade, syncing, loading, allowedSl
|
||||
) : null}
|
||||
</RowFixed>
|
||||
<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)} />
|
||||
</RowFixed>
|
||||
</StyledHeaderRow>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {
|
||||
PREVIEW_EXACT_IN_TRADE,
|
||||
TEST_ALLOWED_SLIPPAGE,
|
||||
TEST_TOKEN_1,
|
||||
TEST_TOKEN_2,
|
||||
@ -15,6 +16,7 @@ describe('SwapModalFooter.tsx', () => {
|
||||
it('matches base snapshot, test trade exact input', () => {
|
||||
const { asFragment } = render(
|
||||
<SwapModalFooter
|
||||
isLoading={false}
|
||||
trade={TEST_TRADE_EXACT_INPUT}
|
||||
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
|
||||
swapResult={undefined}
|
||||
@ -50,6 +52,7 @@ describe('SwapModalFooter.tsx', () => {
|
||||
const mockAcceptChanges = jest.fn()
|
||||
render(
|
||||
<SwapModalFooter
|
||||
isLoading={false}
|
||||
trade={TEST_TRADE_EXACT_INPUT}
|
||||
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
|
||||
swapResult={undefined}
|
||||
@ -77,6 +80,7 @@ describe('SwapModalFooter.tsx', () => {
|
||||
it('test trade exact output, no recipient', () => {
|
||||
render(
|
||||
<SwapModalFooter
|
||||
isLoading={false}
|
||||
trade={TEST_TRADE_EXACT_OUTPUT}
|
||||
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
|
||||
swapResult={undefined}
|
||||
@ -109,6 +113,7 @@ describe('SwapModalFooter.tsx', () => {
|
||||
it('test trade fee on input token transfer', () => {
|
||||
render(
|
||||
<SwapModalFooter
|
||||
isLoading={false}
|
||||
trade={TEST_TRADE_FEE_ON_SELL}
|
||||
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
|
||||
swapResult={undefined}
|
||||
@ -138,6 +143,7 @@ describe('SwapModalFooter.tsx', () => {
|
||||
it('test trade fee on output token transfer', () => {
|
||||
render(
|
||||
<SwapModalFooter
|
||||
isLoading={false}
|
||||
trade={TEST_TRADE_FEE_ON_BUY}
|
||||
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
|
||||
swapResult={undefined}
|
||||
@ -163,4 +169,30 @@ describe('SwapModalFooter.tsx', () => {
|
||||
).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 { TraceEvent } from 'analytics'
|
||||
import Column from 'components/Column'
|
||||
import SpinningLoader from 'components/Loader/SpinningLoader'
|
||||
import { MouseoverTooltip, TooltipSize } from 'components/Tooltip'
|
||||
import { ZERO_PERCENT } from 'constants/misc'
|
||||
import { SwapResult } from 'hooks/useSwapCallback'
|
||||
@ -11,8 +12,8 @@ import useTransactionDeadline from 'hooks/useTransactionDeadline'
|
||||
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
|
||||
import { ReactNode } from 'react'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import { ClassicTrade, InterfaceTrade, RouterPreference } from 'state/routing/types'
|
||||
import { getTransactionCount, isClassicTrade } from 'state/routing/utils'
|
||||
import { ClassicTrade, InterfaceTrade, PreviewTrade, RouterPreference } from 'state/routing/types'
|
||||
import { getTransactionCount, isClassicTrade, isPreviewTrade, isSubmittableTrade } from 'state/routing/utils'
|
||||
import { useRouterPreference, useUserSlippageTolerance } from 'state/user/hooks'
|
||||
import styled, { DefaultTheme, useTheme } from 'styled-components'
|
||||
import { ExternalLink, ThemedText } from 'theme/components'
|
||||
@ -60,6 +61,7 @@ export default function SwapModalFooter({
|
||||
fiatValueOutput,
|
||||
showAcceptChanges,
|
||||
onAcceptChanges,
|
||||
isLoading,
|
||||
}: {
|
||||
trade: InterfaceTrade
|
||||
swapResult?: SwapResult
|
||||
@ -71,6 +73,7 @@ export default function SwapModalFooter({
|
||||
fiatValueOutput: { data?: number; isLoading: boolean }
|
||||
showAcceptChanges: boolean
|
||||
onAcceptChanges: () => void
|
||||
isLoading: boolean
|
||||
}) {
|
||||
const transactionDeadlineSecondsSinceEpoch = useTransactionDeadline()?.toNumber() // in seconds since epoch
|
||||
const isAutoSlippage = useUserSlippageTolerance()[0] === 'auto'
|
||||
@ -113,17 +116,23 @@ export default function SwapModalFooter({
|
||||
<Plural value={txCount} one="Network fee" other="Network fees" />
|
||||
</Label>
|
||||
</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>
|
||||
{formatNumber({
|
||||
input: trade.totalGasUseEstimateUSD,
|
||||
type: NumberType.FiatGasPrice,
|
||||
})}
|
||||
{isSubmittableTrade(trade)
|
||||
? formatNumber({
|
||||
input: trade.totalGasUseEstimateUSD,
|
||||
type: NumberType.FiatGasPrice,
|
||||
})
|
||||
: '-'}
|
||||
</DetailRowValue>
|
||||
</MouseoverTooltip>
|
||||
</Row>
|
||||
</ThemedText.BodySmall>
|
||||
{isClassicTrade(trade) && (
|
||||
{(isClassicTrade(trade) || isPreviewTrade(trade)) && (
|
||||
<>
|
||||
<TokenTaxLineItem trade={trade} type="input" />
|
||||
<TokenTaxLineItem trade={trade} type="output" />
|
||||
@ -134,8 +143,10 @@ export default function SwapModalFooter({
|
||||
<Trans>Price impact</Trans>
|
||||
</Label>
|
||||
</MouseoverTooltip>
|
||||
<DetailRowValue warningColor={getPriceImpactColor(trade.priceImpact)}>
|
||||
{trade.priceImpact ? formatPriceImpact(trade.priceImpact) : '-'}
|
||||
<DetailRowValue
|
||||
warningColor={isClassicTrade(trade) ? getPriceImpactColor(trade.priceImpact) : undefined}
|
||||
>
|
||||
{isClassicTrade(trade) && trade.priceImpact ? formatPriceImpact(trade.priceImpact) : '-'}
|
||||
</DetailRowValue>
|
||||
</Row>
|
||||
</ThemedText.BodySmall>
|
||||
@ -219,9 +230,18 @@ export default function SwapModalFooter({
|
||||
$borderRadius="12px"
|
||||
id={InterfaceElementName.CONFIRM_SWAP_BUTTON}
|
||||
>
|
||||
<ThemedText.HeadlineSmall color="deprecated_accentTextLightPrimary">
|
||||
<Trans>Confirm swap</Trans>
|
||||
</ThemedText.HeadlineSmall>
|
||||
{isLoading ? (
|
||||
<ThemedText.HeadlineSmall color="neutral2">
|
||||
<Row>
|
||||
<SpinningLoader />
|
||||
<Trans>Finalizing quote...</Trans>
|
||||
</Row>
|
||||
</ThemedText.HeadlineSmall>
|
||||
) : (
|
||||
<ThemedText.HeadlineSmall color="deprecated_accentTextLightPrimary">
|
||||
<Trans>Confirm swap</Trans>
|
||||
</ThemedText.HeadlineSmall>
|
||||
)}
|
||||
</ConfirmButton>
|
||||
</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 [currency, percentage] =
|
||||
|
@ -1,7 +1,9 @@
|
||||
import {
|
||||
ETH_MAINNET,
|
||||
PREVIEW_EXACT_IN_TRADE,
|
||||
TEST_ALLOWED_SLIPPAGE,
|
||||
TEST_DUTCH_TRADE_ETH_INPUT,
|
||||
TEST_TOKEN_2,
|
||||
TEST_TRADE_EXACT_INPUT,
|
||||
TEST_TRADE_EXACT_OUTPUT,
|
||||
} from 'test-utils/constants'
|
||||
@ -44,4 +46,15 @@ describe('SwapModalHeader.tsx', () => {
|
||||
expect(screen.getByTestId('INPUT-amount')).toHaveTextContent(`<0.00001 ABC`)
|
||||
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 { useUSDPrice } from 'hooks/useUSDPrice'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
import { isPreviewTrade } from 'state/routing/utils'
|
||||
import { Field } from 'state/swap/actions'
|
||||
import styled from 'styled-components'
|
||||
import { Divider, ThemedText } from 'theme/components'
|
||||
@ -38,6 +39,7 @@ export default function SwapModalHeader({
|
||||
amount={trade.inputAmount}
|
||||
currency={inputCurrency ?? trade.inputAmount.currency}
|
||||
usdAmount={fiatValueInput.data}
|
||||
isLoading={isPreviewTrade(trade) && trade.tradeType === TradeType.EXACT_OUTPUT}
|
||||
/>
|
||||
<SwapModalHeaderAmount
|
||||
field={Field.OUTPUT}
|
||||
@ -45,6 +47,7 @@ export default function SwapModalHeader({
|
||||
amount={trade.postTaxOutputAmount}
|
||||
currency={trade.outputAmount.currency}
|
||||
usdAmount={fiatValueOutput.data}
|
||||
isLoading={isPreviewTrade(trade) && trade.tradeType === TradeType.EXACT_INPUT}
|
||||
tooltipText={
|
||||
trade.tradeType === TradeType.EXACT_INPUT ? (
|
||||
<ThemedText.BodySmall>
|
||||
|
@ -33,6 +33,7 @@ const ResponsiveHeadline = ({ children, ...textProps }: PropsWithChildren<TextPr
|
||||
}
|
||||
|
||||
interface AmountProps {
|
||||
isLoading: boolean
|
||||
field: Field
|
||||
tooltipText?: ReactNode
|
||||
label: ReactNode
|
||||
@ -44,7 +45,15 @@ interface AmountProps {
|
||||
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()
|
||||
|
||||
return (
|
||||
@ -56,7 +65,7 @@ export function SwapModalHeaderAmount({ tooltipText, label, amount, usdAmount, f
|
||||
</MouseoverTooltip>
|
||||
</ThemedText.BodySecondary>
|
||||
<Column gap="xs">
|
||||
<ResponsiveHeadline data-testid={`${field}-amount`}>
|
||||
<ResponsiveHeadline data-testid={`${field}-amount`} color={isLoading ? 'neutral2' : undefined}>
|
||||
{formatReviewSwapCurrencyAmount(amount)} {currency?.symbol}
|
||||
</ResponsiveHeadline>
|
||||
{usdAmount && (
|
||||
|
@ -343,3 +343,424 @@ exports[`SwapModalFooter.tsx matches base snapshot, test trade exact input 1`] =
|
||||
</div>
|
||||
</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"
|
||||
>
|
||||
<div
|
||||
class="c8 css-1geukv8"
|
||||
class="css-1geukv8"
|
||||
data-testid="INPUT-amount"
|
||||
>
|
||||
<0.00001 ABC
|
||||
@ -204,7 +204,7 @@ exports[`SwapModalHeader.tsx matches base snapshot, test trade exact input 1`] =
|
||||
class="c5"
|
||||
>
|
||||
<div
|
||||
class="c8 css-1geukv8"
|
||||
class="css-1geukv8"
|
||||
data-testid="OUTPUT-amount"
|
||||
>
|
||||
<0.00001 DEF
|
||||
@ -390,7 +390,7 @@ exports[`SwapModalHeader.tsx renders ETH input token for an ETH input UniswapX s
|
||||
class="c5"
|
||||
>
|
||||
<div
|
||||
class="c8 css-1geukv8"
|
||||
class="css-1geukv8"
|
||||
data-testid="INPUT-amount"
|
||||
>
|
||||
<0.00001 ETH
|
||||
@ -438,7 +438,241 @@ exports[`SwapModalHeader.tsx renders ETH input token for an ETH input UniswapX s
|
||||
class="c5"
|
||||
>
|
||||
<div
|
||||
class="c8 css-1geukv8"
|
||||
class="css-1geukv8"
|
||||
data-testid="OUTPUT-amount"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<0.00001 DEF
|
||||
@ -624,7 +858,7 @@ exports[`SwapModalHeader.tsx test trade exact output, no recipient 1`] = `
|
||||
class="c5"
|
||||
>
|
||||
<div
|
||||
class="c8 css-1geukv8"
|
||||
class="css-1geukv8"
|
||||
data-testid="INPUT-amount"
|
||||
>
|
||||
<0.00001 ABC
|
||||
@ -672,7 +906,7 @@ exports[`SwapModalHeader.tsx test trade exact output, no recipient 1`] = `
|
||||
class="c5"
|
||||
>
|
||||
<div
|
||||
class="c8 css-1geukv8"
|
||||
class="css-1geukv8"
|
||||
data-testid="OUTPUT-amount"
|
||||
>
|
||||
<0.00001 GHI
|
||||
|
9
src/featureFlags/flags/quickRouteMainnet.ts
Normal file
9
src/featureFlags/flags/quickRouteMainnet.ts
Normal file
@ -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',
|
||||
infoLiveViews = 'info_live_views',
|
||||
uniswapXDefaultEnabled = 'uniswapx_default_enabled',
|
||||
quickRouteMainnet = 'enable_quick_route_mainnet',
|
||||
}
|
||||
|
||||
interface FeatureFlagsContextType {
|
||||
|
@ -2,6 +2,7 @@ import { renderHook } from '@testing-library/react'
|
||||
import { CurrencyAmount, TradeType } from '@uniswap/sdk-core'
|
||||
import { DAI, USDC_MAINNET } from 'constants/tokens'
|
||||
import { RouterPreference, TradeState } from 'state/routing/types'
|
||||
import { usePreviewTrade } from 'state/routing/usePreviewTrade'
|
||||
import { useRouterPreference } from 'state/user/hooks'
|
||||
import { mocked } from 'test-utils/mocked'
|
||||
|
||||
@ -18,11 +19,13 @@ jest.mock('./useAutoRouterSupported')
|
||||
jest.mock('./useDebounce')
|
||||
jest.mock('./useIsWindowVisible')
|
||||
jest.mock('state/routing/useRoutingAPITrade')
|
||||
jest.mock('state/routing/usePreviewTrade')
|
||||
jest.mock('state/user/hooks')
|
||||
|
||||
// helpers to set mock expectations
|
||||
const expectRouterMock = (state: TradeState) => {
|
||||
mocked(useRoutingAPITrade).mockReturnValue({ state, trade: undefined })
|
||||
mocked(usePreviewTrade).mockReturnValue({ state, trade: undefined })
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@ -42,11 +45,11 @@ describe('#useBestV3Trade ExactIn', () => {
|
||||
const { result } = renderHook(() => useDebouncedTrade(TradeType.EXACT_INPUT, USDCAmount, DAI))
|
||||
|
||||
expect(useRoutingAPITrade).toHaveBeenCalledWith(
|
||||
/* skipFetch = */ true,
|
||||
TradeType.EXACT_INPUT,
|
||||
USDCAmount,
|
||||
DAI,
|
||||
RouterPreference.CLIENT,
|
||||
/* skipFetch = */ true,
|
||||
/* account = */ undefined,
|
||||
/* inputTax = */ undefined,
|
||||
/* outputTax = */ undefined
|
||||
@ -62,11 +65,11 @@ describe('#useDebouncedTrade ExactOut', () => {
|
||||
|
||||
const { result } = renderHook(() => useDebouncedTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
|
||||
expect(useRoutingAPITrade).toHaveBeenCalledWith(
|
||||
/* skipFetch = */ true,
|
||||
TradeType.EXACT_OUTPUT,
|
||||
DAIAmount,
|
||||
USDC_MAINNET,
|
||||
RouterPreference.CLIENT,
|
||||
/* skipFetch = */ true,
|
||||
/* account = */ undefined,
|
||||
/* inputTax = */ undefined,
|
||||
/* outputTax = */ undefined
|
||||
|
@ -4,6 +4,7 @@ import { WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
|
||||
import { DebounceSwapQuoteVariant, useDebounceSwapQuoteFlag } from 'featureFlags/flags/debounceSwapQuote'
|
||||
import { useMemo } from 'react'
|
||||
import { ClassicTrade, InterfaceTrade, QuoteMethod, RouterPreference, TradeState } from 'state/routing/types'
|
||||
import { usePreviewTrade } from 'state/routing/usePreviewTrade'
|
||||
import { useRoutingAPITrade } from 'state/routing/useRoutingAPITrade'
|
||||
import { useRouterPreference } from 'state/user/hooks'
|
||||
|
||||
@ -13,6 +14,7 @@ import useIsWindowVisible from './useIsWindowVisible'
|
||||
|
||||
// Prevents excessive quote requests between keystrokes.
|
||||
const DEBOUNCE_TIME = 350
|
||||
const DEBOUNCE_TIME_QUICKROUTE = 50
|
||||
|
||||
// Temporary until we remove the feature flag.
|
||||
const DEBOUNCE_TIME_INCREASED = 650
|
||||
@ -79,26 +81,44 @@ export function useDebouncedTrade(
|
||||
const isDebouncing =
|
||||
useDebounce(inputs, debouncedSwapQuoteFlagEnabled ? DEBOUNCE_TIME_INCREASED : DEBOUNCE_TIME) !== inputs
|
||||
|
||||
const isPreviewTradeDebouncing = useDebounce(inputs, DEBOUNCE_TIME_QUICKROUTE) !== inputs
|
||||
|
||||
const isWrap = useMemo(() => {
|
||||
if (!chainId || !amountSpecified || !otherCurrency) return false
|
||||
const weth = WRAPPED_NATIVE_CURRENCY[chainId]
|
||||
return (
|
||||
return Boolean(
|
||||
(amountSpecified.currency.isNative && weth?.equals(otherCurrency)) ||
|
||||
(otherCurrency.isNative && weth?.equals(amountSpecified.currency))
|
||||
(otherCurrency.isNative && weth?.equals(amountSpecified.currency))
|
||||
)
|
||||
}, [amountSpecified, chainId, otherCurrency])
|
||||
|
||||
const skipFetch = isDebouncing || !autoRouterSupported || !isWindowVisible || isWrap
|
||||
|
||||
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,
|
||||
amountSpecified,
|
||||
otherCurrency,
|
||||
routerPreferenceOverride ?? routerPreference,
|
||||
skipFetch,
|
||||
account,
|
||||
inputTax,
|
||||
outputTax
|
||||
)
|
||||
|
||||
return previewTradeResult.currentTrade && !routingApiTradeResult.currentTrade
|
||||
? previewTradeResult
|
||||
: routingApiTradeResult
|
||||
}
|
||||
|
@ -107,7 +107,7 @@ export default function usePermit2Allowance(
|
||||
const shouldRequestApproval = !(isApproved || isApprovalLoading)
|
||||
|
||||
// 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 approveAndPermit = useCallback(async () => {
|
||||
|
@ -36,7 +36,13 @@ export default function useStablecoinPrice(currency?: Currency): Price<Currency,
|
||||
const amountOut = chainId ? STABLECOIN_AMOUNT_OUT[chainId] : undefined
|
||||
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(() => {
|
||||
if (!currency || !stablecoin) {
|
||||
return undefined
|
||||
|
@ -29,11 +29,11 @@ function useETHPrice(currency?: Currency): {
|
||||
|
||||
const amountOut = isSupported ? ETH_AMOUNT_OUT[chainId] : undefined
|
||||
const { trade, state } = useRoutingAPITrade(
|
||||
!isSupported /* skip */,
|
||||
TradeType.EXACT_OUTPUT,
|
||||
amountOut,
|
||||
currency,
|
||||
INTERNAL_ROUTER_PREFERENCE_PRICE,
|
||||
!isSupported
|
||||
INTERNAL_ROUTER_PREFERENCE_PRICE
|
||||
)
|
||||
|
||||
return useMemo(() => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Currency, CurrencyAmount, Percent, Price, Token } from '@uniswap/sdk-core'
|
||||
import { NATIVE_CHAIN_ID } from 'constants/tokens'
|
||||
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'
|
||||
|
||||
export const getDurationUntilTimestampSeconds = (futureTimestampInSecondsSinceEpoch?: number): number | undefined => {
|
||||
@ -29,12 +29,18 @@ export const getPriceUpdateBasisPoints = (
|
||||
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) {
|
||||
return {
|
||||
routing: trade.fillType,
|
||||
type: trade.tradeType,
|
||||
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,
|
||||
token_in_address: getTokenAddress(trade.inputAmount.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
|
||||
: 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),
|
||||
allowed_slippage: formatPercentNumber(allowedSlippage),
|
||||
method: getQuoteMethod(trade),
|
||||
|
@ -51,7 +51,7 @@ import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { Text } from 'rebass'
|
||||
import { useAppSelector } from 'state/hooks'
|
||||
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 { useDefaultsFromURLSearch, useDerivedSwapInfo, useSwapActionHandlers } from 'state/swap/hooks'
|
||||
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}`};
|
||||
`
|
||||
|
||||
function getIsValidSwapQuote(
|
||||
function getIsReviewableQuote(
|
||||
trade: InterfaceTrade | undefined,
|
||||
tradeState: TradeState,
|
||||
swapInputError?: ReactNode
|
||||
): 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) {
|
||||
@ -518,7 +522,7 @@ export function Swap({
|
||||
|
||||
// warnings on the greater of fiat value price impact and execution price impact
|
||||
const { priceImpactSeverity, largerPriceImpact } = useMemo(() => {
|
||||
if (isUniswapXTrade(trade)) {
|
||||
if (!isClassicTrade(trade)) {
|
||||
return { priceImpactSeverity: 0, largerPriceImpact: undefined }
|
||||
}
|
||||
|
||||
@ -607,7 +611,7 @@ export function Swap({
|
||||
showCancel={true}
|
||||
/>
|
||||
<SwapHeader trade={trade} autoSlippage={autoSlippage} chainId={chainId} />
|
||||
{trade && showConfirm && allowance.state !== AllowanceState.LOADING && (
|
||||
{trade && showConfirm && (
|
||||
<ConfirmSwapModal
|
||||
trade={trade}
|
||||
inputCurrency={inputCurrency}
|
||||
@ -744,7 +748,7 @@ export function Swap({
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
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}
|
||||
>
|
||||
<ButtonLight onClick={toggleWalletDrawer} fontWeight={535} $borderRadius="16px">
|
||||
@ -803,7 +807,7 @@ export function Swap({
|
||||
}}
|
||||
id="swap-button"
|
||||
data-testid="swap-button"
|
||||
disabled={!getIsValidSwapQuote(trade, tradeState, swapInputError)}
|
||||
disabled={!getIsReviewableQuote(trade, tradeState, swapInputError)}
|
||||
error={!swapInputError && priceImpactSeverity > 2 && allowance.state === AllowanceState.ALLOWED}
|
||||
>
|
||||
<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.
|
||||
beforeEach(() => {
|
||||
// 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 { sentryEnhancer } from './logging'
|
||||
import reducer from './reducer'
|
||||
import { quickRouteApi } from './routing/quickRouteSlice'
|
||||
import { routingApi } from './routing/slice'
|
||||
|
||||
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
|
||||
// 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],
|
||||
ignoredPaths: [routingApi.reducerPath, quickRouteApi.reducerPath],
|
||||
ignoredActions: [
|
||||
// ignore the redux-persist actions
|
||||
'persist/PERSIST',
|
||||
@ -27,7 +28,9 @@ export function createDefaultStore() {
|
||||
'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 mint from './mint/reducer'
|
||||
import mintV3 from './mint/v3/reducer'
|
||||
import { quickRouteApi } from './routing/quickRouteSlice'
|
||||
import { routingApi } from './routing/slice'
|
||||
import signatures from './signatures/reducer'
|
||||
import transactions from './transactions/reducer'
|
||||
@ -35,6 +36,7 @@ const appReducer = combineReducers({
|
||||
multicall: multicall.reducer,
|
||||
logs,
|
||||
[routingApi.reducerPath]: routingApi.reducer,
|
||||
[quickRouteApi.reducerPath]: quickRouteApi.reducer,
|
||||
...persistedReducers,
|
||||
})
|
||||
|
||||
|
@ -18,6 +18,7 @@ import { MintState } from './mint/reducer'
|
||||
import { Field as FieldV3 } from './mint/v3/actions'
|
||||
import { FullRange, MintState as MintV3State } from './mint/v3/reducer'
|
||||
import { AppState } from './reducer'
|
||||
import { quickRouteApi } from './routing/quickRouteSlice'
|
||||
import { routingApi } from './routing/slice'
|
||||
import { RouterPreference } from './routing/types'
|
||||
import { SignatureState } from './signatures/reducer'
|
||||
@ -61,6 +62,7 @@ type ExpectedAppState = CombinedState<{
|
||||
multicall: ReturnType<typeof multicall.reducer>
|
||||
logs: LogsState
|
||||
[routingApi.reducerPath]: ReturnType<typeof routingApi.reducer>
|
||||
[quickRouteApi.reducerPath]: ReturnType<typeof quickRouteApi.reducer>
|
||||
}>
|
||||
|
||||
assert<Equals<AppState, ExpectedAppState>>()
|
||||
|
112
src/state/routing/quickRouteSlice.ts
Normal file
112
src/state/routing/quickRouteSlice.ts
Normal file
@ -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) {
|
||||
let fellBack = false
|
||||
logSwapQuoteRequest(args.tokenInChainId, args.routerPreference)
|
||||
logSwapQuoteRequest(args.tokenInChainId, args.routerPreference, false)
|
||||
const quoteStartMark = performance.mark(`quote-fetch-start-${Date.now()}`)
|
||||
if (shouldUseAPIRouter(args)) {
|
||||
fellBack = true
|
||||
@ -150,7 +150,7 @@ export const routingApi = createApi({
|
||||
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
|
||||
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' &&
|
||||
|
@ -1,19 +1,20 @@
|
||||
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 { Route as V2Route } from '@uniswap/v2-sdk'
|
||||
import { Route as V3Route } from '@uniswap/v3-sdk'
|
||||
|
||||
export enum TradeState {
|
||||
LOADING,
|
||||
INVALID,
|
||||
STALE,
|
||||
NO_ROUTE_FOUND,
|
||||
VALID,
|
||||
LOADING = 'loading',
|
||||
INVALID = 'invalid',
|
||||
STALE = 'stale',
|
||||
NO_ROUTE_FOUND = 'no_route_found',
|
||||
VALID = 'valid',
|
||||
}
|
||||
|
||||
export enum QuoteMethod {
|
||||
ROUTING_API = 'ROUTING_API',
|
||||
QUICK_ROUTE = 'QUICK_ROUTE',
|
||||
CLIENT_SIDE = 'CLIENT_SIDE',
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
type TokenInRoute = Pick<Token, 'address' | 'chainId' | 'symbol' | 'decimals'>
|
||||
@ -132,6 +147,26 @@ type URAClassicQuoteResponse = {
|
||||
}
|
||||
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 {
|
||||
return data.routing === URAQuoteType.CLASSIC
|
||||
}
|
||||
@ -139,6 +174,7 @@ export function isClassicQuoteResponse(data: URAQuoteResponse): data is URAClass
|
||||
export enum TradeFillType {
|
||||
Classic = 'classic', // Uniswap V1, V2, and V3 trades with on-chain 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 }
|
||||
@ -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 {
|
||||
SUCCESS = 'Success',
|
||||
@ -327,7 +453,19 @@ export type TradeResult =
|
||||
}
|
||||
| {
|
||||
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
|
||||
}
|
||||
|
||||
|
128
src/state/routing/usePreviewTrade.ts
Normal file
128
src/state/routing/usePreviewTrade.ts
Normal file
@ -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 {
|
||||
ClassicTrade,
|
||||
InterfaceTrade,
|
||||
INTERNAL_ROUTER_PREFERENCE_PRICE,
|
||||
QuoteMethod,
|
||||
QuoteState,
|
||||
RouterPreference,
|
||||
SubmittableTrade,
|
||||
TradeState,
|
||||
} from './types'
|
||||
|
||||
const TRADE_NOT_FOUND = { state: TradeState.NO_ROUTE_FOUND, trade: undefined } as const
|
||||
const TRADE_LOADING = { state: TradeState.LOADING, 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, currentData: undefined } as const
|
||||
|
||||
export function useRoutingAPITrade<TTradeType extends TradeType>(
|
||||
skipFetch: boolean,
|
||||
tradeType: TTradeType,
|
||||
amountSpecified: CurrencyAmount<Currency> | undefined,
|
||||
otherCurrency: Currency | undefined,
|
||||
routerPreference: typeof INTERNAL_ROUTER_PREFERENCE_PRICE,
|
||||
skipFetch?: boolean,
|
||||
account?: string,
|
||||
inputTax?: Percent,
|
||||
outputTax?: Percent
|
||||
): {
|
||||
state: TradeState
|
||||
trade?: ClassicTrade
|
||||
currentTrade?: ClassicTrade
|
||||
swapQuoteLatency?: number
|
||||
}
|
||||
|
||||
export function useRoutingAPITrade<TTradeType extends TradeType>(
|
||||
skipFetch: boolean,
|
||||
tradeType: TTradeType,
|
||||
amountSpecified: CurrencyAmount<Currency> | undefined,
|
||||
otherCurrency: Currency | undefined,
|
||||
routerPreference: RouterPreference,
|
||||
skipFetch?: boolean,
|
||||
account?: string,
|
||||
inputTax?: Percent,
|
||||
outputTax?: Percent
|
||||
): {
|
||||
state: TradeState
|
||||
trade?: InterfaceTrade
|
||||
trade?: SubmittableTrade
|
||||
currentTrade?: SubmittableTrade
|
||||
swapQuoteLatency?: number
|
||||
}
|
||||
|
||||
@ -57,17 +59,18 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
|
||||
* @param otherCurrency the desired output/payment currency
|
||||
*/
|
||||
export function useRoutingAPITrade<TTradeType extends TradeType>(
|
||||
skipFetch = false,
|
||||
tradeType: TTradeType,
|
||||
amountSpecified: CurrencyAmount<Currency> | undefined,
|
||||
otherCurrency: Currency | undefined,
|
||||
routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE,
|
||||
skipFetch = false,
|
||||
account?: string,
|
||||
inputTax = ZERO_PERCENT,
|
||||
outputTax = ZERO_PERCENT
|
||||
): {
|
||||
state: TradeState
|
||||
trade?: InterfaceTrade
|
||||
trade?: SubmittableTrade
|
||||
currentTrade?: SubmittableTrade
|
||||
method?: QuoteMethod
|
||||
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
|
||||
refetchOnMountOrArgChange: 2 * 60,
|
||||
})
|
||||
|
||||
const isFetching = currentData !== tradeResult || !currentData
|
||||
|
||||
return useMemo(() => {
|
||||
@ -104,12 +108,14 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
|
||||
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) {
|
||||
@ -120,6 +126,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
|
||||
return {
|
||||
state: isFetching ? TradeState.LOADING : TradeState.VALID,
|
||||
trade: tradeResult?.trade,
|
||||
currentTrade: currentData?.trade,
|
||||
swapQuoteLatency: tradeResult?.latencyMs,
|
||||
}
|
||||
}
|
||||
@ -132,5 +139,6 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
|
||||
tradeResult?.latencyMs,
|
||||
tradeResult?.state,
|
||||
tradeResult?.trade,
|
||||
currentData?.trade,
|
||||
])
|
||||
}
|
||||
|
@ -12,13 +12,17 @@ import {
|
||||
ClassicQuoteData,
|
||||
ClassicTrade,
|
||||
DutchOrderTrade,
|
||||
GetQuickQuoteArgs,
|
||||
GetQuoteArgs,
|
||||
InterfaceTrade,
|
||||
isClassicQuoteResponse,
|
||||
PoolType,
|
||||
PreviewTrade,
|
||||
QuickRouteResponse,
|
||||
QuoteMethod,
|
||||
QuoteState,
|
||||
RouterPreference,
|
||||
SubmittableTrade,
|
||||
SwapRouterNativeAssets,
|
||||
TradeFillType,
|
||||
TradeResult,
|
||||
@ -114,7 +118,7 @@ function toDutchOrderInfo(orderInfoJSON: DutchOrderInfoJSON): DutchOrderInfo {
|
||||
// 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
|
||||
// swap will use WETH.
|
||||
function getTradeCurrencies(args: GetQuoteArgs, isUniswapXTrade: boolean): [Currency, Currency] {
|
||||
function getTradeCurrencies(args: GetQuoteArgs | GetQuickQuoteArgs, isUniswapXTrade: boolean): [Currency, Currency] {
|
||||
const {
|
||||
tokenInAddress,
|
||||
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(
|
||||
args: GetQuoteArgs,
|
||||
data: URAQuoteResponse,
|
||||
@ -313,6 +328,14 @@ export function isClassicTrade(trade?: InterfaceTrade): trade is ClassicTrade {
|
||||
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 {
|
||||
return trade?.fillType === TradeFillType.UniswapX
|
||||
}
|
||||
@ -322,6 +345,8 @@ export function shouldUseAPIRouter(args: GetQuoteArgs): boolean {
|
||||
}
|
||||
|
||||
export function getTransactionCount(trade: InterfaceTrade): number {
|
||||
if (!isSubmittableTrade(trade)) return 0
|
||||
|
||||
let count = 0
|
||||
if (trade.approveInfo.needsApprove) {
|
||||
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 { BigNumber } from 'ethers/lib/ethers'
|
||||
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_2 = new Token(1, '0x0000000000000000000000000000000000000002', 18, 'DEF', 'Def')
|
||||
@ -160,3 +160,11 @@ export const TEST_TRADE_FEE_ON_BUY = new ClassicTrade({
|
||||
inputTax: ZERO_PERCENT,
|
||||
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, {
|
||||
chainId: mockChainId,
|
||||
isQuickRoute: false,
|
||||
time_to_first_quote_request: 100,
|
||||
time_to_first_quote_request_since_first_input: 100,
|
||||
})
|
||||
@ -75,6 +76,7 @@ describe('swapFlowLoggers', () => {
|
||||
|
||||
expect(sendAnalyticsEvent).toHaveBeenCalledWith(SwapEventName.SWAP_QUOTE_FETCH, {
|
||||
chainId: mockChainId,
|
||||
isQuickRoute: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -34,7 +34,8 @@ export function maybeLogFirstSwapAction(analyticsContext: ITraceContext) {
|
||||
|
||||
export function logSwapQuoteRequest(
|
||||
chainId: number,
|
||||
routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE
|
||||
routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE,
|
||||
isQuickRoute?: boolean
|
||||
) {
|
||||
let performanceMetrics = {}
|
||||
if (routerPreference !== INTERNAL_ROUTER_PREFERENCE_PRICE) {
|
||||
@ -50,6 +51,7 @@ export function logSwapQuoteRequest(
|
||||
}
|
||||
sendAnalyticsEvent(SwapEventName.SWAP_QUOTE_FETCH, {
|
||||
chainId,
|
||||
isQuickRoute: isQuickRoute ?? false,
|
||||
...performanceMetrics,
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user