refactor: swap line items and tooltips decomposition (#7390)

* refactor: swap line items and tooltips decomposition

* test: test line items directly

* refactor: added tooltip prop

* refactor: preview trade logic

* fix: percentage color

* lint

* fix: exchange rate alignment

* fix: initial pr comments

* test: fix snapshots

* refactor: var naming

* fix: uneeded dep array var

* refactor: small nit
This commit is contained in:
cartcrom 2023-10-03 15:22:07 -04:00 committed by GitHub
parent 55a509cad8
commit 82aaf0784a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 9791 additions and 1101 deletions

@ -32,6 +32,13 @@ export const LoadingRows = styled.div`
}
`
export const LoadingRow = styled.div<{ height: number; width: number }>`
${shimmerMixin}
border-radius: 12px;
height: ${({ height }) => height}px;
width: ${({ width }) => width}px;
`
export const loadingOpacityMixin = css<{ $loading: boolean }>`
filter: ${({ $loading }) => ($loading ? 'grayscale(1)' : 'none')};
opacity: ${({ $loading }) => ($loading ? '0.6' : '1')};

@ -1,10 +1,11 @@
import { QuoteMethod, SubmittableTrade } from 'state/routing/types'
import { isUniswapXTrade } from 'state/routing/utils'
import { DefaultTheme } from 'styled-components'
import { ThemedText } from 'theme/components'
import UniswapXRouterLabel from './UniswapXRouterLabel'
export default function RouterLabel({ trade }: { trade: SubmittableTrade }) {
export default function RouterLabel({ trade, color }: { trade: SubmittableTrade; color?: keyof DefaultTheme }) {
if (isUniswapXTrade(trade)) {
return (
<UniswapXRouterLabel>
@ -13,7 +14,7 @@ export default function RouterLabel({ trade }: { trade: SubmittableTrade }) {
)
}
if (trade.quoteMethod === QuoteMethod.CLIENT_SIDE || trade.quoteMethod === QuoteMethod.CLIENT_SIDE_FALLBACK) {
return <ThemedText.BodySmall>Uniswap Client</ThemedText.BodySmall>
return <ThemedText.BodySmall color={color}>Uniswap Client</ThemedText.BodySmall>
}
return <ThemedText.BodySmall>Uniswap API</ThemedText.BodySmall>
return <ThemedText.BodySmall color={color}>Uniswap API</ThemedText.BodySmall>
}

@ -77,9 +77,11 @@ type MouseoverTooltipProps = Omit<PopoverProps, 'content' | 'show'> &
timeout?: number
placement?: PopoverProps['placement']
onOpen?: () => void
forceShow?: boolean
}>
export function MouseoverTooltip({ text, disabled, children, onOpen, timeout, ...rest }: MouseoverTooltipProps) {
export function MouseoverTooltip(props: MouseoverTooltipProps) {
const { text, disabled, children, onOpen, forceShow, timeout, ...rest } = props
const [show, setShow] = useState(false)
const open = () => {
setShow(true)
@ -101,7 +103,14 @@ export function MouseoverTooltip({ text, disabled, children, onOpen, timeout, ..
}, [timeout, show])
return (
<Tooltip {...rest} open={open} close={close} disabled={disabled} show={show} text={disabled ? null : text}>
<Tooltip
{...rest}
open={open}
close={close}
disabled={disabled}
show={forceShow || show}
text={disabled ? null : text}
>
<div onMouseEnter={disabled ? noop : open} onMouseLeave={disabled || timeout ? noop : close}>
{children}
</div>

@ -1,38 +0,0 @@
import userEvent from '@testing-library/user-event'
import { TEST_ALLOWED_SLIPPAGE, TEST_TRADE_EXACT_INPUT, TEST_TRADE_EXACT_OUTPUT } from 'test-utils/constants'
import { act, render, screen } from 'test-utils/render'
import { AdvancedSwapDetails } from './AdvancedSwapDetails'
describe('AdvancedSwapDetails.tsx', () => {
it('matches base snapshot', () => {
const { asFragment } = render(
<AdvancedSwapDetails trade={TEST_TRADE_EXACT_INPUT} allowedSlippage={TEST_ALLOWED_SLIPPAGE} />
)
expect(asFragment()).toMatchSnapshot()
})
it('renders correct copy on mouseover', async () => {
render(<AdvancedSwapDetails trade={TEST_TRADE_EXACT_INPUT} allowedSlippage={TEST_ALLOWED_SLIPPAGE} />)
await act(() => userEvent.hover(screen.getByText('Expected output')))
expect(await screen.getByText(/The amount you expect to receive at the current market price./i)).toBeVisible()
await act(() => userEvent.hover(screen.getByText(/Minimum output/i)))
expect(await screen.getByText(/The minimum amount you are guaranteed to receive./i)).toBeVisible()
})
it('renders correct tooltips for test trade with exact output and gas use estimate USD', async () => {
TEST_TRADE_EXACT_OUTPUT.gasUseEstimateUSD = 1.0
render(<AdvancedSwapDetails trade={TEST_TRADE_EXACT_OUTPUT} allowedSlippage={TEST_ALLOWED_SLIPPAGE} />)
await act(() => userEvent.hover(screen.getByText(/Maximum input/i)))
expect(await screen.getByText(/The minimum amount you are guaranteed to receive./i)).toBeVisible()
await act(() => userEvent.hover(screen.getByText('Network fee')))
expect(await screen.getByText(/The fee paid to miners who process your transaction./i)).toBeVisible()
})
it('renders loading rows when syncing', async () => {
render(
<AdvancedSwapDetails trade={TEST_TRADE_EXACT_OUTPUT} allowedSlippage={TEST_ALLOWED_SLIPPAGE} syncing={true} />
)
expect(screen.getAllByTestId('loading-rows').length).toBeGreaterThan(0)
})
})

@ -1,228 +0,0 @@
import { Plural, Trans } from '@lingui/macro'
import { InterfaceElementName, SwapEventName } from '@uniswap/analytics-events'
import { Percent, TradeType } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { sendAnalyticsEvent } from 'analytics'
import { LoadingRows } from 'components/Loader/styled'
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
import { ZERO_PERCENT } from 'constants/misc'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { ClassicTrade, InterfaceTrade } from 'state/routing/types'
import { getTransactionCount, isClassicTrade, isSubmittableTrade } from 'state/routing/utils'
import { ExternalLink, Separator, ThemedText } from 'theme/components'
import { NumberType, useFormatter } from 'utils/formatNumbers'
import Column from '../Column'
import RouterLabel from '../RouterLabel'
import { RowBetween, RowFixed } from '../Row'
import { MouseoverTooltip, TooltipSize } from '../Tooltip'
import { GasBreakdownTooltip } from './GasBreakdownTooltip'
import SwapRoute from './SwapRoute'
interface AdvancedSwapDetailsProps {
trade: InterfaceTrade
allowedSlippage: Percent
syncing?: boolean
}
function TextWithLoadingPlaceholder({
syncing,
width,
children,
}: {
syncing: boolean
width: number
children: JSX.Element
}) {
return syncing ? (
<LoadingRows data-testid="loading-rows">
<div style={{ height: '15px', width: `${width}px` }} />
</LoadingRows>
) : (
children
)
}
export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }: AdvancedSwapDetailsProps) {
const { chainId } = useWeb3React()
const nativeCurrency = useNativeCurrency(chainId)
const txCount = getTransactionCount(trade)
const { formatCurrencyAmount, formatNumber, formatPriceImpact } = useFormatter()
const supportsGasEstimate = chainId && SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) && isSubmittableTrade(trade)
return (
<Column gap="md">
<Separator />
{supportsGasEstimate && (
<RowBetween>
<MouseoverTooltip
text={
<Trans>
The fee paid to miners who process your transaction. This must be paid in {nativeCurrency.symbol}.
</Trans>
}
>
<ThemedText.BodySmall color="neutral2">
<Plural value={txCount} one="Network fee" other="Network fees" />
</ThemedText.BodySmall>
</MouseoverTooltip>
<MouseoverTooltip
placement="right"
size={TooltipSize.Small}
text={<GasBreakdownTooltip trade={trade} hideUniswapXDescription />}
>
<TextWithLoadingPlaceholder syncing={syncing} width={50}>
<ThemedText.BodySmall>
{`${trade.totalGasUseEstimateUSD ? '~' : ''}${formatNumber({
input: trade.totalGasUseEstimateUSD,
type: NumberType.FiatGasPrice,
})}`}
</ThemedText.BodySmall>
</TextWithLoadingPlaceholder>
</MouseoverTooltip>
</RowBetween>
)}
{isClassicTrade(trade) && (
<>
<TokenTaxLineItem trade={trade} type="input" syncing={syncing} />
<TokenTaxLineItem trade={trade} type="output" syncing={syncing} />
<RowBetween>
<MouseoverTooltip text={<Trans>The impact your trade has on the market price of this pool.</Trans>}>
<ThemedText.BodySmall color="neutral2">
<Trans>Price Impact</Trans>
</ThemedText.BodySmall>
</MouseoverTooltip>
<TextWithLoadingPlaceholder syncing={syncing} width={50}>
<ThemedText.BodySmall>{formatPriceImpact(trade.priceImpact)}</ThemedText.BodySmall>
</TextWithLoadingPlaceholder>
</RowBetween>
</>
)}
<RowBetween>
<RowFixed>
<MouseoverTooltip
text={
<Trans>
The minimum amount you are guaranteed to receive. If the price slips any further, your transaction will
revert.
</Trans>
}
>
<ThemedText.BodySmall color="neutral2">
{trade.tradeType === TradeType.EXACT_INPUT ? <Trans>Minimum output</Trans> : <Trans>Maximum input</Trans>}
</ThemedText.BodySmall>
</MouseoverTooltip>
</RowFixed>
<TextWithLoadingPlaceholder syncing={syncing} width={70}>
<ThemedText.BodySmall>
{trade.tradeType === TradeType.EXACT_INPUT
? `${formatCurrencyAmount({
amount: trade.minimumAmountOut(allowedSlippage),
type: NumberType.SwapTradeAmount,
})} ${trade.outputAmount.currency.symbol}`
: `${trade.maximumAmountIn(allowedSlippage).toSignificant(6)} ${trade.inputAmount.currency.symbol}`}
</ThemedText.BodySmall>
</TextWithLoadingPlaceholder>
</RowBetween>
<RowBetween>
<RowFixed>
<MouseoverTooltip
text={
<Trans>
The amount you expect to receive at the current market price. You may receive less or more if the market
price changes while your transaction is pending.
</Trans>
}
>
<ThemedText.BodySmall color="neutral2">
<Trans>Expected output</Trans>
</ThemedText.BodySmall>
</MouseoverTooltip>
</RowFixed>
<TextWithLoadingPlaceholder syncing={syncing} width={65}>
<ThemedText.BodySmall>
{`${formatCurrencyAmount({
amount: trade.postTaxOutputAmount,
type: NumberType.SwapTradeAmount,
})} ${trade.outputAmount.currency.symbol}`}
</ThemedText.BodySmall>
</TextWithLoadingPlaceholder>
</RowBetween>
<Separator />
{isSubmittableTrade(trade) && (
<RowBetween>
<ThemedText.BodySmall color="neutral2">
<Trans>Order routing</Trans>
</ThemedText.BodySmall>
{isClassicTrade(trade) ? (
<MouseoverTooltip
size={TooltipSize.Large}
text={<SwapRoute data-testid="swap-route-info" trade={trade} syncing={syncing} />}
onOpen={() => {
sendAnalyticsEvent(SwapEventName.SWAP_AUTOROUTER_VISUALIZATION_EXPANDED, {
element: InterfaceElementName.AUTOROUTER_VISUALIZATION_ROW,
})
}}
>
<RouterLabel trade={trade} />
</MouseoverTooltip>
) : (
<MouseoverTooltip
size={TooltipSize.Small}
text={<GasBreakdownTooltip trade={trade} hideFees />}
placement="right"
onOpen={() => {
sendAnalyticsEvent(SwapEventName.SWAP_AUTOROUTER_VISUALIZATION_EXPANDED, {
element: InterfaceElementName.AUTOROUTER_VISUALIZATION_ROW,
})
}}
>
<RouterLabel trade={trade} />
</MouseoverTooltip>
)}
</RowBetween>
)}
</Column>
)
}
function TokenTaxLineItem({
trade,
type,
syncing,
}: {
trade: ClassicTrade
type: 'input' | 'output'
syncing: boolean
}) {
const { formatPriceImpact } = useFormatter()
if (syncing) return null
const [currency, percentage] =
type === 'input' ? [trade.inputAmount.currency, trade.inputTax] : [trade.outputAmount.currency, trade.outputTax]
if (percentage.equalTo(ZERO_PERCENT)) return null
return (
<RowBetween>
<MouseoverTooltip
text={
<>
<Trans>
Some tokens take a fee when they are bought or sold, which is set by the token issuer. Uniswap does not
receive any of these fees.
</Trans>{' '}
<ExternalLink href="https://support.uniswap.org/hc/en-us/articles/18673568523789-What-is-a-token-fee-">
Learn more
</ExternalLink>
</>
}
>
<ThemedText.BodySmall color="neutral2">{`${currency.symbol} fee`}</ThemedText.BodySmall>
</MouseoverTooltip>
<ThemedText.BodySmall>{formatPriceImpact(percentage)}</ThemedText.BodySmall>
</RowBetween>
)
}

@ -1,10 +1,12 @@
import { Trans } from '@lingui/macro'
import { Currency } from '@uniswap/sdk-core'
import { AutoColumn } from 'components/Column'
import UniswapXRouterLabel, { UniswapXGradient } from 'components/RouterLabel/UniswapXRouterLabel'
import Row from 'components/Row'
import { nativeOnChain } from 'constants/tokens'
import { ReactNode } from 'react'
import { SubmittableTrade } from 'state/routing/types'
import { isClassicTrade, isUniswapXTrade } from 'state/routing/utils'
import { InterfaceTrade } from 'state/routing/types'
import { isPreviewTrade, isUniswapXTrade } from 'state/routing/utils'
import styled from 'styled-components'
import { Divider, ExternalLink, ThemedText } from 'theme/components'
import { NumberType, useFormatter } from 'utils/formatNumbers'
@ -13,99 +15,83 @@ const Container = styled(AutoColumn)`
padding: 4px;
`
const InlineLink = styled(ThemedText.BodySmall)`
color: ${({ theme }) => theme.accent1};
display: inline;
cursor: pointer;
&:hover {
opacity: ${({ theme }) => theme.opacity.hover};
}
`
type GasCostItemProps = { title: ReactNode; itemValue?: React.ReactNode; amount?: number }
const InlineUniswapXGradient = styled(UniswapXGradient)`
display: inline;
`
const GasCostItem = ({
title,
amount,
itemValue,
}: {
title: ReactNode
itemValue?: React.ReactNode
amount?: number
}) => {
const GasCostItem = ({ title, amount, itemValue }: GasCostItemProps) => {
const { formatNumber } = useFormatter()
if (!amount && !itemValue) return null
const value = itemValue ?? formatNumber({ input: amount, type: NumberType.FiatGasPrice })
return (
<Row justify="space-between">
<ThemedText.SubHeaderSmall>{title}</ThemedText.SubHeaderSmall>
<ThemedText.SubHeaderSmall color="neutral1">
{itemValue ??
formatNumber({
input: amount,
type: NumberType.FiatGasPrice,
})}
</ThemedText.SubHeaderSmall>
<ThemedText.SubHeaderSmall color="neutral1">{value}</ThemedText.SubHeaderSmall>
</Row>
)
}
export function GasBreakdownTooltip({
trade,
hideFees = false,
hideUniswapXDescription = false,
}: {
trade: SubmittableTrade
hideFees?: boolean
hideUniswapXDescription?: boolean
}) {
const swapEstimate = isClassicTrade(trade) ? trade.gasUseEstimateUSD : undefined
const GaslessSwapLabel = () => <UniswapXRouterLabel>$0</UniswapXRouterLabel>
type GasBreakdownTooltipProps = { trade: InterfaceTrade; hideUniswapXDescription?: boolean }
export function GasBreakdownTooltip({ trade, hideUniswapXDescription }: GasBreakdownTooltipProps) {
const isUniswapX = isUniswapXTrade(trade)
const inputCurrency = trade.inputAmount.currency
const native = nativeOnChain(inputCurrency.chainId)
if (isPreviewTrade(trade)) return <NetworkFeesDescription native={native} />
const swapEstimate = !isUniswapX ? trade.gasUseEstimateUSD : undefined
const approvalEstimate = trade.approveInfo.needsApprove ? trade.approveInfo.approveGasEstimateUSD : undefined
const wrapEstimate =
isUniswapXTrade(trade) && trade.wrapInfo.needsWrap ? trade.wrapInfo.wrapGasEstimateUSD : undefined
const wrapEstimate = isUniswapX && trade.wrapInfo.needsWrap ? trade.wrapInfo.wrapGasEstimateUSD : undefined
const showEstimateDetails = Boolean(wrapEstimate || approvalEstimate)
const description =
isUniswapX && !hideUniswapXDescription ? <UniswapXDescription /> : <NetworkFeesDescription native={native} />
if (!showEstimateDetails) return description
return (
<Container gap="md">
{(wrapEstimate || approvalEstimate) && !hideFees && (
<>
<AutoColumn gap="sm">
{wrapEstimate && <GasCostItem title={<Trans>Wrap ETH</Trans>} amount={wrapEstimate} />}
{approvalEstimate && (
<GasCostItem
title={<Trans>Allow {trade.inputAmount.currency.symbol} (one time)</Trans>}
amount={approvalEstimate}
/>
)}
{swapEstimate && <GasCostItem title={<Trans>Swap</Trans>} amount={swapEstimate} />}
{isUniswapXTrade(trade) && (
<GasCostItem title={<Trans>Swap</Trans>} itemValue={<UniswapXRouterLabel>$0</UniswapXRouterLabel>} />
)}
</AutoColumn>
<Divider />
</>
)}
{isUniswapXTrade(trade) && !hideUniswapXDescription ? (
<ThemedText.BodySmall color="neutral2">
<Trans>
<InlineUniswapXGradient>UniswapX</InlineUniswapXGradient> aggregates liquidity sources for better prices and
gas free swaps.
</Trans>{' '}
<ExternalLink href="https://support.uniswap.org/hc/en-us/articles/17515415311501">
<InlineLink>
<Trans>Learn more</Trans>
</InlineLink>
</ExternalLink>
</ThemedText.BodySmall>
) : (
<ThemedText.BodySmall color="neutral2">
<Trans>Network Fees are paid to the Ethereum network to secure transactions.</Trans>{' '}
<ExternalLink href="https://support.uniswap.org/hc/en-us/articles/8370337377805-What-is-a-network-fee-">
<InlineLink>
<Trans>Learn more</Trans>
</InlineLink>
</ExternalLink>
</ThemedText.BodySmall>
)}
<AutoColumn gap="sm">
<GasCostItem title={<Trans>Wrap {native.symbol}</Trans>} amount={wrapEstimate} />
<GasCostItem title={<Trans>Allow {inputCurrency.symbol} (one time)</Trans>} amount={approvalEstimate} />
<GasCostItem title={<Trans>Swap</Trans>} amount={swapEstimate} />
{isUniswapX && <GasCostItem title={<Trans>Swap</Trans>} itemValue={<GaslessSwapLabel />} />}
</AutoColumn>
<Divider />
{description}
</Container>
)
}
function NetworkFeesDescription({ native }: { native: Currency }) {
return (
<ThemedText.LabelMicro>
<Trans>
The fee paid to the Ethereum network to process your transaction. This must be paid in {native.symbol}.
</Trans>{' '}
<ExternalLink href="https://support.uniswap.org/hc/en-us/articles/8370337377805-What-is-a-network-fee-">
<Trans>Learn more</Trans>
</ExternalLink>
</ThemedText.LabelMicro>
)
}
const InlineUniswapXGradient = styled(UniswapXGradient)`
display: inline;
`
export function UniswapXDescription() {
return (
<ThemedText.Caption color="neutral2">
<Trans>
<InlineUniswapXGradient>UniswapX</InlineUniswapXGradient> aggregates liquidity sources for better prices and gas
free swaps.
</Trans>{' '}
<ExternalLink href="https://support.uniswap.org/hc/en-us/articles/17515415311501">
<Trans>Learn more</Trans>
</ExternalLink>
</ThemedText.Caption>
)
}

@ -12,10 +12,11 @@ import { ChevronDown } from 'react-feather'
import { InterfaceTrade } from 'state/routing/types'
import { isSubmittableTrade } from 'state/routing/utils'
import styled, { useTheme } from 'styled-components'
import { ThemedText } from 'theme/components'
import { Separator, ThemedText } from 'theme/components'
import { useFormatter } from 'utils/formatNumbers'
import { AdvancedSwapDetails } from './AdvancedSwapDetails'
import GasEstimateTooltip from './GasEstimateTooltip'
import SwapLineItem, { SwapLineItemType } from './SwapLineItem'
import TradePrice from './TradePrice'
const StyledHeaderRow = styled(RowBetween)<{ disabled: boolean; open: boolean }>`
@ -29,7 +30,7 @@ const RotatingArrow = styled(ChevronDown)<{ open?: boolean }>`
transition: transform 0.1s linear;
`
const SwapDetailsWrapper = styled.div`
const SwapDetailsWrapper = styled(Column)`
padding-top: ${({ theme }) => theme.grids.md};
`
@ -39,14 +40,15 @@ const Wrapper = styled(Column)`
padding: 12px 16px;
`
interface SwapDetailsInlineProps {
interface SwapDetailsProps {
trade?: InterfaceTrade
syncing: boolean
loading: boolean
allowedSlippage: Percent
}
export default function SwapDetailsDropdown({ trade, syncing, loading, allowedSlippage }: SwapDetailsInlineProps) {
export default function SwapDetailsDropdown(props: SwapDetailsProps) {
const { trade, syncing, loading, allowedSlippage } = props
const theme = useTheme()
const [showDetails, setShowDetails] = useState(false)
const trace = useTrace()
@ -88,13 +90,33 @@ export default function SwapDetailsDropdown({ trade, syncing, loading, allowedSl
</RowFixed>
</StyledHeaderRow>
</TraceEvent>
{trade && (
<AnimatedDropdown open={showDetails}>
<SwapDetailsWrapper data-testid="advanced-swap-details">
<AdvancedSwapDetails trade={trade} allowedSlippage={allowedSlippage} syncing={syncing} />
</SwapDetailsWrapper>
</AnimatedDropdown>
)}
<AdvancedSwapDetails {...props} open={showDetails} />
</Wrapper>
)
}
function AdvancedSwapDetails(props: SwapDetailsProps & { open: boolean }) {
const { open, trade, allowedSlippage, syncing = false } = props
const format = useFormatter()
if (!trade) return null
const lineItemProps = { trade, allowedSlippage, format, syncing }
return (
<AnimatedDropdown open={open}>
<SwapDetailsWrapper gap="md" data-testid="advanced-swap-details">
<Separator />
<SwapLineItem {...lineItemProps} type={SwapLineItemType.NETWORK_FEE} />
<SwapLineItem {...lineItemProps} type={SwapLineItemType.PRICE_IMPACT} />
<SwapLineItem {...lineItemProps} type={SwapLineItemType.INPUT_TOKEN_FEE_ON_TRANSFER} />
<SwapLineItem {...lineItemProps} type={SwapLineItemType.OUTPUT_TOKEN_FEE_ON_TRANSFER} />
<SwapLineItem {...lineItemProps} type={SwapLineItemType.MAXIMUM_INPUT} />
<SwapLineItem {...lineItemProps} type={SwapLineItemType.MINIMUM_OUTPUT} />
<SwapLineItem {...lineItemProps} type={SwapLineItemType.EXPECTED_OUTPUT} />
<Separator />
<SwapLineItem {...lineItemProps} type={SwapLineItemType.ROUTING_INFO} />
</SwapDetailsWrapper>
</AnimatedDropdown>
)
}

@ -0,0 +1,73 @@
import { InterfaceTrade } from 'state/routing/types'
import {
PREVIEW_EXACT_IN_TRADE,
TEST_ALLOWED_SLIPPAGE,
TEST_DUTCH_TRADE_ETH_INPUT,
TEST_TRADE_EXACT_INPUT,
TEST_TRADE_EXACT_INPUT_API,
TEST_TRADE_EXACT_OUTPUT,
TEST_TRADE_FEE_ON_BUY,
TEST_TRADE_FEE_ON_SELL,
} from 'test-utils/constants'
import { render } from 'test-utils/render'
// Forces tooltips to render in snapshots
jest.mock('react-dom', () => {
const original = jest.requireActual('react-dom')
return {
...original,
createPortal: (node: any) => node,
}
})
// Prevents uuid from generating unpredictable values in snapshots
jest.mock('uuid', () => ({
v4: () => 'fixed-uuid-value',
}))
import SwapLineItem, { SwapLineItemType } from './SwapLineItem'
const AllLineItemsTypes = Object.keys(SwapLineItemType).map(Number).filter(Boolean)
const lineItemProps = {
syncing: false,
allowedSlippage: TEST_ALLOWED_SLIPPAGE,
}
function testTradeLineItems(trade: InterfaceTrade, props: Partial<typeof lineItemProps> = {}) {
const { asFragment } = render(
<>
{AllLineItemsTypes.map((type) => (
<SwapLineItem key={type} trade={trade} type={type} {...lineItemProps} {...props} />
))}
</>
)
expect(asFragment()).toMatchSnapshot()
}
/* eslint-disable jest/expect-expect */ // allow expect inside testTradeLineItems
describe('SwapLineItem.tsx', () => {
it('exact input', () => {
testTradeLineItems(TEST_TRADE_EXACT_INPUT)
})
it('exact output', () => {
testTradeLineItems(TEST_TRADE_EXACT_OUTPUT)
})
it('fee on buy', () => {
testTradeLineItems(TEST_TRADE_FEE_ON_BUY)
})
it('fee on sell', () => {
testTradeLineItems(TEST_TRADE_FEE_ON_SELL)
})
it('exact input api', () => {
testTradeLineItems(TEST_TRADE_EXACT_INPUT_API)
})
it('dutch order eth input', () => {
testTradeLineItems(TEST_DUTCH_TRADE_ETH_INPUT)
})
it('syncing', () => {
testTradeLineItems(TEST_TRADE_EXACT_INPUT, { syncing: true })
})
it('preview exact in', () => {
testTradeLineItems(PREVIEW_EXACT_IN_TRADE)
})
})

@ -0,0 +1,252 @@
import { Plural, t, Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core'
import { LoadingRow } from 'components/Loader/styled'
import RouterLabel from 'components/RouterLabel'
import { RowBetween } from 'components/Row'
import { MouseoverTooltip, TooltipSize } from 'components/Tooltip'
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
import useHoverProps from 'hooks/useHoverProps'
import { useIsMobile } from 'nft/hooks'
import React, { PropsWithChildren, useEffect, useState } from 'react'
import { InterfaceTrade, TradeFillType } from 'state/routing/types'
import { getTransactionCount, isPreviewTrade, isUniswapXTrade } from 'state/routing/utils'
import styled, { DefaultTheme } from 'styled-components'
import { ExternalLink, ThemedText } from 'theme/components'
import { NumberType, useFormatter } from 'utils/formatNumbers'
import { getPriceImpactColor } from 'utils/prices'
import { GasBreakdownTooltip, UniswapXDescription } from './GasBreakdownTooltip'
import SwapRoute from './SwapRoute'
export enum SwapLineItemType {
EXCHANGE_RATE,
NETWORK_FEE,
INPUT_TOKEN_FEE_ON_TRANSFER,
OUTPUT_TOKEN_FEE_ON_TRANSFER,
PRICE_IMPACT,
MAXIMUM_INPUT,
MINIMUM_OUTPUT,
EXPECTED_OUTPUT,
ROUTING_INFO,
}
const DetailRowValue = styled(ThemedText.BodySmall)`
text-align: right;
overflow-wrap: break-word;
`
const LabelText = styled(ThemedText.BodySmall)<{ hasTooltip?: boolean }>`
cursor: ${({ hasTooltip }) => (hasTooltip ? 'help' : 'auto')};
color: ${({ theme }) => theme.neutral2};
`
const ColorWrapper = styled.span<{ textColor?: keyof DefaultTheme }>`
${({ textColor, theme }) => textColor && `color: ${theme[textColor]};`}
`
function FOTTooltipContent() {
return (
<>
<Trans>
Some tokens take a fee when they are bought or sold, which is set by the token issuer. Uniswap does not receive
any of these fees.
</Trans>{' '}
<ExternalLink href="https://support.uniswap.org/hc/en-us/articles/18673568523789-What-is-a-token-fee-">
Learn more
</ExternalLink>
</>
)
}
function Loading({ width = 50 }: { width?: number }) {
return <LoadingRow data-testid="loading-row" height={15} width={width} />
}
function ExchangeRateRow({ trade }: { trade: InterfaceTrade }) {
const { formatNumber } = useFormatter()
const rate = `1 ${trade.executionPrice.quoteCurrency.symbol} = ${formatNumber({
input: parseFloat(trade.executionPrice.toFixed(9)),
type: NumberType.TokenTx,
})} ${trade.executionPrice.baseCurrency.symbol}`
return <>{rate}</>
}
function ColoredPercentRow({ percent }: { percent: Percent }) {
const { formatPriceImpact } = useFormatter()
return <ColorWrapper textColor={getPriceImpactColor(percent)}>{formatPriceImpact(percent)}</ColorWrapper>
}
function CurrencyAmountRow({ amount }: { amount: CurrencyAmount<Currency> }) {
const { formatCurrencyAmount } = useFormatter()
const formattedAmount = formatCurrencyAmount({ amount, type: NumberType.SwapDetailsAmount })
return <>{`${formattedAmount} ${amount.currency.symbol}`}</>
}
type LineItemData = {
Label: React.FC
Value: React.FC
TooltipBody?: React.FC
tooltipSize?: TooltipSize
loaderWidth?: number
}
function useLineItem(props: SwapLineItemProps): LineItemData | undefined {
const { trade, syncing, allowedSlippage, type } = props
const { formatNumber } = useFormatter()
const isUniswapX = isUniswapXTrade(trade)
const isPreview = isPreviewTrade(trade)
const chainId = trade.inputAmount.currency.chainId
// Tracks the latest submittable trade's fill type, used to 'guess' whether or not to show price impact during preview
const [lastSubmittableFillType, setLastSubmittableFillType] = useState<TradeFillType>()
useEffect(() => {
if (trade.fillType !== TradeFillType.None) setLastSubmittableFillType(trade.fillType)
}, [trade.fillType])
switch (type) {
case SwapLineItemType.EXCHANGE_RATE:
return {
Label: () => <Trans>Exchange rate</Trans>,
Value: () => <ExchangeRateRow trade={trade} />,
}
case SwapLineItemType.NETWORK_FEE:
if (!SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId)) return
return {
Label: () => <Plural value={getTransactionCount(trade) || 1} one="Network fee" other="Network fees" />,
TooltipBody: () => <GasBreakdownTooltip trade={trade} hideUniswapXDescription />,
Value: () => {
if (isPreview) return <Loading />
return <>{formatNumber({ input: trade.totalGasUseEstimateUSD, type: NumberType.FiatGasPrice })}</>
},
}
case SwapLineItemType.PRICE_IMPACT:
// Hides price impact row if the current trade is UniswapX or we're expecting a preview trade to result in UniswapX
if (isUniswapX || (isPreview && lastSubmittableFillType === TradeFillType.UniswapX)) return
return {
Label: () => <Trans>Price impact</Trans>,
TooltipBody: () => <Trans>The impact your trade has on the market price of this pool.</Trans>,
Value: () => (isPreview ? <Loading /> : <ColoredPercentRow percent={trade.priceImpact} />),
}
case SwapLineItemType.MAXIMUM_INPUT:
if (trade.tradeType === TradeType.EXACT_INPUT) return
return {
Label: () => <Trans>Maximum input</Trans>,
TooltipBody: () => (
<Trans>
The maximum amount you are guaranteed to spend. If the price slips any further, your transaction will
revert.
</Trans>
),
Value: () => <CurrencyAmountRow amount={trade.maximumAmountIn(allowedSlippage)} />,
loaderWidth: 70,
}
case SwapLineItemType.MINIMUM_OUTPUT:
if (trade.tradeType === TradeType.EXACT_OUTPUT) return
return {
Label: () => <Trans>Minimum output</Trans>,
TooltipBody: () => (
<Trans>
The minimum amount you are guaranteed to receive. If the price slips any further, your transaction will
revert.
</Trans>
),
Value: () => <CurrencyAmountRow amount={trade.minimumAmountOut(allowedSlippage)} />,
loaderWidth: 70,
}
case SwapLineItemType.EXPECTED_OUTPUT:
return {
Label: () => <Trans>Expected output</Trans>,
TooltipBody: () => (
<Trans>
The amount you expect to receive at the current market price. You may receive less or more if the market
price changes while your transaction is pending.
</Trans>
),
Value: () => <CurrencyAmountRow amount={trade.postTaxOutputAmount} />,
loaderWidth: 65,
}
case SwapLineItemType.ROUTING_INFO:
if (isPreview) return { Label: () => <Trans>Order routing</Trans>, Value: () => <Loading /> }
return {
Label: () => <Trans>Order routing</Trans>,
TooltipBody: () => {
if (isUniswapX) return <UniswapXDescription />
return <SwapRoute data-testid="swap-route-info" trade={trade} syncing={syncing} />
},
tooltipSize: isUniswapX ? TooltipSize.Small : TooltipSize.Large,
Value: () => <RouterLabel trade={trade} />,
}
case SwapLineItemType.INPUT_TOKEN_FEE_ON_TRANSFER:
case SwapLineItemType.OUTPUT_TOKEN_FEE_ON_TRANSFER:
return getFOTLineItem(props)
}
}
function getFOTLineItem({ type, trade }: SwapLineItemProps): LineItemData | undefined {
const isInput = type === SwapLineItemType.INPUT_TOKEN_FEE_ON_TRANSFER
const currency = isInput ? trade.inputAmount.currency : trade.outputAmount.currency
const tax = isInput ? trade.inputTax : trade.outputTax
if (tax.equalTo(0)) return
return {
Label: () => <>{t`${currency.symbol ?? currency.name ?? t`Token`} fee`}</>,
TooltipBody: FOTTooltipContent,
Value: () => <ColoredPercentRow percent={tax} />,
}
}
type ValueWrapperProps = PropsWithChildren<{
lineItem: LineItemData
labelHovered: boolean
syncing: boolean
}>
function ValueWrapper({ children, lineItem, labelHovered, syncing }: ValueWrapperProps) {
const { TooltipBody, tooltipSize, loaderWidth } = lineItem
const isMobile = useIsMobile()
if (syncing) return <Loading width={loaderWidth} />
if (!TooltipBody) return <DetailRowValue>{children}</DetailRowValue>
return (
<MouseoverTooltip
placement={isMobile ? 'auto' : 'right'}
forceShow={labelHovered} // displays tooltip when hovering either both label or value
size={tooltipSize}
text={
<ThemedText.Caption color="neutral2">
<TooltipBody />
</ThemedText.Caption>
}
>
<DetailRowValue>{children}</DetailRowValue>
</MouseoverTooltip>
)
}
interface SwapLineItemProps {
trade: InterfaceTrade
syncing: boolean
allowedSlippage: Percent
type: SwapLineItemType
}
function SwapLineItem(props: SwapLineItemProps) {
const [labelHovered, hoverProps] = useHoverProps()
const LineItem = useLineItem(props)
if (!LineItem) return null
return (
<RowBetween>
<LabelText {...hoverProps} hasTooltip={!!LineItem.TooltipBody} data-testid="swap-li-label">
<LineItem.Label />
</LabelText>
<ValueWrapper lineItem={LineItem} labelHovered={labelHovered} syncing={props.syncing}>
<LineItem.Value />
</ValueWrapper>
</RowBetween>
)
}
export default React.memo(SwapLineItem)

@ -1,13 +1,4 @@
import {
PREVIEW_EXACT_IN_TRADE,
TEST_ALLOWED_SLIPPAGE,
TEST_TOKEN_1,
TEST_TOKEN_2,
TEST_TRADE_EXACT_INPUT,
TEST_TRADE_EXACT_OUTPUT,
TEST_TRADE_FEE_ON_BUY,
TEST_TRADE_FEE_ON_SELL,
} from 'test-utils/constants'
import { PREVIEW_EXACT_IN_TRADE, TEST_ALLOWED_SLIPPAGE, TEST_TRADE_EXACT_INPUT } from 'test-utils/constants'
import { render, screen, within } from 'test-utils/render'
import SwapModalFooter from './SwapModalFooter'
@ -43,7 +34,7 @@ describe('SwapModalFooter.tsx', () => {
)
).toBeInTheDocument()
expect(
screen.getByText('The fee paid to miners who process your transaction. This must be paid in $ETH.')
screen.getByText('The fee paid to the Ethereum network to process your transaction. This must be paid in ETH.')
).toBeInTheDocument()
expect(screen.getByText('The impact your trade has on the market price of this pool.')).toBeInTheDocument()
})
@ -77,99 +68,6 @@ describe('SwapModalFooter.tsx', () => {
expect(within(showAcceptChanges).getByText('Accept')).toBeVisible()
})
it('test trade exact output, no recipient', () => {
render(
<SwapModalFooter
isLoading={false}
trade={TEST_TRADE_EXACT_OUTPUT}
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
swapResult={undefined}
onConfirm={jest.fn()}
swapErrorMessage={undefined}
disabledConfirm={false}
fiatValueInput={{
data: undefined,
isLoading: false,
}}
fiatValueOutput={{
data: undefined,
isLoading: false,
}}
showAcceptChanges={true}
onAcceptChanges={jest.fn()}
/>
)
expect(
screen.getByText(
'The maximum amount you are guaranteed to spend. If the price slips any further, your transaction will revert.'
)
).toBeInTheDocument()
expect(
screen.getByText('The fee paid to miners who process your transaction. This must be paid in $ETH.')
).toBeInTheDocument()
expect(screen.getByText('The impact your trade has on the market price of this pool.')).toBeInTheDocument()
})
it('test trade fee on input token transfer', () => {
render(
<SwapModalFooter
isLoading={false}
trade={TEST_TRADE_FEE_ON_SELL}
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
swapResult={undefined}
onConfirm={jest.fn()}
swapErrorMessage={undefined}
disabledConfirm={false}
fiatValueInput={{
data: undefined,
isLoading: false,
}}
fiatValueOutput={{
data: undefined,
isLoading: false,
}}
showAcceptChanges={true}
onAcceptChanges={jest.fn()}
/>
)
expect(
screen.getByText(
'Some tokens take a fee when they are bought or sold, which is set by the token issuer. Uniswap does not receive any of these fees.'
)
).toBeInTheDocument()
expect(screen.getByText(`${TEST_TOKEN_1.symbol} fee`)).toBeInTheDocument()
})
it('test trade fee on output token transfer', () => {
render(
<SwapModalFooter
isLoading={false}
trade={TEST_TRADE_FEE_ON_BUY}
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
swapResult={undefined}
onConfirm={jest.fn()}
swapErrorMessage={undefined}
disabledConfirm={false}
fiatValueInput={{
data: undefined,
isLoading: false,
}}
fiatValueOutput={{
data: undefined,
isLoading: false,
}}
showAcceptChanges={true}
onAcceptChanges={jest.fn()}
/>
)
expect(
screen.getByText(
'Some tokens take a fee when they are bought or sold, which is set by the token issuer. Uniswap does not receive any of these fees.'
)
).toBeInTheDocument()
expect(screen.getByText(`${TEST_TOKEN_2.symbol} fee`)).toBeInTheDocument()
})
it('renders a preview trade while disabling submission', () => {
const { asFragment } = render(
<SwapModalFooter

@ -1,34 +1,26 @@
import { Plural, t, Trans } from '@lingui/macro'
import { Trans } from '@lingui/macro'
import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/analytics-events'
import { Percent, TradeType } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { Percent } from '@uniswap/sdk-core'
import { TraceEvent } from 'analytics'
import Column from 'components/Column'
import SpinningLoader from 'components/Loader/SpinningLoader'
import { MouseoverTooltip, TooltipSize } from 'components/Tooltip'
import { ZERO_PERCENT } from 'constants/misc'
import { SwapResult } from 'hooks/useSwapCallback'
import useTransactionDeadline from 'hooks/useTransactionDeadline'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { ReactNode } from 'react'
import { AlertTriangle } from 'react-feather'
import { ClassicTrade, InterfaceTrade, PreviewTrade, RouterPreference } from 'state/routing/types'
import { getTransactionCount, isClassicTrade, isPreviewTrade, isSubmittableTrade } from 'state/routing/utils'
import { InterfaceTrade, RouterPreference } from 'state/routing/types'
import { isClassicTrade } from 'state/routing/utils'
import { useRouterPreference, useUserSlippageTolerance } from 'state/user/hooks'
import styled, { DefaultTheme, useTheme } from 'styled-components'
import { ExternalLink, ThemedText } from 'theme/components'
import { FormatterRule, NumberType, SIX_SIG_FIGS_NO_COMMAS, useFormatter } from 'utils/formatNumbers'
import styled, { useTheme } from 'styled-components'
import { ThemedText } from 'theme/components'
import getRoutingDiagramEntries from 'utils/getRoutingDiagramEntries'
import { formatSwapButtonClickEventProperties } from 'utils/loggingFormatters'
import { getPriceImpactColor } from 'utils/prices'
import { ButtonError, SmallButtonPrimary } from '../Button'
import Row, { AutoRow, RowBetween, RowFixed } from '../Row'
import { GasBreakdownTooltip } from './GasBreakdownTooltip'
import { SwapCallbackError, SwapShowAcceptChanges } from './styled'
import { Label } from './SwapModalHeaderAmount'
const sixFigsFormatterRules: FormatterRule[] = [{ upperBound: Infinity, formatterOptions: SIX_SIG_FIGS_NO_COMMAS }]
import { SwapLineItemType } from './SwapLineItem'
import SwapLineItem from './SwapLineItem'
const DetailsContainer = styled(Column)`
padding: 0 8px;
@ -44,12 +36,6 @@ const ConfirmButton = styled(ButtonError)`
margin-top: 10px;
`
const DetailRowValue = styled(ThemedText.BodySmall)<{ warningColor?: keyof DefaultTheme }>`
text-align: right;
overflow-wrap: break-word;
${({ warningColor, theme }) => warningColor && `color: ${theme[warningColor]};`};
`
export default function SwapModalFooter({
trade,
allowedSlippage,
@ -80,116 +66,19 @@ export default function SwapModalFooter({
const [routerPreference] = useRouterPreference()
const routes = isClassicTrade(trade) ? getRoutingDiagramEntries(trade) : undefined
const theme = useTheme()
const { chainId } = useWeb3React()
const nativeCurrency = useNativeCurrency(chainId)
const { formatCurrencyAmount, formatNumber, formatPriceImpact } = useFormatter()
const label = `${trade.executionPrice.baseCurrency?.symbol} `
const labelInverted = `${trade.executionPrice.quoteCurrency?.symbol}`
const formattedPrice = formatNumber({
input: trade.executionPrice ? parseFloat(trade.executionPrice.toFixed(9)) : undefined,
type: NumberType.TokenTx,
})
const txCount = getTransactionCount(trade)
const lineItemProps = { trade, allowedSlippage, syncing: false }
return (
<>
<DetailsContainer gap="md">
<ThemedText.BodySmall>
<Row align="flex-start" justify="space-between" gap="sm">
<Label>
<Trans>Exchange rate</Trans>
</Label>
<DetailRowValue>{`1 ${labelInverted} = ${formattedPrice ?? '-'} ${label}`}</DetailRowValue>
</Row>
</ThemedText.BodySmall>
<ThemedText.BodySmall>
<Row align="flex-start" justify="space-between" gap="sm">
<MouseoverTooltip
text={
<Trans>
The fee paid to miners who process your transaction. This must be paid in ${nativeCurrency.symbol}.
</Trans>
}
>
<Label cursor="help">
<Plural value={txCount} one="Network fee" other="Network fees" />
</Label>
</MouseoverTooltip>
<MouseoverTooltip
placement="right"
size={TooltipSize.Small}
text={isSubmittableTrade(trade) ? <GasBreakdownTooltip trade={trade} /> : undefined}
>
<DetailRowValue>
{isSubmittableTrade(trade)
? formatNumber({
input: trade.totalGasUseEstimateUSD,
type: NumberType.FiatGasPrice,
})
: '-'}
</DetailRowValue>
</MouseoverTooltip>
</Row>
</ThemedText.BodySmall>
{(isClassicTrade(trade) || isPreviewTrade(trade)) && (
<>
<TokenTaxLineItem trade={trade} type="input" />
<TokenTaxLineItem trade={trade} type="output" />
<ThemedText.BodySmall>
<Row align="flex-start" justify="space-between" gap="sm">
<MouseoverTooltip text={<Trans>The impact your trade has on the market price of this pool.</Trans>}>
<Label cursor="help">
<Trans>Price impact</Trans>
</Label>
</MouseoverTooltip>
<DetailRowValue
warningColor={isClassicTrade(trade) ? getPriceImpactColor(trade.priceImpact) : undefined}
>
{isClassicTrade(trade) && trade.priceImpact ? formatPriceImpact(trade.priceImpact) : '-'}
</DetailRowValue>
</Row>
</ThemedText.BodySmall>
</>
)}
<ThemedText.BodySmall>
<Row align="flex-start" justify="space-between" gap="sm">
<MouseoverTooltip
text={
trade.tradeType === TradeType.EXACT_INPUT ? (
<Trans>
The minimum amount you are guaranteed to receive. If the price slips any further, your transaction
will revert.
</Trans>
) : (
<Trans>
The maximum amount you are guaranteed to spend. If the price slips any further, your transaction
will revert.
</Trans>
)
}
>
<Label cursor="help">
{trade.tradeType === TradeType.EXACT_INPUT ? (
<Trans>Minimum received</Trans>
) : (
<Trans>Maximum sent</Trans>
)}
</Label>
</MouseoverTooltip>
<DetailRowValue>
{trade.tradeType === TradeType.EXACT_INPUT
? `${formatCurrencyAmount({
amount: trade.minimumAmountOut(allowedSlippage),
type: sixFigsFormatterRules,
})} ${trade.outputAmount.currency.symbol}`
: `${formatCurrencyAmount({
amount: trade.maximumAmountIn(allowedSlippage),
type: sixFigsFormatterRules,
})} ${trade.inputAmount.currency.symbol}`}
</DetailRowValue>
</Row>
</ThemedText.BodySmall>
<SwapLineItem {...lineItemProps} type={SwapLineItemType.EXCHANGE_RATE} />
<SwapLineItem {...lineItemProps} type={SwapLineItemType.NETWORK_FEE} />
<SwapLineItem {...lineItemProps} type={SwapLineItemType.PRICE_IMPACT} />
<SwapLineItem {...lineItemProps} type={SwapLineItemType.INPUT_TOKEN_FEE_ON_TRANSFER} />
<SwapLineItem {...lineItemProps} type={SwapLineItemType.OUTPUT_TOKEN_FEE_ON_TRANSFER} />
<SwapLineItem {...lineItemProps} type={SwapLineItemType.MAXIMUM_INPUT} />
<SwapLineItem {...lineItemProps} type={SwapLineItemType.MINIMUM_OUTPUT} />
</DetailsContainer>
{showAcceptChanges ? (
<SwapShowAcceptChanges data-testid="show-accept-changes">
@ -251,35 +140,3 @@ export default function SwapModalFooter({
</>
)
}
function TokenTaxLineItem({ trade, type }: { trade: ClassicTrade | PreviewTrade; type: 'input' | 'output' }) {
const { formatPriceImpact } = useFormatter()
const [currency, percentage] =
type === 'input' ? [trade.inputAmount.currency, trade.inputTax] : [trade.outputAmount.currency, trade.outputTax]
if (percentage.equalTo(ZERO_PERCENT)) return null
return (
<ThemedText.BodySmall>
<Row align="flex-start" justify="space-between" gap="sm">
<MouseoverTooltip
text={
<>
<Trans>
Some tokens take a fee when they are bought or sold, which is set by the token issuer. Uniswap does not
receive any of these fees.
</Trans>{' '}
<ExternalLink href="https://support.uniswap.org/hc/en-us/articles/18673568523789-What-is-a-token-fee-">
Learn more
</ExternalLink>
</>
}
>
<Label cursor="help">{t`${currency.symbol} fee`}</Label>
</MouseoverTooltip>
<DetailRowValue warningColor={getPriceImpactColor(percentage)}>{formatPriceImpact(percentage)}</DetailRowValue>
</Row>
</ThemedText.BodySmall>
)
}

@ -12,7 +12,7 @@ import { BREAKPOINTS } from 'theme'
import { ThemedText } from 'theme/components'
import { NumberType, useFormatter } from 'utils/formatNumbers'
export const Label = styled(ThemedText.BodySmall)<{ cursor?: string }>`
const Label = styled(ThemedText.BodySmall)<{ cursor?: string }>`
cursor: ${({ cursor }) => cursor};
color: ${({ theme }) => theme.neutral2};
margin-right: 8px;

@ -28,7 +28,7 @@ export default function SwapRoute({ trade, syncing }: { trade: ClassicTrade; syn
return (
<Column gap="md">
<RouterLabel trade={trade} />
<RouterLabel trade={trade} color="neutral2" />
<Separator />
{syncing ? (
<LoadingRows>
@ -49,13 +49,13 @@ export default function SwapRoute({ trade, syncing }: { trade: ClassicTrade; syn
<div style={{ width: '100%', height: '15px' }} />
</LoadingRows>
) : (
<ThemedText.BodySmall color="neutral2">
<ThemedText.Caption color="neutral2">
{gasPrice ? <Trans>Best price route costs ~{gasPrice} in gas.</Trans> : null}{' '}
<Trans>
This route optimizes your total output by considering split routes, multiple hops, and the gas cost of
each step.
</Trans>
</ThemedText.BodySmall>
</ThemedText.Caption>
)}
</>
)}

@ -1,200 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
<DocumentFragment>
.c2 {
box-sizing: border-box;
margin: 0;
min-width: 0;
}
.c3 {
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;
}
.c4 {
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
}
.c8 {
width: -webkit-fit-content;
width: -moz-fit-content;
width: fit-content;
}
.c6 {
color: #7D7D7D;
}
.c7 {
color: #222222;
}
.c1 {
width: 100%;
height: 1px;
background-color: #22222212;
}
.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;
}
.c5 {
display: inline-block;
height: inherit;
}
<div
class="c0"
>
<div
class="c1"
/>
<div
class="c2 c3 c4"
>
<div
class="c5"
>
<div>
<div
class="c6 css-142zc9n"
>
Network fee
</div>
</div>
</div>
<div
class="c5"
>
<div>
<div
class="c7 css-142zc9n"
>
~$1.00
</div>
</div>
</div>
</div>
<div
class="c2 c3 c4"
>
<div
class="c5"
>
<div>
<div
class="c6 css-142zc9n"
>
Price Impact
</div>
</div>
</div>
<div
class="c7 css-142zc9n"
>
105566.373%
</div>
</div>
<div
class="c2 c3 c4"
>
<div
class="c2 c3 c8"
>
<div
class="c5"
>
<div>
<div
class="c6 css-142zc9n"
>
Minimum output
</div>
</div>
</div>
</div>
<div
class="c7 css-142zc9n"
>
0.00000000000000098 DEF
</div>
</div>
<div
class="c2 c3 c4"
>
<div
class="c2 c3 c8"
>
<div
class="c5"
>
<div>
<div
class="c6 css-142zc9n"
>
Expected output
</div>
</div>
</div>
</div>
<div
class="c7 css-142zc9n"
>
0.000000000000001 DEF
</div>
</div>
<div
class="c1"
/>
<div
class="c2 c3 c4"
>
<div
class="c6 css-142zc9n"
>
Order routing
</div>
<div
class="c5"
>
<div>
<div
class="c7 css-142zc9n"
>
Uniswap Client
</div>
</div>
</div>
</div>
</div>
</DocumentFragment>
`;

@ -91,7 +91,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
justify-content: flex-start;
}
.c17 {
.c16 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
@ -128,6 +128,16 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
fill: #7D7D7D;
}
.c20 {
text-align: right;
overflow-wrap: break-word;
}
.c19 {
cursor: help;
color: #7D7D7D;
}
.c8 {
background-color: transparent;
border: none;
@ -178,7 +188,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
transition: transform 0.1s linear;
}
.c16 {
.c17 {
padding-top: 12px;
}
@ -281,126 +291,121 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
>
<div>
<div
class="c16"
class="c16 c17"
data-testid="advanced-swap-details"
>
<div
class="c17"
class="c18"
/>
<div
class="c2 c3 c4"
>
<div
class="c18"
/>
<div
class="c2 c3 c4"
class="c9 c19 css-142zc9n"
data-testid="swap-li-label"
>
<div
class="c12"
>
<div>
<div
class="c14 css-142zc9n"
>
Network fee
</div>
</div>
</div>
<div
class="c12"
>
<div>
<div
class="c9 css-142zc9n"
>
~$1.00
</div>
</div>
</div>
Network fee
</div>
<div
class="c2 c3 c4"
class="c12"
>
<div
class="c12"
>
<div>
<div
class="c14 css-142zc9n"
>
Price Impact
</div>
</div>
</div>
<div
class="c9 css-142zc9n"
>
105566.373%
</div>
</div>
<div
class="c2 c3 c4"
>
<div
class="c2 c3 c6"
>
<div>
<div
class="c12"
class="c9 c20 css-142zc9n"
>
<div>
<div
class="c14 css-142zc9n"
>
Minimum output
</div>
</div>
$1.00
</div>
</div>
<div
class="c9 css-142zc9n"
>
0.00000000000000098 DEF
</div>
</div>
</div>
<div
class="c2 c3 c4"
>
<div
class="c9 c19 css-142zc9n"
data-testid="swap-li-label"
>
Price impact
</div>
<div
class="c2 c3 c4"
class="c12"
>
<div
class="c2 c3 c6"
>
<div>
<div
class="c12"
class="c9 c20 css-142zc9n"
>
<div>
<div
class="c14 css-142zc9n"
>
Expected output
</div>
</div>
<span
class=""
>
105566.373%
</span>
</div>
</div>
<div
class="c9 css-142zc9n"
>
0.000000000000001 DEF
</div>
</div>
</div>
<div
class="c2 c3 c4"
>
<div
class="c9 c19 css-142zc9n"
data-testid="swap-li-label"
>
Minimum output
</div>
<div
class="c18"
/>
<div
class="c2 c3 c4"
class="c12"
>
<div
class="c14 css-142zc9n"
>
Order routing
<div>
<div
class="c9 c20 css-142zc9n"
>
0.00000000000000098 DEF
</div>
</div>
<div
class="c12"
>
<div>
</div>
</div>
<div
class="c2 c3 c4"
>
<div
class="c9 c19 css-142zc9n"
data-testid="swap-li-label"
>
Expected output
</div>
<div
class="c12"
>
<div>
<div
class="c9 c20 css-142zc9n"
>
0.000000000000001 DEF
</div>
</div>
</div>
</div>
<div
class="c18"
/>
<div
class="c2 c3 c4"
>
<div
class="c9 c19 css-142zc9n"
data-testid="swap-li-label"
>
Order routing
</div>
<div
class="c12"
>
<div>
<div
class="c9 c20 css-142zc9n"
>
<div
class="c9 css-142zc9n"
class="css-142zc9n"
>
Uniswap Client
</div>

File diff suppressed because it is too large Load Diff

@ -2,31 +2,37 @@
exports[`SwapModalFooter.tsx matches base snapshot, test trade exact input 1`] = `
<DocumentFragment>
.c3 {
.c2 {
box-sizing: border-box;
margin: 0;
min-width: 0;
}
.c4 {
.c3 {
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-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;
}
.c4 {
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
gap: 8px;
}
.c2 {
.c5 {
color: #222222;
}
@ -45,131 +51,113 @@ exports[`SwapModalFooter.tsx matches base snapshot, test trade exact input 1`] =
gap: 12px;
}
.c7 {
.c9 {
display: inline-block;
height: inherit;
}
.c5 {
.c7 {
text-align: right;
overflow-wrap: break-word;
}
.c6 {
cursor: auto;
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;
}
<div
class="c0 c1"
>
<div
class="c2 css-142zc9n"
class="c2 c3 c4"
>
<div
class="c3 c4"
class="c5 c6 css-142zc9n"
data-testid="swap-li-label"
>
<div
class="c2 c5 css-142zc9n"
>
Exchange rate
</div>
<div
class="c2 c6 css-142zc9n"
>
1 DEF = 1.00 ABC
</div>
Exchange rate
</div>
<div
class="c5 c7 css-142zc9n"
>
1 DEF = 1.00 ABC
</div>
</div>
<div
class="c2 css-142zc9n"
class="c2 c3 c4"
>
<div
class="c3 c4"
class="c5 c8 css-142zc9n"
data-testid="swap-li-label"
>
<div
class="c7"
>
<div>
<div
class="c2 c8 css-142zc9n"
cursor="help"
>
Network fee
</div>
</div>
</div>
<div
class="c7"
>
<div>
<div
class="c2 c6 css-142zc9n"
>
$1.00
</div>
Network fee
</div>
<div
class="c9"
>
<div>
<div
class="c5 c7 css-142zc9n"
>
$1.00
</div>
</div>
</div>
</div>
<div
class="c2 css-142zc9n"
class="c2 c3 c4"
>
<div
class="c3 c4"
class="c5 c8 css-142zc9n"
data-testid="swap-li-label"
>
<div
class="c7"
>
<div>
<div
class="c2 c8 css-142zc9n"
cursor="help"
Price impact
</div>
<div
class="c9"
>
<div>
<div
class="c5 c7 css-142zc9n"
>
<span
class=""
>
Price impact
</div>
105566.373%
</span>
</div>
</div>
<div
class="c2 c6 css-142zc9n"
>
105566.373%
</div>
</div>
</div>
<div
class="c2 css-142zc9n"
class="c2 c3 c4"
>
<div
class="c3 c4"
class="c5 c8 css-142zc9n"
data-testid="swap-li-label"
>
<div
class="c7"
>
<div>
<div
class="c2 c8 css-142zc9n"
cursor="help"
>
Minimum received
</div>
Minimum output
</div>
<div
class="c9"
>
<div>
<div
class="c5 c7 css-142zc9n"
>
0.00000000000000098 DEF
</div>
</div>
<div
class="c2 c6 css-142zc9n"
>
0.00000000000000098 DEF
</div>
</div>
</div>
</div>
@ -346,31 +334,37 @@ exports[`SwapModalFooter.tsx matches base snapshot, test trade exact input 1`] =
exports[`SwapModalFooter.tsx renders a preview trade while disabling submission 1`] = `
<DocumentFragment>
.c3 {
.c2 {
box-sizing: border-box;
margin: 0;
min-width: 0;
}
.c4 {
.c3 {
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-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;
}
.c4 {
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
gap: 8px;
}
.c2 {
.c5 {
color: #222222;
}
@ -389,31 +383,43 @@ exports[`SwapModalFooter.tsx renders a preview trade while disabling submission
gap: 12px;
}
.c7 {
.c10 {
-webkit-animation: fAQEyV 1.5s infinite;
animation: fAQEyV 1.5s infinite;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
background: linear-gradient( to left, #FFFFFF 25%, #22222212 50%, #FFFFFF 75% );
background-size: 400%;
will-change: background-position;
border-radius: 12px;
height: 15px;
width: 50px;
}
.c9 {
display: inline-block;
height: inherit;
}
.c5 {
.c7 {
text-align: right;
overflow-wrap: break-word;
}
.c6 {
cursor: auto;
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) {
}
@ -422,102 +428,91 @@ exports[`SwapModalFooter.tsx renders a preview trade while disabling submission
class="c0 c1"
>
<div
class="c2 css-142zc9n"
class="c2 c3 c4"
>
<div
class="c3 c4"
class="c5 c6 css-142zc9n"
data-testid="swap-li-label"
>
<div
class="c2 c5 css-142zc9n"
>
Exchange rate
</div>
<div
class="c2 c6 css-142zc9n"
>
1 DEF = 1.00 ABC
</div>
Exchange rate
</div>
<div
class="c5 c7 css-142zc9n"
>
1 DEF = 1.00 ABC
</div>
</div>
<div
class="c2 css-142zc9n"
class="c2 c3 c4"
>
<div
class="c3 c4"
class="c5 c8 css-142zc9n"
data-testid="swap-li-label"
>
<div
class="c7"
>
<div>
Network fee
</div>
<div
class="c9"
>
<div>
<div
class="c5 c7 css-142zc9n"
>
<div
class="c2 c8 css-142zc9n"
cursor="help"
>
Network fees
</div>
</div>
</div>
<div
class="c7"
>
<div>
<div
class="c2 c6 css-142zc9n"
>
-
</div>
class="c10"
data-testid="loading-row"
height="15"
width="50"
/>
</div>
</div>
</div>
</div>
<div
class="c2 css-142zc9n"
class="c2 c3 c4"
>
<div
class="c3 c4"
class="c5 c8 css-142zc9n"
data-testid="swap-li-label"
>
<div
class="c7"
>
<div>
Price impact
</div>
<div
class="c9"
>
<div>
<div
class="c5 c7 css-142zc9n"
>
<div
class="c2 c8 css-142zc9n"
cursor="help"
>
Price impact
</div>
class="c10"
data-testid="loading-row"
height="15"
width="50"
/>
</div>
</div>
<div
class="c2 c6 css-142zc9n"
>
-
</div>
</div>
</div>
<div
class="c2 css-142zc9n"
class="c2 c3 c4"
>
<div
class="c3 c4"
class="c5 c8 css-142zc9n"
data-testid="swap-li-label"
>
<div
class="c7"
>
<div>
<div
class="c2 c8 css-142zc9n"
cursor="help"
>
Minimum received
</div>
Minimum output
</div>
<div
class="c9"
>
<div>
<div
class="c5 c7 css-142zc9n"
>
0.00000000000000098 DEF
</div>
</div>
<div
class="c2 c6 css-142zc9n"
>
0.00000000000000098 DEF
</div>
</div>
</div>
</div>

@ -0,0 +1,8 @@
import { useState } from 'react'
export default function useHoverProps(): [boolean, { onMouseEnter: () => void; onMouseLeave: () => void }] {
const [hover, setHover] = useState(false)
const hoverProps = { onMouseEnter: () => setHover(true), onMouseLeave: () => setHover(false) }
return [hover, hoverProps]
}

@ -3,6 +3,7 @@ import { ChainId, Currency, CurrencyAmount, Fraction, Percent, Price, Token, Tra
import { DutchOrderInfo, DutchOrderInfoJSON, DutchOrderTrade as IDutchOrderTrade } from '@uniswap/uniswapx-sdk'
import { Route as V2Route } from '@uniswap/v2-sdk'
import { Route as V3Route } from '@uniswap/v3-sdk'
import { ZERO_PERCENT } from 'constants/misc'
export enum TradeState {
LOADING = 'loading',
@ -280,6 +281,9 @@ export class DutchOrderTrade extends IDutchOrderTrade<Currency, Currency, TradeT
deadlineBufferSecs: number
slippageTolerance: Percent
inputTax = ZERO_PERCENT
outputTax = ZERO_PERCENT
constructor({
currencyIn,
currenciesOut,

@ -46,6 +46,9 @@ export const ThemedText = {
LabelMicro(props: TextProps) {
return <TextWrapper fontWeight={485} fontSize={12} color="neutral2" {...props} />
},
Caption(props: TextProps) {
return <TextWrapper fontWeight={485} fontSize={12} lineHeight="16px" color="neutral1" {...props} />
},
Link(props: TextProps) {
return <TextWrapper fontWeight={485} fontSize={14} color="accent1" {...props} />
},

@ -119,7 +119,7 @@ const SIX_SIG_FIGS_TWO_DECIMALS: NumberFormatOptions = {
minimumFractionDigits: 2,
}
export const SIX_SIG_FIGS_NO_COMMAS: NumberFormatOptions = {
const SIX_SIG_FIGS_NO_COMMAS: NumberFormatOptions = {
notation: 'standard',
maximumSignificantDigits: 6,
useGrouping: false,
@ -178,7 +178,7 @@ type FormatterBaseRule = { formatterOptions: NumberFormatOptions }
type FormatterExactRule = { upperBound?: undefined; exact: number } & FormatterBaseRule
type FormatterUpperBoundRule = { upperBound: number; exact?: undefined } & FormatterBaseRule
export type FormatterRule = (FormatterExactRule | FormatterUpperBoundRule) & { hardCodedInput?: HardCodedInputFormat }
type FormatterRule = (FormatterExactRule | FormatterUpperBoundRule) & { hardCodedInput?: HardCodedInputFormat }
// these formatter objects dictate which formatter rule to use based on the interval that
// the number falls into. for example, based on the rule set below, if your number
@ -215,6 +215,8 @@ const swapTradeAmountFormatter: FormatterRule[] = [
{ upperBound: Infinity, formatterOptions: SIX_SIG_FIGS_TWO_DECIMALS_NO_COMMAS },
]
const swapDetailsAmountFormatter: FormatterRule[] = [{ upperBound: Infinity, formatterOptions: SIX_SIG_FIGS_NO_COMMAS }]
const swapPriceFormatter: FormatterRule[] = [
{ exact: 0, formatterOptions: NO_DECIMALS },
{
@ -322,6 +324,8 @@ export enum NumberType {
// in the text input boxes. Output amounts on review screen should use the above TokenTx formatter
SwapTradeAmount = 'swap-trade-amount',
SwapDetailsAmount = 'swap-details-amount',
// fiat prices in any component that belongs in the Token Details flow (except for token stats)
FiatTokenDetails = 'fiat-token-details',
@ -356,6 +360,7 @@ const TYPE_TO_FORMATTER_RULES = {
[NumberType.TokenTx]: tokenTxFormatter,
[NumberType.SwapPrice]: swapPriceFormatter,
[NumberType.SwapTradeAmount]: swapTradeAmountFormatter,
[NumberType.SwapDetailsAmount]: swapDetailsAmountFormatter,
[NumberType.FiatTokenQuantity]: fiatTokenQuantityFormatter,
[NumberType.FiatTokenDetails]: fiatTokenDetailsFormatter,
[NumberType.FiatTokenPrice]: fiatTokenPricesFormatter,

@ -109,7 +109,7 @@ export function getPriceImpactWarning(priceImpact: Percent): 'warning' | 'error'
export function getPriceImpactColor(priceImpact: Percent): keyof DefaultTheme | undefined {
switch (getPriceImpactWarning(priceImpact)) {
case 'error':
return 'deprecated_accentFailureSoft'
return 'critical'
case 'warning':
return 'deprecated_accentWarning'
default: