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 { 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}
|
||||||
|
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 }>`
|
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"
|
||||||
>
|
>
|
||||||
<0.00001 ABC
|
<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"
|
||||||
>
|
>
|
||||||
<0.00001 DEF
|
<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"
|
||||||
>
|
>
|
||||||
<0.00001 ETH
|
<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"
|
||||||
|
>
|
||||||
|
<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"
|
data-testid="OUTPUT-amount"
|
||||||
>
|
>
|
||||||
<0.00001 DEF
|
<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"
|
||||||
>
|
>
|
||||||
<0.00001 ABC
|
<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"
|
||||||
>
|
>
|
||||||
<0.00001 GHI
|
<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',
|
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>>()
|
||||||
|
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) {
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 { 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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user