Commit 82aaf078 authored by cartcrom's avatar cartcrom Committed by GitHub

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
parent 55a509ca
...@@ -32,6 +32,13 @@ export const LoadingRows = styled.div` ...@@ -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 }>` export const loadingOpacityMixin = css<{ $loading: boolean }>`
filter: ${({ $loading }) => ($loading ? 'grayscale(1)' : 'none')}; filter: ${({ $loading }) => ($loading ? 'grayscale(1)' : 'none')};
opacity: ${({ $loading }) => ($loading ? '0.6' : '1')}; opacity: ${({ $loading }) => ($loading ? '0.6' : '1')};
......
import { QuoteMethod, SubmittableTrade } from 'state/routing/types' import { QuoteMethod, SubmittableTrade } from 'state/routing/types'
import { isUniswapXTrade } from 'state/routing/utils' import { isUniswapXTrade } from 'state/routing/utils'
import { DefaultTheme } from 'styled-components'
import { ThemedText } from 'theme/components' import { ThemedText } from 'theme/components'
import UniswapXRouterLabel from './UniswapXRouterLabel' 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)) { if (isUniswapXTrade(trade)) {
return ( return (
<UniswapXRouterLabel> <UniswapXRouterLabel>
...@@ -13,7 +14,7 @@ export default function RouterLabel({ trade }: { trade: SubmittableTrade }) { ...@@ -13,7 +14,7 @@ export default function RouterLabel({ trade }: { trade: SubmittableTrade }) {
) )
} }
if (trade.quoteMethod === QuoteMethod.CLIENT_SIDE || trade.quoteMethod === QuoteMethod.CLIENT_SIDE_FALLBACK) { 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'> & ...@@ -77,9 +77,11 @@ type MouseoverTooltipProps = Omit<PopoverProps, 'content' | 'show'> &
timeout?: number timeout?: number
placement?: PopoverProps['placement'] placement?: PopoverProps['placement']
onOpen?: () => void 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 [show, setShow] = useState(false)
const open = () => { const open = () => {
setShow(true) setShow(true)
...@@ -101,7 +103,14 @@ export function MouseoverTooltip({ text, disabled, children, onOpen, timeout, .. ...@@ -101,7 +103,14 @@ export function MouseoverTooltip({ text, disabled, children, onOpen, timeout, ..
}, [timeout, show]) }, [timeout, show])
return ( 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}> <div onMouseEnter={disabled ? noop : open} onMouseLeave={disabled || timeout ? noop : close}>
{children} {children}
</div> </div>
......
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)
})
})
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>
)
}
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { Currency } from '@uniswap/sdk-core'
import { AutoColumn } from 'components/Column' 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 { nativeOnChain } from 'constants/tokens'
import { ReactNode } from 'react' import { ReactNode } from 'react'
import { SubmittableTrade } from 'state/routing/types' import { InterfaceTrade } from 'state/routing/types'
import { isClassicTrade, isUniswapXTrade } from 'state/routing/utils' import { isPreviewTrade, 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'
import { NumberType, useFormatter } from 'utils/formatNumbers' import { NumberType, useFormatter } from 'utils/formatNumbers'
...@@ -13,99 +15,83 @@ const Container = styled(AutoColumn)` ...@@ -13,99 +15,83 @@ const Container = styled(AutoColumn)`
padding: 4px; padding: 4px;
` `
const InlineLink = styled(ThemedText.BodySmall)` type GasCostItemProps = { title: ReactNode; itemValue?: React.ReactNode; amount?: number }
color: ${({ theme }) => theme.accent1};
display: inline;
cursor: pointer;
&:hover {
opacity: ${({ theme }) => theme.opacity.hover};
}
`
const InlineUniswapXGradient = styled(UniswapXGradient)` const GasCostItem = ({ title, amount, itemValue }: GasCostItemProps) => {
display: inline;
`
const GasCostItem = ({
title,
amount,
itemValue,
}: {
title: ReactNode
itemValue?: React.ReactNode
amount?: number
}) => {
const { formatNumber } = useFormatter() const { formatNumber } = useFormatter()
if (!amount && !itemValue) return null
const value = itemValue ?? formatNumber({ input: amount, type: NumberType.FiatGasPrice })
return ( return (
<Row justify="space-between"> <Row justify="space-between">
<ThemedText.SubHeaderSmall>{title}</ThemedText.SubHeaderSmall> <ThemedText.SubHeaderSmall>{title}</ThemedText.SubHeaderSmall>
<ThemedText.SubHeaderSmall color="neutral1"> <ThemedText.SubHeaderSmall color="neutral1">{value}</ThemedText.SubHeaderSmall>
{itemValue ??
formatNumber({
input: amount,
type: NumberType.FiatGasPrice,
})}
</ThemedText.SubHeaderSmall>
</Row> </Row>
) )
} }
export function GasBreakdownTooltip({ const GaslessSwapLabel = () => <UniswapXRouterLabel>$0</UniswapXRouterLabel>
trade,
hideFees = false, type GasBreakdownTooltipProps = { trade: InterfaceTrade; hideUniswapXDescription?: boolean }
hideUniswapXDescription = false,
}: { export function GasBreakdownTooltip({ trade, hideUniswapXDescription }: GasBreakdownTooltipProps) {
trade: SubmittableTrade const isUniswapX = isUniswapXTrade(trade)
hideFees?: boolean const inputCurrency = trade.inputAmount.currency
hideUniswapXDescription?: boolean const native = nativeOnChain(inputCurrency.chainId)
}) {
const swapEstimate = isClassicTrade(trade) ? trade.gasUseEstimateUSD : undefined if (isPreviewTrade(trade)) return <NetworkFeesDescription native={native} />
const swapEstimate = !isUniswapX ? trade.gasUseEstimateUSD : undefined
const approvalEstimate = trade.approveInfo.needsApprove ? trade.approveInfo.approveGasEstimateUSD : undefined const approvalEstimate = trade.approveInfo.needsApprove ? trade.approveInfo.approveGasEstimateUSD : undefined
const wrapEstimate = const wrapEstimate = isUniswapX && trade.wrapInfo.needsWrap ? trade.wrapInfo.wrapGasEstimateUSD : undefined
isUniswapXTrade(trade) && 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 ( return (
<Container gap="md"> <Container gap="md">
{(wrapEstimate || approvalEstimate) && !hideFees && ( <AutoColumn gap="sm">
<> <GasCostItem title={<Trans>Wrap {native.symbol}</Trans>} amount={wrapEstimate} />
<AutoColumn gap="sm"> <GasCostItem title={<Trans>Allow {inputCurrency.symbol} (one time)</Trans>} amount={approvalEstimate} />
{wrapEstimate && <GasCostItem title={<Trans>Wrap ETH</Trans>} amount={wrapEstimate} />} <GasCostItem title={<Trans>Swap</Trans>} amount={swapEstimate} />
{approvalEstimate && ( {isUniswapX && <GasCostItem title={<Trans>Swap</Trans>} itemValue={<GaslessSwapLabel />} />}
<GasCostItem </AutoColumn>
title={<Trans>Allow {trade.inputAmount.currency.symbol} (one time)</Trans>} <Divider />
amount={approvalEstimate} {description}
/>
)}
{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>
)}
</Container> </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' ...@@ -12,10 +12,11 @@ import { ChevronDown } from 'react-feather'
import { InterfaceTrade } from 'state/routing/types' import { InterfaceTrade } from 'state/routing/types'
import { isSubmittableTrade } from 'state/routing/utils' import { isSubmittableTrade } from 'state/routing/utils'
import styled, { useTheme } from 'styled-components' 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 GasEstimateTooltip from './GasEstimateTooltip'
import SwapLineItem, { SwapLineItemType } from './SwapLineItem'
import TradePrice from './TradePrice' import TradePrice from './TradePrice'
const StyledHeaderRow = styled(RowBetween)<{ disabled: boolean; open: boolean }>` const StyledHeaderRow = styled(RowBetween)<{ disabled: boolean; open: boolean }>`
...@@ -29,7 +30,7 @@ const RotatingArrow = styled(ChevronDown)<{ open?: boolean }>` ...@@ -29,7 +30,7 @@ const RotatingArrow = styled(ChevronDown)<{ open?: boolean }>`
transition: transform 0.1s linear; transition: transform 0.1s linear;
` `
const SwapDetailsWrapper = styled.div` const SwapDetailsWrapper = styled(Column)`
padding-top: ${({ theme }) => theme.grids.md}; padding-top: ${({ theme }) => theme.grids.md};
` `
...@@ -39,14 +40,15 @@ const Wrapper = styled(Column)` ...@@ -39,14 +40,15 @@ const Wrapper = styled(Column)`
padding: 12px 16px; padding: 12px 16px;
` `
interface SwapDetailsInlineProps { interface SwapDetailsProps {
trade?: InterfaceTrade trade?: InterfaceTrade
syncing: boolean syncing: boolean
loading: boolean loading: boolean
allowedSlippage: Percent 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 theme = useTheme()
const [showDetails, setShowDetails] = useState(false) const [showDetails, setShowDetails] = useState(false)
const trace = useTrace() const trace = useTrace()
...@@ -88,13 +90,33 @@ export default function SwapDetailsDropdown({ trade, syncing, loading, allowedSl ...@@ -88,13 +90,33 @@ export default function SwapDetailsDropdown({ trade, syncing, loading, allowedSl
</RowFixed> </RowFixed>
</StyledHeaderRow> </StyledHeaderRow>
</TraceEvent> </TraceEvent>
{trade && ( <AdvancedSwapDetails {...props} open={showDetails} />
<AnimatedDropdown open={showDetails}>
<SwapDetailsWrapper data-testid="advanced-swap-details">
<AdvancedSwapDetails trade={trade} allowedSlippage={allowedSlippage} syncing={syncing} />
</SwapDetailsWrapper>
</AnimatedDropdown>
)}
</Wrapper> </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>
)
}
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)
})
})
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)
import { import { PREVIEW_EXACT_IN_TRADE, TEST_ALLOWED_SLIPPAGE, TEST_TRADE_EXACT_INPUT } from 'test-utils/constants'
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 { render, screen, within } from 'test-utils/render' import { render, screen, within } from 'test-utils/render'
import SwapModalFooter from './SwapModalFooter' import SwapModalFooter from './SwapModalFooter'
...@@ -43,7 +34,7 @@ describe('SwapModalFooter.tsx', () => { ...@@ -43,7 +34,7 @@ describe('SwapModalFooter.tsx', () => {
) )
).toBeInTheDocument() ).toBeInTheDocument()
expect( 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() ).toBeInTheDocument()
expect(screen.getByText('The impact your trade has on the market price of this pool.')).toBeInTheDocument() expect(screen.getByText('The impact your trade has on the market price of this pool.')).toBeInTheDocument()
}) })
...@@ -77,99 +68,6 @@ describe('SwapModalFooter.tsx', () => { ...@@ -77,99 +68,6 @@ describe('SwapModalFooter.tsx', () => {
expect(within(showAcceptChanges).getByText('Accept')).toBeVisible() 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', () => { it('renders a preview trade while disabling submission', () => {
const { asFragment } = render( const { asFragment } = render(
<SwapModalFooter <SwapModalFooter
......
import { Plural, t, Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/analytics-events' import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/analytics-events'
import { Percent, TradeType } from '@uniswap/sdk-core' import { Percent } from '@uniswap/sdk-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 SpinningLoader from 'components/Loader/SpinningLoader'
import { MouseoverTooltip, TooltipSize } from 'components/Tooltip'
import { ZERO_PERCENT } from 'constants/misc'
import { SwapResult } from 'hooks/useSwapCallback' import { SwapResult } from 'hooks/useSwapCallback'
import useTransactionDeadline from 'hooks/useTransactionDeadline' import useTransactionDeadline from 'hooks/useTransactionDeadline'
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, PreviewTrade, RouterPreference } from 'state/routing/types' import { InterfaceTrade, RouterPreference } from 'state/routing/types'
import { getTransactionCount, isClassicTrade, isPreviewTrade, isSubmittableTrade } from 'state/routing/utils' import { isClassicTrade } 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, { useTheme } from 'styled-components'
import { ExternalLink, ThemedText } from 'theme/components' import { ThemedText } from 'theme/components'
import { FormatterRule, NumberType, SIX_SIG_FIGS_NO_COMMAS, useFormatter } from 'utils/formatNumbers'
import getRoutingDiagramEntries from 'utils/getRoutingDiagramEntries' import getRoutingDiagramEntries from 'utils/getRoutingDiagramEntries'
import { formatSwapButtonClickEventProperties } from 'utils/loggingFormatters' import { formatSwapButtonClickEventProperties } from 'utils/loggingFormatters'
import { getPriceImpactColor } from 'utils/prices'
import { ButtonError, SmallButtonPrimary } from '../Button' import { ButtonError, SmallButtonPrimary } from '../Button'
import Row, { AutoRow, RowBetween, RowFixed } from '../Row' import Row, { AutoRow, RowBetween, RowFixed } from '../Row'
import { GasBreakdownTooltip } from './GasBreakdownTooltip'
import { SwapCallbackError, SwapShowAcceptChanges } from './styled' import { SwapCallbackError, SwapShowAcceptChanges } from './styled'
import { Label } from './SwapModalHeaderAmount' import { SwapLineItemType } from './SwapLineItem'
import SwapLineItem from './SwapLineItem'
const sixFigsFormatterRules: FormatterRule[] = [{ upperBound: Infinity, formatterOptions: SIX_SIG_FIGS_NO_COMMAS }]
const DetailsContainer = styled(Column)` const DetailsContainer = styled(Column)`
padding: 0 8px; padding: 0 8px;
...@@ -44,12 +36,6 @@ const ConfirmButton = styled(ButtonError)` ...@@ -44,12 +36,6 @@ const ConfirmButton = styled(ButtonError)`
margin-top: 10px; 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({ export default function SwapModalFooter({
trade, trade,
allowedSlippage, allowedSlippage,
...@@ -80,116 +66,19 @@ export default function SwapModalFooter({ ...@@ -80,116 +66,19 @@ export default function SwapModalFooter({
const [routerPreference] = useRouterPreference() const [routerPreference] = useRouterPreference()
const routes = isClassicTrade(trade) ? getRoutingDiagramEntries(trade) : undefined const routes = isClassicTrade(trade) ? getRoutingDiagramEntries(trade) : undefined
const theme = useTheme() const theme = useTheme()
const { chainId } = useWeb3React()
const nativeCurrency = useNativeCurrency(chainId)
const { formatCurrencyAmount, formatNumber, formatPriceImpact } = useFormatter()
const label = `${trade.executionPrice.baseCurrency?.symbol} ` const lineItemProps = { trade, allowedSlippage, syncing: false }
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)
return ( return (
<> <>
<DetailsContainer gap="md"> <DetailsContainer gap="md">
<ThemedText.BodySmall> <SwapLineItem {...lineItemProps} type={SwapLineItemType.EXCHANGE_RATE} />
<Row align="flex-start" justify="space-between" gap="sm"> <SwapLineItem {...lineItemProps} type={SwapLineItemType.NETWORK_FEE} />
<Label> <SwapLineItem {...lineItemProps} type={SwapLineItemType.PRICE_IMPACT} />
<Trans>Exchange rate</Trans> <SwapLineItem {...lineItemProps} type={SwapLineItemType.INPUT_TOKEN_FEE_ON_TRANSFER} />
</Label> <SwapLineItem {...lineItemProps} type={SwapLineItemType.OUTPUT_TOKEN_FEE_ON_TRANSFER} />
<DetailRowValue>{`1 ${labelInverted} = ${formattedPrice ?? '-'} ${label}`}</DetailRowValue> <SwapLineItem {...lineItemProps} type={SwapLineItemType.MAXIMUM_INPUT} />
</Row> <SwapLineItem {...lineItemProps} type={SwapLineItemType.MINIMUM_OUTPUT} />
</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>
</DetailsContainer> </DetailsContainer>
{showAcceptChanges ? ( {showAcceptChanges ? (
<SwapShowAcceptChanges data-testid="show-accept-changes"> <SwapShowAcceptChanges data-testid="show-accept-changes">
...@@ -251,35 +140,3 @@ export default function SwapModalFooter({ ...@@ -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' ...@@ -12,7 +12,7 @@ import { BREAKPOINTS } from 'theme'
import { ThemedText } from 'theme/components' import { ThemedText } from 'theme/components'
import { NumberType, useFormatter } from 'utils/formatNumbers' import { NumberType, useFormatter } from 'utils/formatNumbers'
export const Label = styled(ThemedText.BodySmall)<{ cursor?: string }>` const Label = styled(ThemedText.BodySmall)<{ cursor?: string }>`
cursor: ${({ cursor }) => cursor}; cursor: ${({ cursor }) => cursor};
color: ${({ theme }) => theme.neutral2}; color: ${({ theme }) => theme.neutral2};
margin-right: 8px; margin-right: 8px;
......
...@@ -28,7 +28,7 @@ export default function SwapRoute({ trade, syncing }: { trade: ClassicTrade; syn ...@@ -28,7 +28,7 @@ export default function SwapRoute({ trade, syncing }: { trade: ClassicTrade; syn
return ( return (
<Column gap="md"> <Column gap="md">
<RouterLabel trade={trade} /> <RouterLabel trade={trade} color="neutral2" />
<Separator /> <Separator />
{syncing ? ( {syncing ? (
<LoadingRows> <LoadingRows>
...@@ -49,13 +49,13 @@ export default function SwapRoute({ trade, syncing }: { trade: ClassicTrade; syn ...@@ -49,13 +49,13 @@ export default function SwapRoute({ trade, syncing }: { trade: ClassicTrade; syn
<div style={{ width: '100%', height: '15px' }} /> <div style={{ width: '100%', height: '15px' }} />
</LoadingRows> </LoadingRows>
) : ( ) : (
<ThemedText.BodySmall color="neutral2"> <ThemedText.Caption color="neutral2">
{gasPrice ? <Trans>Best price route costs ~{gasPrice} in gas.</Trans> : null}{' '} {gasPrice ? <Trans>Best price route costs ~{gasPrice} in gas.</Trans> : null}{' '}
<Trans> <Trans>
This route optimizes your total output by considering split routes, multiple hops, and the gas cost of This route optimizes your total output by considering split routes, multiple hops, and the gas cost of
each step. each step.
</Trans> </Trans>
</ThemedText.BodySmall> </ThemedText.Caption>
)} )}
</> </>
)} )}
......
// 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`] = ` ...@@ -91,7 +91,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
justify-content: flex-start; justify-content: flex-start;
} }
.c17 { .c16 {
display: -webkit-box; display: -webkit-box;
display: -webkit-flex; display: -webkit-flex;
display: -ms-flexbox; display: -ms-flexbox;
...@@ -128,6 +128,16 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = ` ...@@ -128,6 +128,16 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
fill: #7D7D7D; fill: #7D7D7D;
} }
.c20 {
text-align: right;
overflow-wrap: break-word;
}
.c19 {
cursor: help;
color: #7D7D7D;
}
.c8 { .c8 {
background-color: transparent; background-color: transparent;
border: none; border: none;
...@@ -178,7 +188,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = ` ...@@ -178,7 +188,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
transition: transform 0.1s linear; transition: transform 0.1s linear;
} }
.c16 { .c17 {
padding-top: 12px; padding-top: 12px;
} }
...@@ -281,126 +291,121 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = ` ...@@ -281,126 +291,121 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
> >
<div> <div>
<div <div
class="c16" class="c16 c17"
data-testid="advanced-swap-details" data-testid="advanced-swap-details"
> >
<div <div
class="c17" class="c18"
/>
<div
class="c2 c3 c4"
> >
<div <div
class="c18" class="c9 c19 css-142zc9n"
/> data-testid="swap-li-label"
>
Network fee
</div>
<div <div
class="c2 c3 c4" class="c12"
> >
<div <div>
class="c12" <div
> class="c9 c20 css-142zc9n"
<div> >
<div $1.00
class="c14 css-142zc9n"
>
Network fee
</div>
</div>
</div>
<div
class="c12"
>
<div>
<div
class="c9 css-142zc9n"
>
~$1.00
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
<div
class="c2 c3 c4"
>
<div <div
class="c2 c3 c4" class="c9 c19 css-142zc9n"
data-testid="swap-li-label"
> >
<div Price impact
class="c12" </div>
> <div
<div> class="c12"
<div >
class="c14 css-142zc9n" <div>
<div
class="c9 c20 css-142zc9n"
>
<span
class=""
> >
Price Impact 105566.373%
</div> </span>
</div> </div>
</div> </div>
<div
class="c9 css-142zc9n"
>
105566.373%
</div>
</div> </div>
</div>
<div
class="c2 c3 c4"
>
<div <div
class="c2 c3 c4" class="c9 c19 css-142zc9n"
data-testid="swap-li-label"
> >
<div Minimum output
class="c2 c3 c6" </div>
> <div
class="c12"
>
<div>
<div <div
class="c12" class="c9 c20 css-142zc9n"
> >
<div> 0.00000000000000098 DEF
<div
class="c14 css-142zc9n"
>
Minimum output
</div>
</div>
</div> </div>
</div> </div>
<div
class="c9 css-142zc9n"
>
0.00000000000000098 DEF
</div>
</div> </div>
</div>
<div
class="c2 c3 c4"
>
<div <div
class="c2 c3 c4" class="c9 c19 css-142zc9n"
data-testid="swap-li-label"
> >
<div Expected output
class="c2 c3 c6" </div>
> <div
class="c12"
>
<div>
<div <div
class="c12" class="c9 c20 css-142zc9n"
> >
<div> 0.000000000000001 DEF
<div
class="c14 css-142zc9n"
>
Expected output
</div>
</div>
</div> </div>
</div> </div>
<div
class="c9 css-142zc9n"
>
0.000000000000001 DEF
</div>
</div> </div>
</div>
<div
class="c18"
/>
<div
class="c2 c3 c4"
>
<div <div
class="c18" class="c9 c19 css-142zc9n"
/> data-testid="swap-li-label"
>
Order routing
</div>
<div <div
class="c2 c3 c4" class="c12"
> >
<div <div>
class="c14 css-142zc9n" <div
> class="c9 c20 css-142zc9n"
Order routing >
</div>
<div
class="c12"
>
<div>
<div <div
class="c9 css-142zc9n" class="css-142zc9n"
> >
Uniswap Client Uniswap Client
</div> </div>
......
This diff is collapsed.
...@@ -2,31 +2,37 @@ ...@@ -2,31 +2,37 @@
exports[`SwapModalFooter.tsx matches base snapshot, test trade exact input 1`] = ` exports[`SwapModalFooter.tsx matches base snapshot, test trade exact input 1`] = `
<DocumentFragment> <DocumentFragment>
.c3 { .c2 {
box-sizing: border-box; box-sizing: border-box;
margin: 0; margin: 0;
min-width: 0; min-width: 0;
} }
.c4 { .c3 {
width: 100%; width: 100%;
display: -webkit-box; display: -webkit-box;
display: -webkit-flex; display: -webkit-flex;
display: -ms-flexbox; display: -ms-flexbox;
display: flex; display: flex;
padding: 0; padding: 0;
-webkit-align-items: flex-start; -webkit-align-items: center;
-webkit-box-align: flex-start; -webkit-box-align: center;
-ms-flex-align: flex-start; -ms-flex-align: center;
align-items: flex-start; 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-box-pack: justify;
-webkit-justify-content: space-between; -webkit-justify-content: space-between;
-ms-flex-pack: justify; -ms-flex-pack: justify;
justify-content: space-between; justify-content: space-between;
gap: 8px;
} }
.c2 { .c5 {
color: #222222; color: #222222;
} }
...@@ -45,131 +51,113 @@ exports[`SwapModalFooter.tsx matches base snapshot, test trade exact input 1`] = ...@@ -45,131 +51,113 @@ exports[`SwapModalFooter.tsx matches base snapshot, test trade exact input 1`] =
gap: 12px; gap: 12px;
} }
.c7 { .c9 {
display: inline-block; display: inline-block;
height: inherit; height: inherit;
} }
.c5 { .c7 {
text-align: right;
overflow-wrap: break-word;
}
.c6 {
cursor: auto;
color: #7D7D7D; color: #7D7D7D;
margin-right: 8px;
} }
.c8 { .c8 {
cursor: help; cursor: help;
color: #7D7D7D; color: #7D7D7D;
margin-right: 8px;
} }
.c1 { .c1 {
padding: 0 8px; padding: 0 8px;
} }
.c6 {
text-align: right;
overflow-wrap: break-word;
}
<div <div
class="c0 c1" class="c0 c1"
> >
<div <div
class="c2 css-142zc9n" class="c2 c3 c4"
> >
<div <div
class="c3 c4" class="c5 c6 css-142zc9n"
data-testid="swap-li-label"
> >
<div Exchange rate
class="c2 c5 css-142zc9n" </div>
> <div
Exchange rate class="c5 c7 css-142zc9n"
</div> >
<div 1 DEF = 1.00 ABC
class="c2 c6 css-142zc9n"
>
1 DEF = 1.00 ABC
</div>
</div> </div>
</div> </div>
<div <div
class="c2 css-142zc9n" class="c2 c3 c4"
> >
<div <div
class="c3 c4" class="c5 c8 css-142zc9n"
data-testid="swap-li-label"
> >
<div Network fee
class="c7" </div>
> <div
<div> class="c9"
<div >
class="c2 c8 css-142zc9n" <div>
cursor="help" <div
> class="c5 c7 css-142zc9n"
Network fee >
</div> $1.00
</div>
</div>
<div
class="c7"
>
<div>
<div
class="c2 c6 css-142zc9n"
>
$1.00
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div <div
class="c2 css-142zc9n" class="c2 c3 c4"
> >
<div <div
class="c3 c4" class="c5 c8 css-142zc9n"
data-testid="swap-li-label"
> >
<div Price impact
class="c7" </div>
> <div
<div> class="c9"
<div >
class="c2 c8 css-142zc9n" <div>
cursor="help" <div
class="c5 c7 css-142zc9n"
>
<span
class=""
> >
Price impact 105566.373%
</div> </span>
</div> </div>
</div> </div>
<div
class="c2 c6 css-142zc9n"
>
105566.373%
</div>
</div> </div>
</div> </div>
<div <div
class="c2 css-142zc9n" class="c2 c3 c4"
> >
<div <div
class="c3 c4" class="c5 c8 css-142zc9n"
data-testid="swap-li-label"
> >
<div Minimum output
class="c7" </div>
> <div
<div> class="c9"
<div >
class="c2 c8 css-142zc9n" <div>
cursor="help" <div
> class="c5 c7 css-142zc9n"
Minimum received >
</div> 0.00000000000000098 DEF
</div> </div>
</div> </div>
<div
class="c2 c6 css-142zc9n"
>
0.00000000000000098 DEF
</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -346,31 +334,37 @@ exports[`SwapModalFooter.tsx matches base snapshot, test trade exact input 1`] = ...@@ -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`] = ` exports[`SwapModalFooter.tsx renders a preview trade while disabling submission 1`] = `
<DocumentFragment> <DocumentFragment>
.c3 { .c2 {
box-sizing: border-box; box-sizing: border-box;
margin: 0; margin: 0;
min-width: 0; min-width: 0;
} }
.c4 { .c3 {
width: 100%; width: 100%;
display: -webkit-box; display: -webkit-box;
display: -webkit-flex; display: -webkit-flex;
display: -ms-flexbox; display: -ms-flexbox;
display: flex; display: flex;
padding: 0; padding: 0;
-webkit-align-items: flex-start; -webkit-align-items: center;
-webkit-box-align: flex-start; -webkit-box-align: center;
-ms-flex-align: flex-start; -ms-flex-align: center;
align-items: flex-start; 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-box-pack: justify;
-webkit-justify-content: space-between; -webkit-justify-content: space-between;
-ms-flex-pack: justify; -ms-flex-pack: justify;
justify-content: space-between; justify-content: space-between;
gap: 8px;
} }
.c2 { .c5 {
color: #222222; color: #222222;
} }
...@@ -389,31 +383,43 @@ exports[`SwapModalFooter.tsx renders a preview trade while disabling submission ...@@ -389,31 +383,43 @@ exports[`SwapModalFooter.tsx renders a preview trade while disabling submission
gap: 12px; 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; display: inline-block;
height: inherit; height: inherit;
} }
.c5 { .c7 {
text-align: right;
overflow-wrap: break-word;
}
.c6 {
cursor: auto;
color: #7D7D7D; color: #7D7D7D;
margin-right: 8px;
} }
.c8 { .c8 {
cursor: help; cursor: help;
color: #7D7D7D; color: #7D7D7D;
margin-right: 8px;
} }
.c1 { .c1 {
padding: 0 8px; padding: 0 8px;
} }
.c6 {
text-align: right;
overflow-wrap: break-word;
}
@media (max-width:960px) { @media (max-width:960px) {
} }
...@@ -422,102 +428,91 @@ exports[`SwapModalFooter.tsx renders a preview trade while disabling submission ...@@ -422,102 +428,91 @@ exports[`SwapModalFooter.tsx renders a preview trade while disabling submission
class="c0 c1" class="c0 c1"
> >
<div <div
class="c2 css-142zc9n" class="c2 c3 c4"
> >
<div <div
class="c3 c4" class="c5 c6 css-142zc9n"
data-testid="swap-li-label"
> >
<div Exchange rate
class="c2 c5 css-142zc9n" </div>
> <div
Exchange rate class="c5 c7 css-142zc9n"
</div> >
<div 1 DEF = 1.00 ABC
class="c2 c6 css-142zc9n"
>
1 DEF = 1.00 ABC
</div>
</div> </div>
</div> </div>
<div <div
class="c2 css-142zc9n" class="c2 c3 c4"
> >
<div <div
class="c3 c4" class="c5 c8 css-142zc9n"
data-testid="swap-li-label"
> >
<div Network fee
class="c7" </div>
> <div
<div> class="c9"
<div >
class="c2 c8 css-142zc9n" <div>
cursor="help" <div
> class="c5 c7 css-142zc9n"
Network fees >
</div>
</div>
</div>
<div
class="c7"
>
<div>
<div <div
class="c2 c6 css-142zc9n" class="c10"
> data-testid="loading-row"
- height="15"
</div> width="50"
/>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div <div
class="c2 css-142zc9n" class="c2 c3 c4"
> >
<div <div
class="c3 c4" class="c5 c8 css-142zc9n"
data-testid="swap-li-label"
> >
<div Price impact
class="c7" </div>
> <div
<div> class="c9"
>
<div>
<div
class="c5 c7 css-142zc9n"
>
<div <div
class="c2 c8 css-142zc9n" class="c10"
cursor="help" data-testid="loading-row"
> height="15"
Price impact width="50"
</div> />
</div> </div>
</div> </div>
<div
class="c2 c6 css-142zc9n"
>
-
</div>
</div> </div>
</div> </div>
<div <div
class="c2 css-142zc9n" class="c2 c3 c4"
> >
<div <div
class="c3 c4" class="c5 c8 css-142zc9n"
data-testid="swap-li-label"
> >
<div Minimum output
class="c7" </div>
> <div
<div> class="c9"
<div >
class="c2 c8 css-142zc9n" <div>
cursor="help" <div
> class="c5 c7 css-142zc9n"
Minimum received >
</div> 0.00000000000000098 DEF
</div> </div>
</div> </div>
<div
class="c2 c6 css-142zc9n"
>
0.00000000000000098 DEF
</div>
</div> </div>
</div> </div>
</div> </div>
......
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 ...@@ -3,6 +3,7 @@ import { ChainId, Currency, CurrencyAmount, Fraction, Percent, Price, Token, Tra
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'
import { ZERO_PERCENT } from 'constants/misc'
export enum TradeState { export enum TradeState {
LOADING = 'loading', LOADING = 'loading',
...@@ -280,6 +281,9 @@ export class DutchOrderTrade extends IDutchOrderTrade<Currency, Currency, TradeT ...@@ -280,6 +281,9 @@ export class DutchOrderTrade extends IDutchOrderTrade<Currency, Currency, TradeT
deadlineBufferSecs: number deadlineBufferSecs: number
slippageTolerance: Percent slippageTolerance: Percent
inputTax = ZERO_PERCENT
outputTax = ZERO_PERCENT
constructor({ constructor({
currencyIn, currencyIn,
currenciesOut, currenciesOut,
......
...@@ -46,6 +46,9 @@ export const ThemedText = { ...@@ -46,6 +46,9 @@ export const ThemedText = {
LabelMicro(props: TextProps) { LabelMicro(props: TextProps) {
return <TextWrapper fontWeight={485} fontSize={12} color="neutral2" {...props} /> 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) { Link(props: TextProps) {
return <TextWrapper fontWeight={485} fontSize={14} color="accent1" {...props} /> return <TextWrapper fontWeight={485} fontSize={14} color="accent1" {...props} />
}, },
......
...@@ -119,7 +119,7 @@ const SIX_SIG_FIGS_TWO_DECIMALS: NumberFormatOptions = { ...@@ -119,7 +119,7 @@ const SIX_SIG_FIGS_TWO_DECIMALS: NumberFormatOptions = {
minimumFractionDigits: 2, minimumFractionDigits: 2,
} }
export const SIX_SIG_FIGS_NO_COMMAS: NumberFormatOptions = { const SIX_SIG_FIGS_NO_COMMAS: NumberFormatOptions = {
notation: 'standard', notation: 'standard',
maximumSignificantDigits: 6, maximumSignificantDigits: 6,
useGrouping: false, useGrouping: false,
...@@ -178,7 +178,7 @@ type FormatterBaseRule = { formatterOptions: NumberFormatOptions } ...@@ -178,7 +178,7 @@ type FormatterBaseRule = { formatterOptions: NumberFormatOptions }
type FormatterExactRule = { upperBound?: undefined; exact: number } & FormatterBaseRule type FormatterExactRule = { upperBound?: undefined; exact: number } & FormatterBaseRule
type FormatterUpperBoundRule = { upperBound: number; exact?: undefined } & 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 // 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 // the number falls into. for example, based on the rule set below, if your number
...@@ -215,6 +215,8 @@ const swapTradeAmountFormatter: FormatterRule[] = [ ...@@ -215,6 +215,8 @@ const swapTradeAmountFormatter: FormatterRule[] = [
{ upperBound: Infinity, formatterOptions: SIX_SIG_FIGS_TWO_DECIMALS_NO_COMMAS }, { upperBound: Infinity, formatterOptions: SIX_SIG_FIGS_TWO_DECIMALS_NO_COMMAS },
] ]
const swapDetailsAmountFormatter: FormatterRule[] = [{ upperBound: Infinity, formatterOptions: SIX_SIG_FIGS_NO_COMMAS }]
const swapPriceFormatter: FormatterRule[] = [ const swapPriceFormatter: FormatterRule[] = [
{ exact: 0, formatterOptions: NO_DECIMALS }, { exact: 0, formatterOptions: NO_DECIMALS },
{ {
...@@ -322,6 +324,8 @@ export enum NumberType { ...@@ -322,6 +324,8 @@ export enum NumberType {
// in the text input boxes. Output amounts on review screen should use the above TokenTx formatter // in the text input boxes. Output amounts on review screen should use the above TokenTx formatter
SwapTradeAmount = 'swap-trade-amount', SwapTradeAmount = 'swap-trade-amount',
SwapDetailsAmount = 'swap-details-amount',
// fiat prices in any component that belongs in the Token Details flow (except for token stats) // fiat prices in any component that belongs in the Token Details flow (except for token stats)
FiatTokenDetails = 'fiat-token-details', FiatTokenDetails = 'fiat-token-details',
...@@ -356,6 +360,7 @@ const TYPE_TO_FORMATTER_RULES = { ...@@ -356,6 +360,7 @@ const TYPE_TO_FORMATTER_RULES = {
[NumberType.TokenTx]: tokenTxFormatter, [NumberType.TokenTx]: tokenTxFormatter,
[NumberType.SwapPrice]: swapPriceFormatter, [NumberType.SwapPrice]: swapPriceFormatter,
[NumberType.SwapTradeAmount]: swapTradeAmountFormatter, [NumberType.SwapTradeAmount]: swapTradeAmountFormatter,
[NumberType.SwapDetailsAmount]: swapDetailsAmountFormatter,
[NumberType.FiatTokenQuantity]: fiatTokenQuantityFormatter, [NumberType.FiatTokenQuantity]: fiatTokenQuantityFormatter,
[NumberType.FiatTokenDetails]: fiatTokenDetailsFormatter, [NumberType.FiatTokenDetails]: fiatTokenDetailsFormatter,
[NumberType.FiatTokenPrice]: fiatTokenPricesFormatter, [NumberType.FiatTokenPrice]: fiatTokenPricesFormatter,
......
...@@ -109,7 +109,7 @@ export function getPriceImpactWarning(priceImpact: Percent): 'warning' | 'error' ...@@ -109,7 +109,7 @@ export function getPriceImpactWarning(priceImpact: Percent): 'warning' | 'error'
export function getPriceImpactColor(priceImpact: Percent): keyof DefaultTheme | undefined { export function getPriceImpactColor(priceImpact: Percent): keyof DefaultTheme | undefined {
switch (getPriceImpactWarning(priceImpact)) { switch (getPriceImpactWarning(priceImpact)) {
case 'error': case 'error':
return 'deprecated_accentFailureSoft' return 'critical'
case 'warning': case 'warning':
return 'deprecated_accentWarning' return 'deprecated_accentWarning'
default: default:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment