Commit 0050b1e1 authored by Mike Grabowski's avatar Mike Grabowski Committed by GitHub

feat: new routing diagram (#6510)

* chore: initial commit

* chore: add todo to refactor and simplify if conditional in the future

* chore: update layout

* chore: ui tweaks

* chore: add todo

* chore: change todo

* chore: update UI

* chore: tmp

* feat: rename router preference

* chore: update type

* fix error

* fix one more issue

* finish UI work

* chore: remove unecessary components

* chore: update non-snapshot unit tests

* chore: fix lint

* chore: fix ts

* chore: one more time

* fix

* chore: update snapshots

* chore: add jira tickets

* chore: fix mobile popovers

* chore: add analytics event

* chore: fix padding and send event

* chore: fix loading state

* oops

* chore: address review

* chore: comment

* chore
parent 5bf33ab0
...@@ -95,7 +95,7 @@ export const ChainSelector = ({ leftAlign }: ChainSelectorProps) => { ...@@ -95,7 +95,7 @@ export const ChainSelector = ({ leftAlign }: ChainSelectorProps) => {
return ( return (
<Box position="relative" ref={ref}> <Box position="relative" ref={ref}>
<MouseoverTooltip text={t`Your wallet's current network is unsupported.`} disableHover={isSupported}> <MouseoverTooltip text={t`Your wallet's current network is unsupported.`} disabled={isSupported}>
<Row <Row
as="button" as="button"
gap="8" gap="8"
......
...@@ -99,8 +99,8 @@ export default function Popover({ ...@@ -99,8 +99,8 @@ export default function Popover({
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null) const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null)
const [arrowElement, setArrowElement] = useState<HTMLDivElement | null>(null) const [arrowElement, setArrowElement] = useState<HTMLDivElement | null>(null)
const options = useMemo( const options: Options = useMemo(
(): Options => ({ () => ({
placement, placement,
strategy: 'fixed', strategy: 'fixed',
modifiers: [ modifiers: [
...@@ -109,7 +109,7 @@ export default function Popover({ ...@@ -109,7 +109,7 @@ export default function Popover({
{ name: 'preventOverflow', options: { padding: 8 } }, { name: 'preventOverflow', options: { padding: 8 } },
], ],
}), }),
[arrowElement, offsetX, offsetY, placement] [placement, offsetX, offsetY, arrowElement]
) )
const { styles, update, attributes } = usePopper(referenceElement, popperElement, options) const { styles, update, attributes } = usePopper(referenceElement, popperElement, options)
......
import { Protocol } from '@uniswap/router-sdk' import { Protocol } from '@uniswap/router-sdk'
import { Currency, Percent } from '@uniswap/sdk-core' import { Currency, Percent } from '@uniswap/sdk-core'
import { FeeAmount } from '@uniswap/v3-sdk' import { FeeAmount } from '@uniswap/v3-sdk'
import { RoutingDiagramEntry } from 'components/swap/SwapRoute'
import { DAI, USDC_MAINNET, WBTC } from 'constants/tokens' import { DAI, USDC_MAINNET, WBTC } from 'constants/tokens'
import { render } from 'test-utils/render' import { render } from 'test-utils/render'
import { RoutingDiagramEntry } from 'utils/getRoutingDiagramEntries'
import RoutingDiagram from './RoutingDiagram' import RoutingDiagram from './RoutingDiagram'
......
...@@ -6,12 +6,12 @@ import Badge from 'components/Badge' ...@@ -6,12 +6,12 @@ import Badge from 'components/Badge'
import DoubleCurrencyLogo from 'components/DoubleLogo' import DoubleCurrencyLogo from 'components/DoubleLogo'
import CurrencyLogo from 'components/Logo/CurrencyLogo' import CurrencyLogo from 'components/Logo/CurrencyLogo'
import Row, { AutoRow } from 'components/Row' import Row, { AutoRow } from 'components/Row'
import { RoutingDiagramEntry } from 'components/swap/SwapRoute'
import { useTokenInfoFromActiveList } from 'hooks/useTokenInfoFromActiveList' import { useTokenInfoFromActiveList } from 'hooks/useTokenInfoFromActiveList'
import { Box } from 'rebass' import { Box } from 'rebass'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { ThemedText } from 'theme' import { ThemedText } from 'theme'
import { Z_INDEX } from 'theme/zIndex' import { Z_INDEX } from 'theme/zIndex'
import { RoutingDiagramEntry } from 'utils/getRoutingDiagramEntries'
import { ReactComponent as DotLine } from '../../assets/svg/dot_line.svg' import { ReactComponent as DotLine } from '../../assets/svg/dot_line.svg'
import { MouseoverTooltip } from '../Tooltip' import { MouseoverTooltip } from '../Tooltip'
......
import { transparentize } from 'polished' import { transparentize } from 'polished'
import { ReactNode, useEffect, useState } from 'react' import { PropsWithChildren, ReactNode, useEffect, useState } from 'react'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import noop from 'utils/noop' import noop from 'utils/noop'
import Popover, { PopoverProps } from '../Popover' import Popover, { PopoverProps } from '../Popover'
export const TooltipContainer = styled.div` export enum TooltipSize {
max-width: 256px; Small = '256px',
Large = '400px',
}
const getPaddingForSize = (size: TooltipSize) => {
switch (size) {
case TooltipSize.Small:
return '12px'
case TooltipSize.Large:
return '16px 20px'
}
}
const TooltipContainer = styled.div<{ size: TooltipSize }>`
max-width: ${({ size }) => size};
width: calc(100vw - 16px);
cursor: default; cursor: default;
padding: 0.6rem 1rem; padding: ${({ size }) => getPaddingForSize(size)};
pointer-events: auto; pointer-events: auto;
color: ${({ theme }) => theme.textPrimary}; color: ${({ theme }) => theme.textPrimary};
...@@ -23,30 +38,23 @@ export const TooltipContainer = styled.div` ...@@ -23,30 +38,23 @@ export const TooltipContainer = styled.div`
box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.9, theme.shadow1)}; box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.9, theme.shadow1)};
` `
interface TooltipProps extends Omit<PopoverProps, 'content'> { type TooltipProps = Omit<PopoverProps, 'content'> & {
text: ReactNode text: ReactNode
open?: () => void open?: () => void
close?: () => void close?: () => void
disableHover?: boolean // disable the hover and content display size?: TooltipSize
disabled?: boolean
timeout?: number timeout?: number
} }
interface TooltipContentProps extends Omit<PopoverProps, 'content'> { // TODO(WEB-3305)
content: ReactNode // Migrate to MouseoverTooltip and move this component inline to MouseoverTooltip
onOpen?: () => void export default function Tooltip({ text, open, close, disabled, size = TooltipSize.Small, ...rest }: TooltipProps) {
open?: () => void
close?: () => void
// whether to wrap the content in a `TooltipContainer`
wrap?: boolean
disableHover?: boolean // disable the hover and content display
}
export default function Tooltip({ text, open, close, disableHover, ...rest }: TooltipProps) {
return ( return (
<Popover <Popover
content={ content={
text && ( text && (
<TooltipContainer onMouseEnter={disableHover ? noop : open} onMouseLeave={disableHover ? noop : close}> <TooltipContainer size={size} onMouseEnter={disabled ? noop : open} onMouseLeave={disabled ? noop : close}>
{text} {text}
</TooltipContainer> </TooltipContainer>
) )
...@@ -56,27 +64,24 @@ export default function Tooltip({ text, open, close, disableHover, ...rest }: To ...@@ -56,27 +64,24 @@ export default function Tooltip({ text, open, close, disableHover, ...rest }: To
) )
} }
function TooltipContent({ content, wrap = false, open, close, disableHover, ...rest }: TooltipContentProps) { // TODO(WEB-3305)
return ( // Do not pass through PopoverProps. Prefer higher-level interface to control MouseoverTooltip.
<Popover type MouseoverTooltipProps = Omit<PopoverProps, 'content' | 'show'> &
content={ PropsWithChildren<{
wrap ? ( text: ReactNode
<TooltipContainer onMouseEnter={disableHover ? noop : open} onMouseLeave={disableHover ? noop : close}> size?: TooltipSize
{content} disabled?: boolean
</TooltipContainer> timeout?: number
) : ( placement?: PopoverProps['placement']
content onOpen?: () => void
) }>
}
{...rest}
/>
)
}
/** Standard text tooltip. */ export function MouseoverTooltip({ text, disabled, children, onOpen, timeout, ...rest }: MouseoverTooltipProps) {
export function MouseoverTooltip({ text, disableHover, children, timeout, ...rest }: Omit<TooltipProps, 'show'>) {
const [show, setShow] = useState(false) const [show, setShow] = useState(false)
const open = () => text && setShow(true) const open = () => {
setShow(true)
onOpen?.()
}
const close = () => setShow(false) const close = () => setShow(false)
useEffect(() => { useEffect(() => {
...@@ -93,49 +98,10 @@ export function MouseoverTooltip({ text, disableHover, children, timeout, ...res ...@@ -93,49 +98,10 @@ export function MouseoverTooltip({ text, disableHover, children, timeout, ...res
}, [timeout, show]) }, [timeout, show])
return ( return (
<Tooltip <Tooltip {...rest} open={open} close={close} disabled={disabled} show={show} text={disabled ? null : text}>
{...rest} <div onMouseEnter={disabled ? noop : open} onMouseLeave={disabled || timeout ? noop : close}>
open={open}
close={close}
disableHover={disableHover}
show={show}
text={disableHover ? null : text}
>
<div onMouseEnter={disableHover ? noop : open} onMouseLeave={disableHover || timeout ? noop : close}>
{children} {children}
</div> </div>
</Tooltip> </Tooltip>
) )
} }
/** Tooltip that displays custom content. */
export function MouseoverTooltipContent({
content,
children,
onOpen: openCallback = undefined,
disableHover,
...rest
}: Omit<TooltipContentProps, 'show'>) {
const [show, setShow] = useState(false)
const open = () => {
setShow(true)
openCallback?.()
}
const close = () => {
setShow(false)
}
return (
<TooltipContent
{...rest}
open={open}
close={close}
show={!disableHover && show}
content={disableHover ? null : content}
>
<div onMouseEnter={open} onMouseLeave={close}>
{children}
</div>
</TooltipContent>
)
}
...@@ -20,20 +20,18 @@ describe('AdvancedSwapDetails.tsx', () => { ...@@ -20,20 +20,18 @@ describe('AdvancedSwapDetails.tsx', () => {
it('renders correct copy on mouseover', async () => { it('renders correct copy on mouseover', async () => {
render(<AdvancedSwapDetails trade={TEST_TRADE_EXACT_INPUT} allowedSlippage={TEST_ALLOWED_SLIPPAGE} />) render(<AdvancedSwapDetails trade={TEST_TRADE_EXACT_INPUT} allowedSlippage={TEST_ALLOWED_SLIPPAGE} />)
await act(() => userEvent.hover(screen.getByText('Price Impact'))) await act(() => userEvent.hover(screen.getByText('Expected output')))
expect(await screen.getByText(/The impact your trade has on the market price of this pool./i)).toBeVisible()
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() expect(await screen.getByText(/The amount you expect to receive at the current market price./i)).toBeVisible()
await act(() => userEvent.hover(screen.getByText(/Minimum received/i))) await act(() => userEvent.hover(screen.getByText(/Minimum output/i)))
expect(await screen.getByText(/The minimum amount you are guaranteed to receive./i)).toBeVisible() 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 () => { it('renders correct tooltips for test trade with exact output and gas use estimate USD', async () => {
TEST_TRADE_EXACT_OUTPUT.gasUseEstimateUSD = toCurrencyAmount(TEST_TOKEN_1, 1) TEST_TRADE_EXACT_OUTPUT.gasUseEstimateUSD = toCurrencyAmount(TEST_TOKEN_1, 1)
render(<AdvancedSwapDetails trade={TEST_TRADE_EXACT_OUTPUT} allowedSlippage={TEST_ALLOWED_SLIPPAGE} />) render(<AdvancedSwapDetails trade={TEST_TRADE_EXACT_OUTPUT} allowedSlippage={TEST_ALLOWED_SLIPPAGE} />)
await act(() => userEvent.hover(screen.getByText(/Maximum sent/i))) await act(() => userEvent.hover(screen.getByText(/Minimum output/i)))
expect(await screen.getByText(/The minimum amount you are guaranteed to receive./i)).toBeVisible() expect(await screen.getByText(/The minimum amount you are guaranteed to receive./i)).toBeVisible()
await act(() => userEvent.hover(screen.getByText('Network Fee'))) await act(() => userEvent.hover(screen.getByText('Network fee')))
expect(await screen.getByText(/The fee paid to miners who process your transaction./i)).toBeVisible() expect(await screen.getByText(/The fee paid to miners who process your transaction./i)).toBeVisible()
}) })
......
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { InterfaceElementName, SwapEventName } from '@uniswap/analytics-events'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core' import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import Card from 'components/Card'
import { LoadingRows } from 'components/Loader/styled' import { LoadingRows } from 'components/Loader/styled'
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains' import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
import useNativeCurrency from 'lib/hooks/useNativeCurrency' import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { useMemo } from 'react'
import { InterfaceTrade } from 'state/routing/types' import { InterfaceTrade } from 'state/routing/types'
import styled, { useTheme } from 'styled-components/macro'
import { Separator, ThemedText } from '../../theme' import { Separator, ThemedText } from '../../theme'
import { computeRealizedPriceImpact } from '../../utils/prices' import Column from '../Column'
import { AutoColumn } from '../Column'
import { RowBetween, RowFixed } from '../Row' import { RowBetween, RowFixed } from '../Row'
import { MouseoverTooltip } from '../Tooltip' import { MouseoverTooltip, TooltipSize } from '../Tooltip'
import FormattedPriceImpact from './FormattedPriceImpact' import RouterLabel from './RouterLabel'
import SwapRoute from './SwapRoute'
const StyledCard = styled(Card)`
padding: 0;
`
interface AdvancedSwapDetailsProps { interface AdvancedSwapDetailsProps {
trade?: InterfaceTrade<Currency, Currency, TradeType> trade: InterfaceTrade<Currency, Currency, TradeType>
allowedSlippage: Percent allowedSlippage: Percent
syncing?: boolean syncing?: boolean
hideInfoTooltips?: boolean
} }
function TextWithLoadingPlaceholder({ function TextWithLoadingPlaceholder({
...@@ -45,119 +39,92 @@ function TextWithLoadingPlaceholder({ ...@@ -45,119 +39,92 @@ function TextWithLoadingPlaceholder({
) )
} }
export function AdvancedSwapDetails({ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }: AdvancedSwapDetailsProps) {
trade,
allowedSlippage,
syncing = false,
hideInfoTooltips = false,
}: AdvancedSwapDetailsProps) {
const theme = useTheme()
const { chainId } = useWeb3React() const { chainId } = useWeb3React()
const nativeCurrency = useNativeCurrency(chainId) const nativeCurrency = useNativeCurrency(chainId)
const { expectedOutputAmount, priceImpact } = useMemo(() => { return (
return { <Column gap="md">
expectedOutputAmount: trade?.outputAmount, <Separator />
priceImpact: trade ? computeRealizedPriceImpact(trade) : undefined, {!trade.gasUseEstimateUSD || !chainId || !SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) ? null : (
}
}, [trade])
return !trade ? null : (
<StyledCard>
<AutoColumn gap="sm">
<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>
}
disableHover={hideInfoTooltips}
>
<ThemedText.DeprecatedSubHeader color={theme.textPrimary}>
<Trans>Expected Output</Trans>
</ThemedText.DeprecatedSubHeader>
</MouseoverTooltip>
</RowFixed>
<TextWithLoadingPlaceholder syncing={syncing} width={65}>
<ThemedText.DeprecatedBlack textAlign="right" fontSize={14}>
{expectedOutputAmount
? `${expectedOutputAmount.toSignificant(6)} ${expectedOutputAmount.currency.symbol}`
: '-'}
</ThemedText.DeprecatedBlack>
</TextWithLoadingPlaceholder>
</RowBetween>
<RowBetween> <RowBetween>
<RowFixed> <MouseoverTooltip
<MouseoverTooltip text={
text={<Trans>The impact your trade has on the market price of this pool.</Trans>} <Trans>
disableHover={hideInfoTooltips} The fee paid to miners who process your transaction. This must be paid in {nativeCurrency.symbol}.
> </Trans>
<ThemedText.DeprecatedSubHeader color={theme.textPrimary}> }
<Trans>Price Impact</Trans> >
</ThemedText.DeprecatedSubHeader> <ThemedText.BodySmall color="textSecondary">
</MouseoverTooltip> <Trans>Network fee</Trans>
</RowFixed> </ThemedText.BodySmall>
</MouseoverTooltip>
<TextWithLoadingPlaceholder syncing={syncing} width={50}> <TextWithLoadingPlaceholder syncing={syncing} width={50}>
<ThemedText.DeprecatedBlack textAlign="right" fontSize={14}> <ThemedText.BodySmall>~${trade.gasUseEstimateUSD.toFixed(2)}</ThemedText.BodySmall>
<FormattedPriceImpact priceImpact={priceImpact} />
</ThemedText.DeprecatedBlack>
</TextWithLoadingPlaceholder>
</RowBetween>
<Separator />
<RowBetween>
<RowFixed style={{ marginRight: '20px' }}>
<MouseoverTooltip
text={
<Trans>
The minimum amount you are guaranteed to receive. If the price slips any further, your transaction
will revert.
</Trans>
}
disableHover={hideInfoTooltips}
>
<ThemedText.DeprecatedSubHeader color={theme.textTertiary}>
{trade.tradeType === TradeType.EXACT_INPUT ? (
<Trans>Minimum received</Trans>
) : (
<Trans>Maximum sent</Trans>
)}{' '}
<Trans>after slippage</Trans> ({allowedSlippage.toFixed(2)}%)
</ThemedText.DeprecatedSubHeader>
</MouseoverTooltip>
</RowFixed>
<TextWithLoadingPlaceholder syncing={syncing} width={70}>
<ThemedText.DeprecatedBlack textAlign="right" fontSize={14} color={theme.textTertiary}>
{trade.tradeType === TradeType.EXACT_INPUT
? `${trade.minimumAmountOut(allowedSlippage).toSignificant(6)} ${trade.outputAmount.currency.symbol}`
: `${trade.maximumAmountIn(allowedSlippage).toSignificant(6)} ${trade.inputAmount.currency.symbol}`}
</ThemedText.DeprecatedBlack>
</TextWithLoadingPlaceholder> </TextWithLoadingPlaceholder>
</RowBetween> </RowBetween>
{!trade?.gasUseEstimateUSD || !chainId || !SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) ? null : ( )}
<RowBetween> <RowBetween>
<MouseoverTooltip <RowFixed>
text={ <MouseoverTooltip
<Trans> text={
The fee paid to miners who process your transaction. This must be paid in {nativeCurrency.symbol}. <Trans>
</Trans> The minimum amount you are guaranteed to receive. If the price slips any further, your transaction will
} revert.
disableHover={hideInfoTooltips} </Trans>
> }
<ThemedText.DeprecatedSubHeader color={theme.textTertiary}> >
<Trans>Network Fee</Trans> <ThemedText.BodySmall color="textSecondary">
</ThemedText.DeprecatedSubHeader> <Trans>Minimum output</Trans>
</MouseoverTooltip> </ThemedText.BodySmall>
<TextWithLoadingPlaceholder syncing={syncing} width={50}> </MouseoverTooltip>
<ThemedText.DeprecatedBlack textAlign="right" fontSize={14} color={theme.textTertiary}> </RowFixed>
~${trade.gasUseEstimateUSD.toFixed(2)} <TextWithLoadingPlaceholder syncing={syncing} width={70}>
</ThemedText.DeprecatedBlack> <ThemedText.BodySmall>
</TextWithLoadingPlaceholder> {trade.tradeType === TradeType.EXACT_INPUT
</RowBetween> ? `${trade.minimumAmountOut(allowedSlippage).toSignificant(6)} ${trade.outputAmount.currency.symbol}`
)} : `${trade.maximumAmountIn(allowedSlippage).toSignificant(6)} ${trade.inputAmount.currency.symbol}`}
</AutoColumn> </ThemedText.BodySmall>
</StyledCard> </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="textSecondary">
<Trans>Expected output</Trans>
</ThemedText.BodySmall>
</MouseoverTooltip>
</RowFixed>
<TextWithLoadingPlaceholder syncing={syncing} width={65}>
<ThemedText.BodySmall>
{`${trade.outputAmount.toSignificant(6)} ${trade.outputAmount.currency.symbol}`}
</ThemedText.BodySmall>
</TextWithLoadingPlaceholder>
</RowBetween>
<Separator />
<RowBetween>
<ThemedText.BodySmall color="textSecondary">
<Trans>Order routing</Trans>
</ThemedText.BodySmall>
<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 />
</MouseoverTooltip>
</RowBetween>
</Column>
) )
} }
import { useRef } from 'react'
let uniqueId = 0
const getUniqueId = () => uniqueId++
export default function AutoRouterIcon({ className, id }: { className?: string; id?: string }) {
const componentIdRef = useRef(id ?? getUniqueId())
const componentId = `AutoRouterIconGradient${componentIdRef.current}`
return (
<svg
width="23"
height="20"
viewBox="0 0 23 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<defs>
<linearGradient id={componentId} x1="0" y1="0" x2="1" y2="0" gradientTransform="rotate(95)">
<stop id="stop1" offset="0" stopColor="#2274E2" />
<stop id="stop1" offset="0.5" stopColor="#2274E2" />
<stop id="stop2" offset="1" stopColor="#3FB672" />
</linearGradient>
</defs>
<path
d="M16 16C10 16 9 10 5 10M16 16C16 17.6569 17.3431 19 19 19C20.6569 19 22 17.6569 22 16C22 14.3431 20.6569 13 19 13C17.3431 13 16 14.3431 16 16ZM5 10C9 10 10 4 16 4M5 10H1.5M16 4C16 5.65685 17.3431 7 19 7C20.6569 7 22 5.65685 22 4C22 2.34315 20.6569 1 19 1C17.3431 1 16 2.34315 16 4Z"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
stroke={`url(#${componentId})`}
/>
</svg>
)
}
import { Percent } from '@uniswap/sdk-core'
import { warningSeverity } from '../../utils/prices'
import { ErrorText } from './styleds'
export const formatPriceImpact = (priceImpact: Percent) => `${priceImpact.multiply(-1).toFixed(2)}%`
/**
* Formatted version of price impact text with warning colors
*/
export default function FormattedPriceImpact({ priceImpact }: { priceImpact?: Percent }) {
return (
<ErrorText fontWeight={500} fontSize={14} severity={warningSeverity(priceImpact)}>
{priceImpact ? formatPriceImpact(priceImpact) : '-'}
</ErrorText>
)
}
import { Trans } from '@lingui/macro'
import { Currency, TradeType } from '@uniswap/sdk-core'
import { sendEvent } from 'components/analytics'
import { AutoColumn } from 'components/Column'
import { LoadingOpacityContainer } from 'components/Loader/styled'
import { RowFixed } from 'components/Row'
import { MouseoverTooltipContent } from 'components/Tooltip'
import { InterfaceTrade } from 'state/routing/types'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import { ReactComponent as GasIcon } from '../../assets/images/gas-icon.svg'
import { ResponsiveTooltipContainer } from './styleds'
import SwapRoute from './SwapRoute'
const GasWrapper = styled(RowFixed)`
border-radius: 8px;
padding: 4px 6px;
height: 24px;
color: ${({ theme }) => theme.textTertiary};
background-color: ${({ theme }) => theme.deprecated_bg1};
font-size: 14px;
font-weight: 500;
user-select: none;
`
const StyledGasIcon = styled(GasIcon)`
margin-right: 4px;
height: 14px;
& > * {
stroke: ${({ theme }) => theme.textTertiary};
}
`
export default function GasEstimateBadge({
trade,
loading,
showRoute,
disableHover,
}: {
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined | null // dollar amount in active chain's stablecoin
loading: boolean
showRoute?: boolean // show route instead of gas estimation summary
disableHover?: boolean
}) {
const formattedGasPriceString = trade?.gasUseEstimateUSD
? trade.gasUseEstimateUSD.toFixed(2) === '0.00'
? '<$0.01'
: '$' + trade.gasUseEstimateUSD.toFixed(2)
: undefined
return (
<MouseoverTooltipContent
wrap={false}
disableHover={disableHover}
content={
loading ? null : (
<ResponsiveTooltipContainer
origin="top right"
style={{
padding: showRoute ? '0' : '12px',
border: 'none',
borderRadius: showRoute ? '16px' : '12px',
maxWidth: '400px',
}}
>
{showRoute ? (
trade ? (
<SwapRoute trade={trade} syncing={loading} fixedOpen={showRoute} />
) : null
) : (
<AutoColumn gap="4px" justify="center">
<ThemedText.DeprecatedMain fontSize="12px" textAlign="center">
<Trans>Estimated network fee</Trans>
</ThemedText.DeprecatedMain>
<ThemedText.DeprecatedBody textAlign="center" fontWeight={500} style={{ userSelect: 'none' }}>
<Trans>${trade?.gasUseEstimateUSD?.toFixed(2)}</Trans>
</ThemedText.DeprecatedBody>
<ThemedText.DeprecatedMain fontSize="10px" textAlign="center" maxWidth="140px" color="text3">
<Trans>Estimate may differ due to your wallet gas settings</Trans>
</ThemedText.DeprecatedMain>
</AutoColumn>
)}
</ResponsiveTooltipContainer>
)
}
placement="bottom"
onOpen={() =>
sendEvent({
category: 'Gas',
action: 'Gas Details Tooltip Open',
})
}
>
<LoadingOpacityContainer $loading={loading}>
<GasWrapper>
<StyledGasIcon />
{formattedGasPriceString ?? null}
</GasWrapper>
</LoadingOpacityContainer>
</MouseoverTooltipContent>
)
}
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { InterfaceElementName, SwapEventName } from '@uniswap/analytics-events'
import { Currency, TradeType } from '@uniswap/sdk-core'
import { LoadingOpacityContainer } from 'components/Loader/styled'
import { RowFixed } from 'components/Row'
import { MouseoverTooltip, TooltipSize } from 'components/Tooltip'
import { InterfaceTrade } from 'state/routing/types'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import { ReactComponent as GasIcon } from '../../assets/images/gas-icon.svg'
import SwapRoute from './SwapRoute'
const StyledGasIcon = styled(GasIcon)`
margin-right: 4px;
height: 18px;
// We apply the following to all children of the SVG in order to override the default color
& > * {
stroke: ${({ theme }) => theme.textTertiary};
}
`
export default function GasEstimateTooltip({
trade,
loading,
disabled,
}: {
trade: InterfaceTrade<Currency, Currency, TradeType> // dollar amount in active chain's stablecoin
loading: boolean
disabled?: boolean
}) {
const formattedGasPriceString = trade?.gasUseEstimateUSD
? trade.gasUseEstimateUSD.toFixed(2) === '0.00'
? '<$0.01'
: '$' + trade.gasUseEstimateUSD.toFixed(2)
: undefined
return (
<MouseoverTooltip
disabled={disabled}
size={TooltipSize.Large}
// TODO(WEB-3304)
// Most of Swap-related components accept either `syncing`, `loading` or both props at the same time.
// We are often using them interchangeably, or pass both values as one of them (`syncing={loading || syncing}`).
// This is confusing and can lead to unpredicted UI behavior. We should refactor and unify this.
text={<SwapRoute trade={trade} syncing={loading} />}
onOpen={() => {
sendAnalyticsEvent(SwapEventName.SWAP_AUTOROUTER_VISUALIZATION_EXPANDED, {
element: InterfaceElementName.AUTOROUTER_VISUALIZATION_ROW,
})
}}
placement="bottom"
>
<LoadingOpacityContainer $loading={loading}>
<RowFixed>
<StyledGasIcon />
<ThemedText.BodySmall color="textSecondary">{formattedGasPriceString}</ThemedText.BodySmall>
</RowFixed>
</LoadingOpacityContainer>
</MouseoverTooltip>
)
}
...@@ -8,7 +8,6 @@ import { ThemedText } from '../../theme' ...@@ -8,7 +8,6 @@ import { ThemedText } from '../../theme'
import { AutoColumn } from '../Column' import { AutoColumn } from '../Column'
import { RowBetween, RowFixed } from '../Row' import { RowBetween, RowFixed } from '../Row'
import { MouseoverTooltip } from '../Tooltip' import { MouseoverTooltip } from '../Tooltip'
import { formatPriceImpact } from './FormattedPriceImpact'
const StyledCard = styled(OutlineCard)` const StyledCard = styled(OutlineCard)`
padding: 12px; padding: 12px;
...@@ -19,6 +18,8 @@ interface PriceImpactWarningProps { ...@@ -19,6 +18,8 @@ interface PriceImpactWarningProps {
priceImpact: Percent priceImpact: Percent
} }
const formatPriceImpact = (priceImpact: Percent) => `${priceImpact.multiply(-1).toFixed(2)}%`
export default function PriceImpactWarning({ priceImpact }: PriceImpactWarningProps) { export default function PriceImpactWarning({ priceImpact }: PriceImpactWarningProps) {
const theme = useTheme() const theme = useTheme()
......
import { Trans } from '@lingui/macro' import { RouterPreference } from 'state/routing/slice'
import useAutoRouterSupported from 'hooks/useAutoRouterSupported' import { useRouterPreference } from 'state/user/hooks'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme' import { ThemedText } from 'theme'
import { ReactComponent as StaticRouterIcon } from '../../assets/svg/static_route.svg' export default function RouterLabel() {
import AutoRouterIcon from './AutoRouterIcon' const [routerPreference] = useRouterPreference()
const StyledAutoRouterIcon = styled(AutoRouterIcon)` switch (routerPreference) {
height: 16px; case RouterPreference.AUTO:
width: 16px; case RouterPreference.API:
return <ThemedText.BodySmall>Uniswap API</ThemedText.BodySmall>
:hover { case RouterPreference.CLIENT:
filter: brightness(1.3); return <ThemedText.BodySmall>Uniswap Client</ThemedText.BodySmall>
}
`
const StyledStaticRouterIcon = styled(StaticRouterIcon)`
height: 16px;
width: 16px;
fill: ${({ theme }) => theme.textTertiary};
:hover {
filter: brightness(1.3);
}
`
const StyledAutoRouterLabel = styled(ThemedText.DeprecatedBlack)`
line-height: 1rem;
/* fallback color */
color: ${({ theme }) => theme.accentSuccess};
@supports (-webkit-background-clip: text) and (-webkit-text-fill-color: transparent) {
background-image: linear-gradient(90deg, #2172e5 0%, #54e521 163.16%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
} }
`
export function AutoRouterLogo() {
const autoRouterSupported = useAutoRouterSupported()
return autoRouterSupported ? <StyledAutoRouterIcon /> : <StyledStaticRouterIcon />
}
export function AutoRouterLabel() {
const autoRouterSupported = useAutoRouterSupported()
return autoRouterSupported ? (
<StyledAutoRouterLabel fontSize={14}>Auto Router</StyledAutoRouterLabel>
) : (
<ThemedText.DeprecatedBlack fontSize={14}>
<Trans>Trade Route</Trans>
</ThemedText.DeprecatedBlack>
)
} }
...@@ -4,7 +4,7 @@ import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/an ...@@ -4,7 +4,7 @@ import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/an
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { useAccountDrawer } from 'components/AccountDrawer' import { useAccountDrawer } from 'components/AccountDrawer'
import { ButtonText } from 'components/Button' import { ButtonText } from 'components/Button'
import { MouseoverTooltipContent } from 'components/Tooltip' import { MouseoverTooltip } from 'components/Tooltip'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { useBuyFiatFlowCompleted } from 'state/user/hooks' import { useBuyFiatFlowCompleted } from 'state/user/hooks'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
...@@ -109,9 +109,8 @@ export default function SwapBuyFiatButton() { ...@@ -109,9 +109,8 @@ export default function SwapBuyFiatButton() {
!fiatOnrampAvailabilityChecked || (fiatOnrampAvailabilityChecked && fiatOnrampAvailable) !fiatOnrampAvailabilityChecked || (fiatOnrampAvailabilityChecked && fiatOnrampAvailable)
return ( return (
<MouseoverTooltipContent <MouseoverTooltip
wrap text={
content={
<div data-testid="fiat-on-ramp-unavailable-tooltip"> <div data-testid="fiat-on-ramp-unavailable-tooltip">
<Trans>Crypto purchases are not available in your region. </Trans> <Trans>Crypto purchases are not available in your region. </Trans>
<TraceEvent <TraceEvent
...@@ -126,7 +125,7 @@ export default function SwapBuyFiatButton() { ...@@ -126,7 +125,7 @@ export default function SwapBuyFiatButton() {
</div> </div>
} }
placement="bottom" placement="bottom"
disableHover={fiatOnRampsUnavailableTooltipDisabled} disabled={fiatOnRampsUnavailableTooltipDisabled}
> >
<TraceEvent <TraceEvent
events={[BrowserEvent.onClick]} events={[BrowserEvent.onClick]}
...@@ -139,6 +138,6 @@ export default function SwapBuyFiatButton() { ...@@ -139,6 +138,6 @@ export default function SwapBuyFiatButton() {
{!buyFiatFlowCompleted && <Dot data-testid="buy-fiat-flow-incomplete-indicator" />} {!buyFiatFlowCompleted && <Dot data-testid="buy-fiat-flow-incomplete-indicator" />}
</StyledTextButton> </StyledTextButton>
</TraceEvent> </TraceEvent>
</MouseoverTooltipContent> </MouseoverTooltip>
) )
} }
...@@ -38,6 +38,5 @@ describe('SwapDetailsDropdown.tsx', () => { ...@@ -38,6 +38,5 @@ describe('SwapDetailsDropdown.tsx', () => {
expect(screen.getByTestId('trade-price-container')).toBeInTheDocument() expect(screen.getByTestId('trade-price-container')).toBeInTheDocument()
await act(() => userEvent.click(screen.getByTestId('swap-details-header-row'))) await act(() => userEvent.click(screen.getByTestId('swap-details-header-row')))
expect(screen.getByTestId('advanced-swap-details')).toBeInTheDocument() expect(screen.getByTestId('advanced-swap-details')).toBeInTheDocument()
expect(screen.getByTestId('swap-route-info')).toBeInTheDocument()
}) })
}) })
...@@ -4,10 +4,9 @@ import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/anal ...@@ -4,10 +4,9 @@ import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/anal
import { Currency, Percent, TradeType } from '@uniswap/sdk-core' import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import AnimatedDropdown from 'components/AnimatedDropdown' import AnimatedDropdown from 'components/AnimatedDropdown'
import { OutlineCard } from 'components/Card' import Column from 'components/Column'
import { AutoColumn } from 'components/Column'
import { LoadingOpacityContainer } from 'components/Loader/styled' import { LoadingOpacityContainer } from 'components/Loader/styled'
import Row, { RowBetween, RowFixed } from 'components/Row' import { RowBetween, RowFixed } from 'components/Row'
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains' import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
import { useState } from 'react' import { useState } from 'react'
import { ChevronDown } from 'react-feather' import { ChevronDown } from 'react-feather'
...@@ -16,24 +15,9 @@ import styled, { keyframes, useTheme } from 'styled-components/macro' ...@@ -16,24 +15,9 @@ import styled, { keyframes, useTheme } from 'styled-components/macro'
import { ThemedText } from 'theme' import { ThemedText } from 'theme'
import { AdvancedSwapDetails } from './AdvancedSwapDetails' import { AdvancedSwapDetails } from './AdvancedSwapDetails'
import GasEstimateBadge from './GasEstimateBadge' import GasEstimateTooltip from './GasEstimateTooltip'
import SwapRoute from './SwapRoute'
import TradePrice from './TradePrice' import TradePrice from './TradePrice'
const Wrapper = styled(Row)`
width: 100%;
justify-content: center;
border-radius: inherit;
padding: 8px 12px;
margin-top: 0;
min-height: 32px;
`
const StyledCard = styled(OutlineCard)`
padding: 12px;
border: 1px solid ${({ theme }) => theme.backgroundOutline};
`
const StyledHeaderRow = styled(RowBetween)<{ disabled: boolean; open: boolean }>` const StyledHeaderRow = styled(RowBetween)<{ disabled: boolean; open: boolean }>`
padding: 0; padding: 0;
align-items: center; align-items: center;
...@@ -97,6 +81,16 @@ const Spinner = styled.div` ...@@ -97,6 +81,16 @@ const Spinner = styled.div`
top: -3px; top: -3px;
` `
const SwapDetailsWrapper = styled.div`
padding-top: ${({ theme }) => theme.grids.md};
`
const Wrapper = styled(Column)`
border: 1px solid ${({ theme }) => theme.backgroundOutline};
border-radius: 16px;
padding: 12px 16px;
`
interface SwapDetailsInlineProps { interface SwapDetailsInlineProps {
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
syncing: boolean syncing: boolean
...@@ -110,68 +104,58 @@ export default function SwapDetailsDropdown({ trade, syncing, loading, allowedSl ...@@ -110,68 +104,58 @@ export default function SwapDetailsDropdown({ trade, syncing, loading, allowedSl
const [showDetails, setShowDetails] = useState(false) const [showDetails, setShowDetails] = useState(false)
return ( return (
<Wrapper style={{ marginTop: '0' }}> <Wrapper>
<AutoColumn gap="sm" style={{ width: '100%', marginBottom: '-8px' }}> <TraceEvent
<TraceEvent events={[BrowserEvent.onClick]}
events={[BrowserEvent.onClick]} name={SwapEventName.SWAP_DETAILS_EXPANDED}
name={SwapEventName.SWAP_DETAILS_EXPANDED} element={InterfaceElementName.SWAP_DETAILS_DROPDOWN}
element={InterfaceElementName.SWAP_DETAILS_DROPDOWN} shouldLogImpression={!showDetails}
shouldLogImpression={!showDetails} >
<StyledHeaderRow
data-testid="swap-details-header-row"
onClick={() => setShowDetails(!showDetails)}
disabled={!trade}
open={showDetails}
> >
<StyledHeaderRow <RowFixed>
data-testid="swap-details-header-row" {Boolean(loading || syncing) && (
onClick={() => setShowDetails(!showDetails)} <StyledPolling>
disabled={!trade} <StyledPollingDot>
open={showDetails} <Spinner />
> </StyledPollingDot>
<RowFixed style={{ position: 'relative' }} align="center"> </StyledPolling>
{Boolean(loading || syncing) && ( )}
<StyledPolling>
<StyledPollingDot>
<Spinner />
</StyledPollingDot>
</StyledPolling>
)}
{trade ? (
<LoadingOpacityContainer $loading={syncing} data-testid="trade-price-container">
<TradePrice price={trade.executionPrice} />
</LoadingOpacityContainer>
) : loading || syncing ? (
<ThemedText.DeprecatedMain fontSize={14}>
<Trans>Fetching best price...</Trans>
</ThemedText.DeprecatedMain>
) : null}
</RowFixed>
<RowFixed>
{!trade?.gasUseEstimateUSD ||
showDetails ||
!chainId ||
!SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) ? null : (
<GasEstimateBadge
trade={trade}
loading={syncing || loading}
showRoute={!showDetails}
disableHover={showDetails}
/>
)}
<RotatingArrow
stroke={trade ? theme.textTertiary : theme.deprecated_bg3}
open={Boolean(trade && showDetails)}
/>
</RowFixed>
</StyledHeaderRow>
</TraceEvent>
<AnimatedDropdown open={showDetails}>
<AutoColumn gap="sm" style={{ padding: '0', paddingBottom: '8px' }}>
{trade ? ( {trade ? (
<StyledCard data-testid="advanced-swap-details"> <LoadingOpacityContainer $loading={syncing} data-testid="trade-price-container">
<AdvancedSwapDetails trade={trade} allowedSlippage={allowedSlippage} syncing={syncing} /> <TradePrice price={trade.executionPrice} />
</StyledCard> </LoadingOpacityContainer>
) : loading || syncing ? (
<ThemedText.DeprecatedMain fontSize={14}>
<Trans>Fetching best price...</Trans>
</ThemedText.DeprecatedMain>
) : null} ) : null}
{trade ? <SwapRoute data-testid="swap-route-info" trade={trade} syncing={syncing} /> : null} </RowFixed>
</AutoColumn> <RowFixed>
{!trade?.gasUseEstimateUSD ||
showDetails ||
!chainId ||
!SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) ? null : (
<GasEstimateTooltip trade={trade} loading={syncing || loading} disabled={showDetails} />
)}
<RotatingArrow
stroke={trade ? theme.textTertiary : theme.deprecated_bg3}
open={Boolean(trade && showDetails)}
/>
</RowFixed>
</StyledHeaderRow>
</TraceEvent>
{trade && (
<AnimatedDropdown open={showDetails}>
<SwapDetailsWrapper data-testid="advanced-swap-details">
<AdvancedSwapDetails trade={trade} allowedSlippage={allowedSlippage} syncing={syncing} />
</SwapDetailsWrapper>
</AnimatedDropdown> </AnimatedDropdown>
</AutoColumn> )}
</Wrapper> </Wrapper>
) )
} }
...@@ -16,12 +16,12 @@ import { Text } from 'rebass' ...@@ -16,12 +16,12 @@ import { Text } from 'rebass'
import { RouterPreference } from 'state/routing/slice' import { RouterPreference } from 'state/routing/slice'
import { InterfaceTrade } from 'state/routing/types' import { InterfaceTrade } from 'state/routing/types'
import { useRouterPreference, useUserSlippageTolerance } from 'state/user/hooks' import { useRouterPreference, useUserSlippageTolerance } from 'state/user/hooks'
import getRoutingDiagramEntries, { RoutingDiagramEntry } from 'utils/getRoutingDiagramEntries'
import { computeRealizedPriceImpact } from 'utils/prices' import { computeRealizedPriceImpact } from 'utils/prices'
import { ButtonError } from '../Button' import { ButtonError } from '../Button'
import { AutoRow } from '../Row' import { AutoRow } from '../Row'
import { SwapCallbackError } from './styleds' import { SwapCallbackError } from './styleds'
import { getTokenPath, RoutingDiagramEntry } from './SwapRoute'
interface AnalyticsEventProps { interface AnalyticsEventProps {
trade: InterfaceTrade<Currency, Currency, TradeType> trade: InterfaceTrade<Currency, Currency, TradeType>
...@@ -125,7 +125,7 @@ export default function SwapModalFooter({ ...@@ -125,7 +125,7 @@ export default function SwapModalFooter({
const transactionDeadlineSecondsSinceEpoch = useTransactionDeadline()?.toNumber() // in seconds since epoch const transactionDeadlineSecondsSinceEpoch = useTransactionDeadline()?.toNumber() // in seconds since epoch
const isAutoSlippage = useUserSlippageTolerance()[0] === 'auto' const isAutoSlippage = useUserSlippageTolerance()[0] === 'auto'
const [routerPreference] = useRouterPreference() const [routerPreference] = useRouterPreference()
const routes = getTokenPath(trade) const routes = getRoutingDiagramEntries(trade)
return ( return (
<> <>
......
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { TraceEvent } from '@uniswap/analytics' import { Currency, TradeType } from '@uniswap/sdk-core'
import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/analytics-events'
import { Protocol } from '@uniswap/router-sdk'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { Pair } from '@uniswap/v2-sdk'
import { FeeAmount } from '@uniswap/v3-sdk'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import AnimatedDropdown from 'components/AnimatedDropdown' import Column from 'components/Column'
import { AutoColumn } from 'components/Column'
import { LoadingRows } from 'components/Loader/styled' import { LoadingRows } from 'components/Loader/styled'
import RoutingDiagram from 'components/RoutingDiagram/RoutingDiagram' import RoutingDiagram from 'components/RoutingDiagram/RoutingDiagram'
import { AutoRow, RowBetween } from 'components/Row'
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains' import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
import useAutoRouterSupported from 'hooks/useAutoRouterSupported' import useAutoRouterSupported from 'hooks/useAutoRouterSupported'
import { memo, useState } from 'react'
import { Plus } from 'react-feather'
import { InterfaceTrade } from 'state/routing/types' import { InterfaceTrade } from 'state/routing/types'
import styled from 'styled-components/macro'
import { Separator, ThemedText } from 'theme' import { Separator, ThemedText } from 'theme'
import { useDarkModeManager } from 'theme/components/ThemeToggle' import getRoutingDiagramEntries from 'utils/getRoutingDiagramEntries'
import { AutoRouterLabel, AutoRouterLogo } from './RouterLabel' import RouterLabel from './RouterLabel'
const Wrapper = styled(AutoColumn)<{ darkMode?: boolean; fixedOpen?: boolean }>` export default function SwapRoute({
padding: ${({ fixedOpen }) => (fixedOpen ? '12px' : '12px 8px 12px 12px')}; trade,
border-radius: 16px; syncing,
border: 1px solid ${({ theme, fixedOpen }) => (fixedOpen ? 'transparent' : theme.backgroundOutline)}; }: {
cursor: pointer;
`
const OpenCloseIcon = styled(Plus)<{ open?: boolean }>`
margin-left: 8px;
height: 20px;
stroke-width: 2px;
transition: transform 0.1s;
transform: ${({ open }) => (open ? 'rotate(45deg)' : 'none')};
stroke: ${({ theme }) => theme.textTertiary};
cursor: pointer;
:hover {
opacity: 0.8;
}
`
interface SwapRouteProps extends React.HTMLAttributes<HTMLDivElement> {
trade: InterfaceTrade<Currency, Currency, TradeType> trade: InterfaceTrade<Currency, Currency, TradeType>
syncing: boolean syncing: boolean
fixedOpen?: boolean // fixed in open state, hide open/close icon }) {
}
export default memo(function SwapRoute({ trade, syncing, fixedOpen = false, ...rest }: SwapRouteProps) {
const autoRouterSupported = useAutoRouterSupported()
const routes = getTokenPath(trade)
const [open, setOpen] = useState(false)
const { chainId } = useWeb3React() const { chainId } = useWeb3React()
const autoRouterSupported = useAutoRouterSupported()
const [darkMode] = useDarkModeManager() const routes = getRoutingDiagramEntries(trade)
const formattedGasPriceString = trade?.gasUseEstimateUSD const gasPrice =
? trade.gasUseEstimateUSD.toFixed(2) === '0.00' // TODO(WEB-3303)
? '<$0.01' // Can `trade.gasUseEstimateUSD` be defined when `chainId` is not in `SUPPORTED_GAS_ESTIMATE_CHAIN_IDS`?
: '$' + trade.gasUseEstimateUSD.toFixed(2) trade.gasUseEstimateUSD && chainId && SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId)
: undefined ? trade.gasUseEstimateUSD.toFixed(2) === '0.00'
? '<$0.01'
: '$' + trade.gasUseEstimateUSD.toFixed(2)
: undefined
return ( return (
<Wrapper {...rest} darkMode={darkMode} fixedOpen={fixedOpen}> <Column gap="md">
<TraceEvent <RouterLabel />
events={[BrowserEvent.onClick]} <Separator />
name={SwapEventName.SWAP_AUTOROUTER_VISUALIZATION_EXPANDED} {syncing ? (
element={InterfaceElementName.AUTOROUTER_VISUALIZATION_ROW} <LoadingRows>
shouldLogImpression={!open} <div style={{ width: '100%', height: '30px' }} />
> </LoadingRows>
<RowBetween onClick={() => setOpen(!open)}> ) : (
<AutoRow gap="4px" width="auto"> <RoutingDiagram
<AutoRouterLogo /> currencyIn={trade.inputAmount.currency}
<AutoRouterLabel /> currencyOut={trade.outputAmount.currency}
</AutoRow> routes={routes}
{fixedOpen ? null : <OpenCloseIcon open={open} />} />
</RowBetween> )}
</TraceEvent> {autoRouterSupported && (
<AnimatedDropdown open={open || fixedOpen}> <>
<AutoRow gap="4px" width="auto" style={{ paddingTop: '12px', margin: 0 }}> <Separator />
{syncing ? ( {syncing ? (
<LoadingRows> <LoadingRows>
<div style={{ width: '400px', height: '30px' }} /> <div style={{ width: '100%', height: '15px' }} />
</LoadingRows> </LoadingRows>
) : ( ) : (
<RoutingDiagram <ThemedText.Caption color="textSecondary">
currencyIn={trade.inputAmount.currency} {gasPrice ? <Trans>Best price route costs ~{gasPrice} in gas.</Trans> : null}{' '}
currencyOut={trade.outputAmount.currency} <Trans>
routes={routes} This route optimizes your total output by considering split routes, multiple hops, and the gas cost of
/> each step.
</Trans>
</ThemedText.Caption>
)} )}
</>
{autoRouterSupported && ( )}
<> </Column>
<Separator />
{syncing ? (
<LoadingRows>
<div style={{ width: '250px', height: '15px' }} />
</LoadingRows>
) : (
<ThemedText.DeprecatedMain fontSize={12} width={400} margin={0}>
{trade?.gasUseEstimateUSD && chainId && SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) ? (
<Trans>Best price route costs ~{formattedGasPriceString} 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.DeprecatedMain>
)}
</>
)}
</AutoRow>
</AnimatedDropdown>
</Wrapper>
) )
})
export interface RoutingDiagramEntry {
percent: Percent
path: [Currency, Currency, FeeAmount][]
protocol: Protocol
}
const V2_DEFAULT_FEE_TIER = 3000
/**
* Loops through all routes on a trade and returns an array of diagram entries.
*/
export function getTokenPath(trade: InterfaceTrade<Currency, Currency, TradeType>): RoutingDiagramEntry[] {
return trade.swaps.map(({ route: { path: tokenPath, pools, protocol }, inputAmount, outputAmount }) => {
const portion =
trade.tradeType === TradeType.EXACT_INPUT
? inputAmount.divide(trade.inputAmount)
: outputAmount.divide(trade.outputAmount)
const percent = new Percent(portion.numerator, portion.denominator)
const path: RoutingDiagramEntry['path'] = []
for (let i = 0; i < pools.length; i++) {
const nextPool = pools[i]
const tokenIn = tokenPath[i]
const tokenOut = tokenPath[i + 1]
const entry: RoutingDiagramEntry['path'][0] = [
tokenIn,
tokenOut,
nextPool instanceof Pair ? V2_DEFAULT_FEE_TIER : nextPool.fee,
]
path.push(entry)
}
return {
percent,
path,
protocol,
}
})
} }
...@@ -25,7 +25,6 @@ const StyledPriceContainer = styled.button` ...@@ -25,7 +25,6 @@ const StyledPriceContainer = styled.button`
flex-direction: row; flex-direction: row;
text-align: left; text-align: left;
flex-wrap: wrap; flex-wrap: wrap;
padding: 8px 0;
user-select: text; user-select: text;
` `
...@@ -60,9 +59,9 @@ export default function TradePrice({ price }: TradePriceProps) { ...@@ -60,9 +59,9 @@ export default function TradePrice({ price }: TradePriceProps) {
> >
<ThemedText.BodySmall>{text}</ThemedText.BodySmall>{' '} <ThemedText.BodySmall>{text}</ThemedText.BodySmall>{' '}
{usdPrice && ( {usdPrice && (
<ThemedText.DeprecatedDarkGray> <ThemedText.BodySmall color="textSecondary">
<Trans>({formatNumber(usdPrice, NumberType.FiatTokenPrice)})</Trans> <Trans>({formatNumber(usdPrice, NumberType.FiatTokenPrice)})</Trans>
</ThemedText.DeprecatedDarkGray> </ThemedText.BodySmall>
)} )}
</StyledPriceContainer> </StyledPriceContainer>
) )
......
...@@ -2,13 +2,13 @@ ...@@ -2,13 +2,13 @@
exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = ` exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
<DocumentFragment> <DocumentFragment>
.c0 { .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;
...@@ -25,139 +25,128 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = ` ...@@ -25,139 +25,128 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
justify-content: flex-start; justify-content: flex-start;
} }
.c5 { .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;
} }
.c6 { .c5 {
width: -webkit-fit-content; width: -webkit-fit-content;
width: -moz-fit-content; width: -moz-fit-content;
width: fit-content; width: fit-content;
} }
.c7 {
color: #7780A0;
}
.c8 { .c8 {
color: #0D111C; color: #0D111C;
} }
.c10 { .c1 {
width: 100%; width: 100%;
height: 1px; height: 1px;
background-color: #D2D9EE; background-color: #D2D9EE;
} }
.c1 { .c0 {
width: 100%; display: -webkit-box;
padding: 1rem; display: -webkit-flex;
border-radius: 16px; display: -ms-flexbox;
} display: flex;
-webkit-flex-direction: column;
.c3 { -ms-flex-direction: column;
display: grid; flex-direction: column;
grid-auto-rows: auto; -webkit-box-pack: start;
grid-row-gap: 8px; -webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 12px;
} }
.c7 { .c6 {
display: inline-block; display: inline-block;
height: inherit; height: inherit;
} }
.c9 {
color: #7780A0;
}
.c2 {
padding: 0;
}
<div <div
class="c0 c1 c2" class="c0"
> >
<div <div
class="c3" class="c1"
/>
<div
class="c2 c3 c4"
> >
<div <div
class="c0 c4 c5" class="c2 c3 c5"
> >
<div <div
class="c0 c4 c6" class="c6"
> >
<div <div>
class="c7" <div
> class="c7 css-zhpkf8"
<div> >
<div Minimum output
class="css-zhpkf8"
>
Expected Output
</div>
</div> </div>
</div> </div>
</div> </div>
<div
class="c8 css-q4yjm0"
>
0.000000000000001 DEF
</div>
</div> </div>
<div <div
class="c0 c4 c5" class="c8 css-zhpkf8"
>
0.00000000000000098 DEF
</div>
</div>
<div
class="c2 c3 c4"
>
<div
class="c2 c3 c5"
> >
<div <div
class="c0 c4 c6" class="c6"
> >
<div <div>
class="c7" <div
> class="c7 css-zhpkf8"
<div> >
<div Expected output
class="css-zhpkf8"
>
Price Impact
</div>
</div> </div>
</div> </div>
</div> </div>
<div
class="c8 css-q4yjm0"
>
<div
class="c9 css-1aekuku"
>
105567.37%
</div>
</div>
</div> </div>
<div <div
class="c10" class="c8 css-zhpkf8"
/> >
0.000000000000001 DEF
</div>
</div>
<div
class="c1"
/>
<div
class="c2 c3 c4"
>
<div <div
class="c0 c4 c5" class="c7 css-zhpkf8"
> >
<div Order routing
class="c0 c4 c6" </div>
style="margin-right: 20px;" <div
> class="c6"
>
<div>
<div <div
class="c7" class="c8 css-zhpkf8"
> >
<div> Uniswap API
<div
class="css-zhpkf8"
>
Minimum received after slippage (2.00%)
</div>
</div>
</div> </div>
</div> </div>
<div
class="css-q4yjm0"
>
0.00000000000000098 DEF
</div>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -81,7 +81,7 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp ...@@ -81,7 +81,7 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
margin: -0px; margin: -0px;
} }
.c21 { .c22 {
width: -webkit-fit-content; width: -webkit-fit-content;
width: -moz-fit-content; width: -moz-fit-content;
width: fit-content; width: fit-content;
...@@ -91,11 +91,11 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp ...@@ -91,11 +91,11 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
color: #0D111C; color: #0D111C;
} }
.c26 { .c24 {
color: #7780A0; color: #7780A0;
} }
.c24 { .c21 {
width: 100%; width: 100%;
height: 1px; height: 1px;
background-color: #D2D9EE; background-color: #D2D9EE;
...@@ -118,6 +118,21 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp ...@@ -118,6 +118,21 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
background-color: #F5F6FC; background-color: #F5F6FC;
} }
.c20 {
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;
}
.c0 { .c0 {
display: grid; display: grid;
grid-auto-rows: auto; grid-auto-rows: auto;
...@@ -152,7 +167,7 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp ...@@ -152,7 +167,7 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
background-size: 400%; background-size: 400%;
} }
.c22 { .c23 {
display: inline-block; display: inline-block;
height: inherit; height: inherit;
} }
...@@ -205,17 +220,12 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp ...@@ -205,17 +220,12 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
-webkit-flex-wrap: wrap; -webkit-flex-wrap: wrap;
-ms-flex-wrap: wrap; -ms-flex-wrap: wrap;
flex-wrap: wrap; flex-wrap: wrap;
padding: 8px 0;
-webkit-user-select: text; -webkit-user-select: text;
-moz-user-select: text; -moz-user-select: text;
-ms-user-select: text; -ms-user-select: text;
user-select: text; user-select: text;
} }
.c23 {
color: #7780A0;
}
.c10 { .c10 {
text-overflow: ellipsis; text-overflow: ellipsis;
max-width: 220px; max-width: 220px;
...@@ -223,10 +233,6 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp ...@@ -223,10 +233,6 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
text-align: right; text-align: right;
} }
.c20 {
padding: 0;
}
.c15 { .c15 {
padding: 4px; padding: 4px;
border-radius: 12px; border-radius: 12px;
...@@ -415,90 +421,80 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp ...@@ -415,90 +421,80 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
style="padding: .75rem; margin-top: 0.5rem;" style="padding: .75rem; margin-top: 0.5rem;"
> >
<div <div
class="c5 c19 c20" class="c20"
> >
<div <div
class="c4" class="c21"
/>
<div
class="c5 c6 c7"
> >
<div <div
class="c5 c6 c7" class="c5 c6 c22"
> >
<div <div
class="c5 c6 c21" class="c23"
> >
<div <div>
class="c22" <div
> class="c24 css-zhpkf8"
<div> >
<div Minimum output
class="css-zhpkf8"
>
Expected Output
</div>
</div> </div>
</div> </div>
</div> </div>
<div
class="c18 css-q4yjm0"
>
0.000000000000001 DEF
</div>
</div> </div>
<div <div
class="c5 c6 c7" class="c18 css-zhpkf8"
>
0.00000000000000098 DEF
</div>
</div>
<div
class="c5 c6 c7"
>
<div
class="c5 c6 c22"
> >
<div <div
class="c5 c6 c21" class="c23"
> >
<div <div>
class="c22" <div
> class="c24 css-zhpkf8"
<div> >
<div Expected output
class="css-zhpkf8"
>
Price Impact
</div>
</div> </div>
</div> </div>
</div> </div>
<div
class="c18 css-q4yjm0"
>
<div
class="c23 css-1aekuku"
>
105567.37%
</div>
</div>
</div> </div>
<div <div
class="c24" class="c18 css-zhpkf8"
/> >
0.000000000000001 DEF
</div>
</div>
<div
class="c21"
/>
<div
class="c5 c6 c7"
>
<div <div
class="c5 c6 c7" class="c24 css-zhpkf8"
> >
<div Order routing
class="c5 c6 c21" </div>
style="margin-right: 20px;" <div
> class="c23"
>
<div>
<div <div
class="c22" class="c18 css-zhpkf8"
> >
<div> Uniswap API
<div
class="css-zhpkf8"
>
Minimum received after slippage (2.00%)
</div>
</div>
</div> </div>
</div> </div>
<div
class="css-q4yjm0"
>
0.00000000000000098 DEF
</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -508,7 +504,7 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp ...@@ -508,7 +504,7 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
style="padding: .75rem 1rem;" style="padding: .75rem 1rem;"
> >
<div <div
class="c26 css-k51stg" class="c24 css-k51stg"
style="width: 100%;" style="width: 100%;"
> >
Output is estimated. You will receive at least Output is estimated. You will receive at least
...@@ -524,7 +520,7 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp ...@@ -524,7 +520,7 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
style="padding: 12px 0px 0px 0px;" style="padding: 12px 0px 0px 0px;"
> >
<div <div
class="c26 css-8mokm4" class="c24 css-8mokm4"
> >
Output will be sent to Output will be sent to
<b <b
......
import { TooltipContainer } from 'components/Tooltip'
import { SupportedChainId } from 'constants/chains' import { SupportedChainId } from 'constants/chains'
import { transparentize } from 'polished' import { transparentize } from 'polished'
import { ReactNode } from 'react' import { ReactNode } from 'react'
...@@ -64,17 +63,6 @@ export const ArrowWrapper = styled.div<{ clickable: boolean }>` ...@@ -64,17 +63,6 @@ export const ArrowWrapper = styled.div<{ clickable: boolean }>`
: null} : null}
` `
export const ErrorText = styled(Text)<{ severity?: 0 | 1 | 2 | 3 | 4 }>`
color: ${({ theme, severity }) =>
severity === 3 || severity === 4
? theme.accentFailure
: severity === 2
? theme.deprecated_yellow2
: severity === 1
? theme.textPrimary
: theme.textSecondary};
`
export const TruncatedText = styled(Text)` export const TruncatedText = styled(Text)`
text-overflow: ellipsis; text-overflow: ellipsis;
max-width: 220px; max-width: 220px;
...@@ -151,15 +139,3 @@ export const SwapShowAcceptChanges = styled(AutoColumn)` ...@@ -151,15 +139,3 @@ export const SwapShowAcceptChanges = styled(AutoColumn)`
border-radius: 12px; border-radius: 12px;
margin-top: 8px; margin-top: 8px;
` `
export const ResponsiveTooltipContainer = styled(TooltipContainer)<{ origin?: string; width?: string }>`
background-color: ${({ theme }) => theme.backgroundSurface};
border: 1px solid ${({ theme }) => theme.backgroundInteractive};
padding: 1rem;
width: ${({ width }) => width ?? 'auto'};
${({ theme, origin }) => theme.deprecated_mediaWidth.deprecated_upToExtraSmall`
transform: scale(0.8);
transform-origin: ${origin ?? 'top left'};
`}
`
...@@ -3,7 +3,7 @@ import { useWeb3React } from '@web3-react/core' ...@@ -3,7 +3,7 @@ import { useWeb3React } from '@web3-react/core'
import { SupportedChainId } from 'constants/chains' import { SupportedChainId } from 'constants/chains'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount' import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { useMemo, useRef } from 'react' import { useMemo, useRef } from 'react'
import { RouterPreference } from 'state/routing/slice' import { INTERNAL_ROUTER_PREFERENCE_PRICE } from 'state/routing/slice'
import { useRoutingAPITrade } from 'state/routing/useRoutingAPITrade' import { useRoutingAPITrade } from 'state/routing/useRoutingAPITrade'
import { CUSD_CELO, DAI_OPTIMISM, USDC_ARBITRUM, USDC_MAINNET, USDC_POLYGON, USDT_BSC } from '../constants/tokens' import { CUSD_CELO, DAI_OPTIMISM, USDC_ARBITRUM, USDC_MAINNET, USDC_POLYGON, USDT_BSC } from '../constants/tokens'
...@@ -28,7 +28,7 @@ export default function useStablecoinPrice(currency?: Currency): Price<Currency, ...@@ -28,7 +28,7 @@ export default function useStablecoinPrice(currency?: Currency): Price<Currency,
const amountOut = chainId ? STABLECOIN_AMOUNT_OUT[chainId] : undefined const amountOut = chainId ? STABLECOIN_AMOUNT_OUT[chainId] : undefined
const stablecoin = amountOut?.currency const stablecoin = amountOut?.currency
const { trade } = useRoutingAPITrade(TradeType.EXACT_OUTPUT, amountOut, currency, RouterPreference.PRICE) const { trade } = useRoutingAPITrade(TradeType.EXACT_OUTPUT, amountOut, currency, INTERNAL_ROUTER_PREFERENCE_PRICE)
const price = useMemo(() => { const price = useMemo(() => {
if (!currency || !stablecoin) { if (!currency || !stablecoin) {
return undefined return undefined
......
...@@ -3,7 +3,7 @@ import { Currency, CurrencyAmount, Price, SupportedChainId, TradeType } from '@u ...@@ -3,7 +3,7 @@ import { Currency, CurrencyAmount, Price, SupportedChainId, TradeType } from '@u
import { nativeOnChain } from 'constants/tokens' import { nativeOnChain } from 'constants/tokens'
import { Chain, useTokenSpotPriceQuery } from 'graphql/data/__generated__/types-and-hooks' import { Chain, useTokenSpotPriceQuery } from 'graphql/data/__generated__/types-and-hooks'
import { chainIdToBackendName, isGqlSupportedChain, PollingInterval } from 'graphql/data/util' import { chainIdToBackendName, isGqlSupportedChain, PollingInterval } from 'graphql/data/util'
import { RouterPreference } from 'state/routing/slice' import { INTERNAL_ROUTER_PREFERENCE_PRICE } from 'state/routing/slice'
import { TradeState } from 'state/routing/types' import { TradeState } from 'state/routing/types'
import { useRoutingAPITrade } from 'state/routing/useRoutingAPITrade' import { useRoutingAPITrade } from 'state/routing/useRoutingAPITrade'
import { getNativeTokenDBAddress } from 'utils/nativeTokens' import { getNativeTokenDBAddress } from 'utils/nativeTokens'
...@@ -30,7 +30,7 @@ function useETHValue(currencyAmount?: CurrencyAmount<Currency>): { ...@@ -30,7 +30,7 @@ function useETHValue(currencyAmount?: CurrencyAmount<Currency>): {
TradeType.EXACT_OUTPUT, TradeType.EXACT_OUTPUT,
amountOut, amountOut,
currencyAmount?.currency, currencyAmount?.currency,
RouterPreference.PRICE INTERNAL_ROUTER_PREFERENCE_PRICE
) )
// Get ETH value of ETH or WETH // Get ETH value of ETH or WETH
......
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { useMemo } from 'react' import { useMemo } from 'react'
import { RouterPreference } from 'state/routing/slice' import { INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference } from 'state/routing/slice'
/** /**
* Returns query arguments for the Routing API query or undefined if the * Returns query arguments for the Routing API query or undefined if the
...@@ -18,7 +18,7 @@ export function useRoutingAPIArguments({ ...@@ -18,7 +18,7 @@ export function useRoutingAPIArguments({
tokenOut: Currency | undefined tokenOut: Currency | undefined
amount: CurrencyAmount<Currency> | undefined amount: CurrencyAmount<Currency> | undefined
tradeType: TradeType tradeType: TradeType
routerPreference: RouterPreference routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE
}) { }) {
return useMemo( return useMemo(
() => () =>
......
...@@ -110,16 +110,8 @@ const SwapSection = styled.div` ...@@ -110,16 +110,8 @@ const SwapSection = styled.div`
} }
` `
const OutputSwapSection = styled(SwapSection)<{ showDetailsDropdown: boolean }>` const OutputSwapSection = styled(SwapSection)`
border-bottom: ${({ theme }) => `1px solid ${theme.backgroundSurface}`}; border-bottom: ${({ theme }) => `1px solid ${theme.backgroundSurface}`};
border-bottom-left-radius: ${({ showDetailsDropdown }) => showDetailsDropdown && '0'};
border-bottom-right-radius: ${({ showDetailsDropdown }) => showDetailsDropdown && '0'};
`
const DetailsSwapSection = styled(SwapSection)`
padding: 0;
border-top-left-radius: 0;
border-top-right-radius: 0;
` `
function getIsValidSwapQuote( function getIsValidSwapQuote(
...@@ -641,9 +633,9 @@ export function Swap({ ...@@ -641,9 +633,9 @@ export function Swap({
</TraceEvent> </TraceEvent>
</ArrowWrapper> </ArrowWrapper>
</div> </div>
<AutoColumn gap="md"> <AutoColumn gap="xs">
<div> <div>
<OutputSwapSection showDetailsDropdown={showDetailsDropdown}> <OutputSwapSection>
<Trace section={InterfaceSectionName.CURRENCY_OUTPUT_PANEL}> <Trace section={InterfaceSectionName.CURRENCY_OUTPUT_PANEL}>
<SwapCurrencyInputPanel <SwapCurrencyInputPanel
value={formattedAmounts[Field.OUTPUT]} value={formattedAmounts[Field.OUTPUT]}
...@@ -676,17 +668,15 @@ export function Swap({ ...@@ -676,17 +668,15 @@ export function Swap({
</> </>
) : null} ) : null}
</OutputSwapSection> </OutputSwapSection>
{showDetailsDropdown && (
<DetailsSwapSection>
<SwapDetailsDropdown
trade={trade}
syncing={routeIsSyncing}
loading={routeIsLoading}
allowedSlippage={allowedSlippage}
/>
</DetailsSwapSection>
)}
</div> </div>
{showDetailsDropdown && (
<SwapDetailsDropdown
trade={trade}
syncing={routeIsSyncing}
loading={routeIsLoading}
allowedSlippage={allowedSlippage}
/>
)}
{showPriceImpactWarning && <PriceImpactWarning priceImpact={largerPriceImpact} />} {showPriceImpactWarning && <PriceImpactWarning priceImpact={largerPriceImpact} />}
<div> <div>
{swapIsUnsupported ? ( {swapIsUnsupported ? (
......
...@@ -13,11 +13,12 @@ export enum RouterPreference { ...@@ -13,11 +13,12 @@ export enum RouterPreference {
AUTO = 'auto', AUTO = 'auto',
API = 'api', API = 'api',
CLIENT = 'client', CLIENT = 'client',
// Used internally for token -> USDC trades to get a USD value.
PRICE = 'price',
} }
// This is excluded from `RouterPreference` enum because it's only used
// internally for token -> USDC trades to get a USD value.
export const INTERNAL_ROUTER_PREFERENCE_PRICE = 'price' as const
const routers = new Map<ChainId, AlphaRouter>() const routers = new Map<ChainId, AlphaRouter>()
function getRouter(chainId: ChainId): AlphaRouter { function getRouter(chainId: ChainId): AlphaRouter {
const router = routers.get(chainId) const router = routers.get(chainId)
...@@ -78,7 +79,7 @@ interface GetQuoteArgs { ...@@ -78,7 +79,7 @@ interface GetQuoteArgs {
tokenOutDecimals: number tokenOutDecimals: number
tokenOutSymbol?: string tokenOutSymbol?: string
amount: string amount: string
routerPreference: RouterPreference routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE
type: 'exactIn' | 'exactOut' type: 'exactIn' | 'exactOut'
} }
...@@ -110,7 +111,7 @@ export const routingApi = createApi({ ...@@ -110,7 +111,7 @@ export const routingApi = createApi({
{ {
data: { data: {
...args, ...args,
isPrice: args.routerPreference === RouterPreference.PRICE, isPrice: args.routerPreference === INTERNAL_ROUTER_PREFERENCE_PRICE,
isAutoRouter: isAutoRouter:
args.routerPreference === RouterPreference.AUTO || args.routerPreference === RouterPreference.API, args.routerPreference === RouterPreference.AUTO || args.routerPreference === RouterPreference.API,
}, },
......
...@@ -7,7 +7,7 @@ import { useStablecoinAmountFromFiatValue } from 'hooks/useStablecoinPrice' ...@@ -7,7 +7,7 @@ import { useStablecoinAmountFromFiatValue } from 'hooks/useStablecoinPrice'
import { useRoutingAPIArguments } from 'lib/hooks/routing/useRoutingAPIArguments' import { useRoutingAPIArguments } from 'lib/hooks/routing/useRoutingAPIArguments'
import ms from 'ms.macro' import ms from 'ms.macro'
import { useMemo } from 'react' import { useMemo } from 'react'
import { RouterPreference, useGetQuoteQuery } from 'state/routing/slice' import { INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference, useGetQuoteQuery } from 'state/routing/slice'
import { InterfaceTrade, TradeState } from './types' import { InterfaceTrade, TradeState } from './types'
import { computeRoutes, transformRoutesToTrade } from './utils' import { computeRoutes, transformRoutesToTrade } from './utils'
...@@ -22,7 +22,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>( ...@@ -22,7 +22,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
tradeType: TTradeType, tradeType: TTradeType,
amountSpecified: CurrencyAmount<Currency> | undefined, amountSpecified: CurrencyAmount<Currency> | undefined,
otherCurrency: Currency | undefined, otherCurrency: Currency | undefined,
routerPreference: RouterPreference routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE
): { ): {
state: TradeState state: TradeState
trade: InterfaceTrade<Currency, Currency, TTradeType> | undefined trade: InterfaceTrade<Currency, Currency, TTradeType> | undefined
...@@ -50,7 +50,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>( ...@@ -50,7 +50,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
currentData, currentData,
} = useGetQuoteQuery(queryArgs ?? skipToken, { } = useGetQuoteQuery(queryArgs ?? skipToken, {
// Price-fetching is informational and costly, so it's done less frequently. // Price-fetching is informational and costly, so it's done less frequently.
pollingInterval: routerPreference === RouterPreference.PRICE ? ms`1m` : AVERAGE_L1_BLOCK_TIME, pollingInterval: routerPreference === INTERNAL_ROUTER_PREFERENCE_PRICE ? ms`1m` : AVERAGE_L1_BLOCK_TIME,
// If latest quote from cache was fetched > 2m ago, instantly repoll for another instead of waiting for next poll period // If latest quote from cache was fetched > 2m ago, instantly repoll for another instead of waiting for next poll period
refetchOnMountOrArgChange: 2 * 60, refetchOnMountOrArgChange: 2 * 60,
}) })
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`getRoutingDiagramEntries returns entries for a trade 1`] = `
Array [
Object {
"path": Array [
Array [
Token {
"address": "0x0000000000000000000000000000000000000001",
"chainId": 1,
"decimals": 18,
"isNative": false,
"isToken": true,
"name": "Abc",
"symbol": "ABC",
},
Token {
"address": "0x0000000000000000000000000000000000000002",
"chainId": 1,
"decimals": 18,
"isNative": false,
"isToken": true,
"name": "Def",
"symbol": "DEF",
},
10000,
],
],
"percent": Percent {
"denominator": JSBI [
1000,
],
"isPercent": true,
"numerator": JSBI [
1000,
],
},
"protocol": "V3",
},
]
`;
import { TEST_TRADE_EXACT_INPUT } from 'test-utils/constants'
import getRoutingDiagramEntries from './getRoutingDiagramEntries'
describe('getRoutingDiagramEntries', () => {
it('returns entries for a trade', () => {
expect(getRoutingDiagramEntries(TEST_TRADE_EXACT_INPUT)).toMatchSnapshot()
})
})
import { Protocol } from '@uniswap/router-sdk'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { Pair } from '@uniswap/v2-sdk'
import { FeeAmount } from '@uniswap/v3-sdk'
import { InterfaceTrade } from 'state/routing/types'
export interface RoutingDiagramEntry {
percent: Percent
path: [Currency, Currency, FeeAmount][]
protocol: Protocol
}
const V2_DEFAULT_FEE_TIER = 3000
/**
* Loops through all routes on a trade and returns an array of diagram entries.
*/
export default function getRoutingDiagramEntries(
trade: InterfaceTrade<Currency, Currency, TradeType>
): RoutingDiagramEntry[] {
return trade.swaps.map(({ route: { path: tokenPath, pools, protocol }, inputAmount, outputAmount }) => {
const portion =
trade.tradeType === TradeType.EXACT_INPUT
? inputAmount.divide(trade.inputAmount)
: outputAmount.divide(trade.outputAmount)
const percent = new Percent(portion.numerator, portion.denominator)
const path: RoutingDiagramEntry['path'] = []
for (let i = 0; i < pools.length; i++) {
const nextPool = pools[i]
const tokenIn = tokenPath[i]
const tokenOut = tokenPath[i + 1]
const entry: RoutingDiagramEntry['path'][0] = [
tokenIn,
tokenOut,
nextPool instanceof Pair ? V2_DEFAULT_FEE_TIER : nextPool.fee,
]
path.push(entry)
}
return {
percent,
path,
protocol,
}
})
}
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