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) => {
return (
<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
as="button"
gap="8"
......
......@@ -99,8 +99,8 @@ export default function Popover({
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null)
const [arrowElement, setArrowElement] = useState<HTMLDivElement | null>(null)
const options = useMemo(
(): Options => ({
const options: Options = useMemo(
() => ({
placement,
strategy: 'fixed',
modifiers: [
......@@ -109,7 +109,7 @@ export default function Popover({
{ name: 'preventOverflow', options: { padding: 8 } },
],
}),
[arrowElement, offsetX, offsetY, placement]
[placement, offsetX, offsetY, arrowElement]
)
const { styles, update, attributes } = usePopper(referenceElement, popperElement, options)
......
import { Protocol } from '@uniswap/router-sdk'
import { Currency, Percent } from '@uniswap/sdk-core'
import { FeeAmount } from '@uniswap/v3-sdk'
import { RoutingDiagramEntry } from 'components/swap/SwapRoute'
import { DAI, USDC_MAINNET, WBTC } from 'constants/tokens'
import { render } from 'test-utils/render'
import { RoutingDiagramEntry } from 'utils/getRoutingDiagramEntries'
import RoutingDiagram from './RoutingDiagram'
......
......@@ -6,12 +6,12 @@ import Badge from 'components/Badge'
import DoubleCurrencyLogo from 'components/DoubleLogo'
import CurrencyLogo from 'components/Logo/CurrencyLogo'
import Row, { AutoRow } from 'components/Row'
import { RoutingDiagramEntry } from 'components/swap/SwapRoute'
import { useTokenInfoFromActiveList } from 'hooks/useTokenInfoFromActiveList'
import { Box } from 'rebass'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import { Z_INDEX } from 'theme/zIndex'
import { RoutingDiagramEntry } from 'utils/getRoutingDiagramEntries'
import { ReactComponent as DotLine } from '../../assets/svg/dot_line.svg'
import { MouseoverTooltip } from '../Tooltip'
......
import { transparentize } from 'polished'
import { ReactNode, useEffect, useState } from 'react'
import { PropsWithChildren, ReactNode, useEffect, useState } from 'react'
import styled from 'styled-components/macro'
import noop from 'utils/noop'
import Popover, { PopoverProps } from '../Popover'
export const TooltipContainer = styled.div`
max-width: 256px;
export enum TooltipSize {
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;
padding: 0.6rem 1rem;
padding: ${({ size }) => getPaddingForSize(size)};
pointer-events: auto;
color: ${({ theme }) => theme.textPrimary};
......@@ -23,30 +38,23 @@ export const TooltipContainer = styled.div`
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
open?: () => void
close?: () => void
disableHover?: boolean // disable the hover and content display
size?: TooltipSize
disabled?: boolean
timeout?: number
}
interface TooltipContentProps extends Omit<PopoverProps, 'content'> {
content: ReactNode
onOpen?: () => void
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) {
// TODO(WEB-3305)
// Migrate to MouseoverTooltip and move this component inline to MouseoverTooltip
export default function Tooltip({ text, open, close, disabled, size = TooltipSize.Small, ...rest }: TooltipProps) {
return (
<Popover
content={
text && (
<TooltipContainer onMouseEnter={disableHover ? noop : open} onMouseLeave={disableHover ? noop : close}>
<TooltipContainer size={size} onMouseEnter={disabled ? noop : open} onMouseLeave={disabled ? noop : close}>
{text}
</TooltipContainer>
)
......@@ -56,27 +64,24 @@ export default function Tooltip({ text, open, close, disableHover, ...rest }: To
)
}
function TooltipContent({ content, wrap = false, open, close, disableHover, ...rest }: TooltipContentProps) {
return (
<Popover
content={
wrap ? (
<TooltipContainer onMouseEnter={disableHover ? noop : open} onMouseLeave={disableHover ? noop : close}>
{content}
</TooltipContainer>
) : (
content
)
}
{...rest}
/>
)
}
// TODO(WEB-3305)
// Do not pass through PopoverProps. Prefer higher-level interface to control MouseoverTooltip.
type MouseoverTooltipProps = Omit<PopoverProps, 'content' | 'show'> &
PropsWithChildren<{
text: ReactNode
size?: TooltipSize
disabled?: boolean
timeout?: number
placement?: PopoverProps['placement']
onOpen?: () => void
}>
/** Standard text tooltip. */
export function MouseoverTooltip({ text, disableHover, children, timeout, ...rest }: Omit<TooltipProps, 'show'>) {
export function MouseoverTooltip({ text, disabled, children, onOpen, timeout, ...rest }: MouseoverTooltipProps) {
const [show, setShow] = useState(false)
const open = () => text && setShow(true)
const open = () => {
setShow(true)
onOpen?.()
}
const close = () => setShow(false)
useEffect(() => {
......@@ -93,49 +98,10 @@ export function MouseoverTooltip({ text, disableHover, children, timeout, ...res
}, [timeout, show])
return (
<Tooltip
{...rest}
open={open}
close={close}
disableHover={disableHover}
show={show}
text={disableHover ? null : text}
>
<div onMouseEnter={disableHover ? noop : open} onMouseLeave={disableHover || timeout ? noop : close}>
<Tooltip {...rest} open={open} close={close} disabled={disabled} show={show} text={disabled ? null : text}>
<div onMouseEnter={disabled ? noop : open} onMouseLeave={disabled || timeout ? noop : close}>
{children}
</div>
</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', () => {
it('renders correct copy on mouseover', async () => {
render(<AdvancedSwapDetails trade={TEST_TRADE_EXACT_INPUT} allowedSlippage={TEST_ALLOWED_SLIPPAGE} />)
await act(() => userEvent.hover(screen.getByText('Price Impact')))
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')))
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 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()
})
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)
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()
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()
})
......
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 { useWeb3React } from '@web3-react/core'
import Card from 'components/Card'
import { LoadingRows } from 'components/Loader/styled'
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { useMemo } from 'react'
import { InterfaceTrade } from 'state/routing/types'
import styled, { useTheme } from 'styled-components/macro'
import { Separator, ThemedText } from '../../theme'
import { computeRealizedPriceImpact } from '../../utils/prices'
import { AutoColumn } from '../Column'
import Column from '../Column'
import { RowBetween, RowFixed } from '../Row'
import { MouseoverTooltip } from '../Tooltip'
import FormattedPriceImpact from './FormattedPriceImpact'
const StyledCard = styled(Card)`
padding: 0;
`
import { MouseoverTooltip, TooltipSize } from '../Tooltip'
import RouterLabel from './RouterLabel'
import SwapRoute from './SwapRoute'
interface AdvancedSwapDetailsProps {
trade?: InterfaceTrade<Currency, Currency, TradeType>
trade: InterfaceTrade<Currency, Currency, TradeType>
allowedSlippage: Percent
syncing?: boolean
hideInfoTooltips?: boolean
}
function TextWithLoadingPlaceholder({
......@@ -45,119 +39,92 @@ function TextWithLoadingPlaceholder({
)
}
export function AdvancedSwapDetails({
trade,
allowedSlippage,
syncing = false,
hideInfoTooltips = false,
}: AdvancedSwapDetailsProps) {
const theme = useTheme()
export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }: AdvancedSwapDetailsProps) {
const { chainId } = useWeb3React()
const nativeCurrency = useNativeCurrency(chainId)
const { expectedOutputAmount, priceImpact } = useMemo(() => {
return {
expectedOutputAmount: trade?.outputAmount,
priceImpact: trade ? computeRealizedPriceImpact(trade) : undefined,
}
}, [trade])
return !trade ? null : (
<StyledCard>
<AutoColumn gap="sm">
return (
<Column gap="md">
<Separator />
{!trade.gasUseEstimateUSD || !chainId || !SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) ? null : (
<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.
The fee paid to miners who process your transaction. This must be paid in {nativeCurrency.symbol}.
</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>
<RowFixed>
<MouseoverTooltip
text={<Trans>The impact your trade has on the market price of this pool.</Trans>}
disableHover={hideInfoTooltips}
>
<ThemedText.DeprecatedSubHeader color={theme.textPrimary}>
<Trans>Price Impact</Trans>
</ThemedText.DeprecatedSubHeader>
<ThemedText.BodySmall color="textSecondary">
<Trans>Network fee</Trans>
</ThemedText.BodySmall>
</MouseoverTooltip>
</RowFixed>
<TextWithLoadingPlaceholder syncing={syncing} width={50}>
<ThemedText.DeprecatedBlack textAlign="right" fontSize={14}>
<FormattedPriceImpact priceImpact={priceImpact} />
</ThemedText.DeprecatedBlack>
<ThemedText.BodySmall>~${trade.gasUseEstimateUSD.toFixed(2)}</ThemedText.BodySmall>
</TextWithLoadingPlaceholder>
</RowBetween>
<Separator />
)}
<RowBetween>
<RowFixed style={{ marginRight: '20px' }}>
<RowFixed>
<MouseoverTooltip
text={
<Trans>
The minimum amount you are guaranteed to receive. If the price slips any further, your transaction
will revert.
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>
<ThemedText.BodySmall color="textSecondary">
<Trans>Minimum output</Trans>
</ThemedText.BodySmall>
</MouseoverTooltip>
</RowFixed>
<TextWithLoadingPlaceholder syncing={syncing} width={70}>
<ThemedText.DeprecatedBlack textAlign="right" fontSize={14} color={theme.textTertiary}>
<ThemedText.BodySmall>
{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>
</ThemedText.BodySmall>
</TextWithLoadingPlaceholder>
</RowBetween>
{!trade?.gasUseEstimateUSD || !chainId || !SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) ? null : (
<RowBetween>
<RowFixed>
<MouseoverTooltip
text={
<Trans>
The fee paid to miners who process your transaction. This must be paid in {nativeCurrency.symbol}.
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.textTertiary}>
<Trans>Network Fee</Trans>
</ThemedText.DeprecatedSubHeader>
<ThemedText.BodySmall color="textSecondary">
<Trans>Expected output</Trans>
</ThemedText.BodySmall>
</MouseoverTooltip>
<TextWithLoadingPlaceholder syncing={syncing} width={50}>
<ThemedText.DeprecatedBlack textAlign="right" fontSize={14} color={theme.textTertiary}>
~${trade.gasUseEstimateUSD.toFixed(2)}
</ThemedText.DeprecatedBlack>
</RowFixed>
<TextWithLoadingPlaceholder syncing={syncing} width={65}>
<ThemedText.BodySmall>
{`${trade.outputAmount.toSignificant(6)} ${trade.outputAmount.currency.symbol}`}
</ThemedText.BodySmall>
</TextWithLoadingPlaceholder>
</RowBetween>
)}
</AutoColumn>
</StyledCard>
<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'
import { AutoColumn } from '../Column'
import { RowBetween, RowFixed } from '../Row'
import { MouseoverTooltip } from '../Tooltip'
import { formatPriceImpact } from './FormattedPriceImpact'
const StyledCard = styled(OutlineCard)`
padding: 12px;
......@@ -19,6 +18,8 @@ interface PriceImpactWarningProps {
priceImpact: Percent
}
const formatPriceImpact = (priceImpact: Percent) => `${priceImpact.multiply(-1).toFixed(2)}%`
export default function PriceImpactWarning({ priceImpact }: PriceImpactWarningProps) {
const theme = useTheme()
......
import { Trans } from '@lingui/macro'
import useAutoRouterSupported from 'hooks/useAutoRouterSupported'
import styled from 'styled-components/macro'
import { RouterPreference } from 'state/routing/slice'
import { useRouterPreference } from 'state/user/hooks'
import { ThemedText } from 'theme'
import { ReactComponent as StaticRouterIcon } from '../../assets/svg/static_route.svg'
import AutoRouterIcon from './AutoRouterIcon'
export default function RouterLabel() {
const [routerPreference] = useRouterPreference()
const StyledAutoRouterIcon = styled(AutoRouterIcon)`
height: 16px;
width: 16px;
:hover {
filter: brightness(1.3);
}
`
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;
switch (routerPreference) {
case RouterPreference.AUTO:
case RouterPreference.API:
return <ThemedText.BodySmall>Uniswap API</ThemedText.BodySmall>
case RouterPreference.CLIENT:
return <ThemedText.BodySmall>Uniswap Client</ThemedText.BodySmall>
}
`
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
import { useWeb3React } from '@web3-react/core'
import { useAccountDrawer } from 'components/AccountDrawer'
import { ButtonText } from 'components/Button'
import { MouseoverTooltipContent } from 'components/Tooltip'
import { MouseoverTooltip } from 'components/Tooltip'
import { useCallback, useEffect, useState } from 'react'
import { useBuyFiatFlowCompleted } from 'state/user/hooks'
import styled from 'styled-components/macro'
......@@ -109,9 +109,8 @@ export default function SwapBuyFiatButton() {
!fiatOnrampAvailabilityChecked || (fiatOnrampAvailabilityChecked && fiatOnrampAvailable)
return (
<MouseoverTooltipContent
wrap
content={
<MouseoverTooltip
text={
<div data-testid="fiat-on-ramp-unavailable-tooltip">
<Trans>Crypto purchases are not available in your region. </Trans>
<TraceEvent
......@@ -126,7 +125,7 @@ export default function SwapBuyFiatButton() {
</div>
}
placement="bottom"
disableHover={fiatOnRampsUnavailableTooltipDisabled}
disabled={fiatOnRampsUnavailableTooltipDisabled}
>
<TraceEvent
events={[BrowserEvent.onClick]}
......@@ -139,6 +138,6 @@ export default function SwapBuyFiatButton() {
{!buyFiatFlowCompleted && <Dot data-testid="buy-fiat-flow-incomplete-indicator" />}
</StyledTextButton>
</TraceEvent>
</MouseoverTooltipContent>
</MouseoverTooltip>
)
}
......@@ -38,6 +38,5 @@ describe('SwapDetailsDropdown.tsx', () => {
expect(screen.getByTestId('trade-price-container')).toBeInTheDocument()
await act(() => userEvent.click(screen.getByTestId('swap-details-header-row')))
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
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import AnimatedDropdown from 'components/AnimatedDropdown'
import { OutlineCard } from 'components/Card'
import { AutoColumn } from 'components/Column'
import Column from 'components/Column'
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 { useState } from 'react'
import { ChevronDown } from 'react-feather'
......@@ -16,24 +15,9 @@ import styled, { keyframes, useTheme } from 'styled-components/macro'
import { ThemedText } from 'theme'
import { AdvancedSwapDetails } from './AdvancedSwapDetails'
import GasEstimateBadge from './GasEstimateBadge'
import SwapRoute from './SwapRoute'
import GasEstimateTooltip from './GasEstimateTooltip'
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 }>`
padding: 0;
align-items: center;
......@@ -97,6 +81,16 @@ const Spinner = styled.div`
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 {
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
syncing: boolean
......@@ -110,8 +104,7 @@ export default function SwapDetailsDropdown({ trade, syncing, loading, allowedSl
const [showDetails, setShowDetails] = useState(false)
return (
<Wrapper style={{ marginTop: '0' }}>
<AutoColumn gap="sm" style={{ width: '100%', marginBottom: '-8px' }}>
<Wrapper>
<TraceEvent
events={[BrowserEvent.onClick]}
name={SwapEventName.SWAP_DETAILS_EXPANDED}
......@@ -124,7 +117,7 @@ export default function SwapDetailsDropdown({ trade, syncing, loading, allowedSl
disabled={!trade}
open={showDetails}
>
<RowFixed style={{ position: 'relative' }} align="center">
<RowFixed>
{Boolean(loading || syncing) && (
<StyledPolling>
<StyledPollingDot>
......@@ -147,12 +140,7 @@ export default function SwapDetailsDropdown({ trade, syncing, loading, allowedSl
showDetails ||
!chainId ||
!SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) ? null : (
<GasEstimateBadge
trade={trade}
loading={syncing || loading}
showRoute={!showDetails}
disableHover={showDetails}
/>
<GasEstimateTooltip trade={trade} loading={syncing || loading} disabled={showDetails} />
)}
<RotatingArrow
stroke={trade ? theme.textTertiary : theme.deprecated_bg3}
......@@ -161,17 +149,13 @@ export default function SwapDetailsDropdown({ trade, syncing, loading, allowedSl
</RowFixed>
</StyledHeaderRow>
</TraceEvent>
{trade && (
<AnimatedDropdown open={showDetails}>
<AutoColumn gap="sm" style={{ padding: '0', paddingBottom: '8px' }}>
{trade ? (
<StyledCard data-testid="advanced-swap-details">
<SwapDetailsWrapper data-testid="advanced-swap-details">
<AdvancedSwapDetails trade={trade} allowedSlippage={allowedSlippage} syncing={syncing} />
</StyledCard>
) : null}
{trade ? <SwapRoute data-testid="swap-route-info" trade={trade} syncing={syncing} /> : null}
</AutoColumn>
</SwapDetailsWrapper>
</AnimatedDropdown>
</AutoColumn>
)}
</Wrapper>
)
}
......@@ -16,12 +16,12 @@ import { Text } from 'rebass'
import { RouterPreference } from 'state/routing/slice'
import { InterfaceTrade } from 'state/routing/types'
import { useRouterPreference, useUserSlippageTolerance } from 'state/user/hooks'
import getRoutingDiagramEntries, { RoutingDiagramEntry } from 'utils/getRoutingDiagramEntries'
import { computeRealizedPriceImpact } from 'utils/prices'
import { ButtonError } from '../Button'
import { AutoRow } from '../Row'
import { SwapCallbackError } from './styleds'
import { getTokenPath, RoutingDiagramEntry } from './SwapRoute'
interface AnalyticsEventProps {
trade: InterfaceTrade<Currency, Currency, TradeType>
......@@ -125,7 +125,7 @@ export default function SwapModalFooter({
const transactionDeadlineSecondsSinceEpoch = useTransactionDeadline()?.toNumber() // in seconds since epoch
const isAutoSlippage = useUserSlippageTolerance()[0] === 'auto'
const [routerPreference] = useRouterPreference()
const routes = getTokenPath(trade)
const routes = getRoutingDiagramEntries(trade)
return (
<>
......
import { Trans } from '@lingui/macro'
import { TraceEvent } from '@uniswap/analytics'
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 { Currency, TradeType } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import AnimatedDropdown from 'components/AnimatedDropdown'
import { AutoColumn } from 'components/Column'
import Column from 'components/Column'
import { LoadingRows } from 'components/Loader/styled'
import RoutingDiagram from 'components/RoutingDiagram/RoutingDiagram'
import { AutoRow, RowBetween } from 'components/Row'
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
import useAutoRouterSupported from 'hooks/useAutoRouterSupported'
import { memo, useState } from 'react'
import { Plus } from 'react-feather'
import { InterfaceTrade } from 'state/routing/types'
import styled from 'styled-components/macro'
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 }>`
padding: ${({ fixedOpen }) => (fixedOpen ? '12px' : '12px 8px 12px 12px')};
border-radius: 16px;
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> {
export default function SwapRoute({
trade,
syncing,
}: {
trade: InterfaceTrade<Currency, Currency, TradeType>
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 autoRouterSupported = useAutoRouterSupported()
const [darkMode] = useDarkModeManager()
const routes = getRoutingDiagramEntries(trade)
const formattedGasPriceString = trade?.gasUseEstimateUSD
const gasPrice =
// TODO(WEB-3303)
// Can `trade.gasUseEstimateUSD` be defined when `chainId` is not in `SUPPORTED_GAS_ESTIMATE_CHAIN_IDS`?
trade.gasUseEstimateUSD && chainId && SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId)
? trade.gasUseEstimateUSD.toFixed(2) === '0.00'
? '<$0.01'
: '$' + trade.gasUseEstimateUSD.toFixed(2)
: undefined
return (
<Wrapper {...rest} darkMode={darkMode} fixedOpen={fixedOpen}>
<TraceEvent
events={[BrowserEvent.onClick]}
name={SwapEventName.SWAP_AUTOROUTER_VISUALIZATION_EXPANDED}
element={InterfaceElementName.AUTOROUTER_VISUALIZATION_ROW}
shouldLogImpression={!open}
>
<RowBetween onClick={() => setOpen(!open)}>
<AutoRow gap="4px" width="auto">
<AutoRouterLogo />
<AutoRouterLabel />
</AutoRow>
{fixedOpen ? null : <OpenCloseIcon open={open} />}
</RowBetween>
</TraceEvent>
<AnimatedDropdown open={open || fixedOpen}>
<AutoRow gap="4px" width="auto" style={{ paddingTop: '12px', margin: 0 }}>
<Column gap="md">
<RouterLabel />
<Separator />
{syncing ? (
<LoadingRows>
<div style={{ width: '400px', height: '30px' }} />
<div style={{ width: '100%', height: '30px' }} />
</LoadingRows>
) : (
<RoutingDiagram
......@@ -91,67 +48,24 @@ export default memo(function SwapRoute({ trade, syncing, fixedOpen = false, ...r
routes={routes}
/>
)}
{autoRouterSupported && (
<>
<Separator />
{syncing ? (
<LoadingRows>
<div style={{ width: '250px', height: '15px' }} />
<div style={{ width: '100%', 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}{' '}
<ThemedText.Caption color="textSecondary">
{gasPrice ? <Trans>Best price route costs ~{gasPrice} in gas.</Trans> : null}{' '}
<Trans>
This route optimizes your total output by considering split routes, multiple hops, and the gas cost
of each step.
This route optimizes your total output by considering split routes, multiple hops, and the gas cost of
each step.
</Trans>
</ThemedText.DeprecatedMain>
</ThemedText.Caption>
)}
</>
)}
</AutoRow>
</AnimatedDropdown>
</Wrapper>
</Column>
)
})
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`
flex-direction: row;
text-align: left;
flex-wrap: wrap;
padding: 8px 0;
user-select: text;
`
......@@ -60,9 +59,9 @@ export default function TradePrice({ price }: TradePriceProps) {
>
<ThemedText.BodySmall>{text}</ThemedText.BodySmall>{' '}
{usdPrice && (
<ThemedText.DeprecatedDarkGray>
<ThemedText.BodySmall color="textSecondary">
<Trans>({formatNumber(usdPrice, NumberType.FiatTokenPrice)})</Trans>
</ThemedText.DeprecatedDarkGray>
</ThemedText.BodySmall>
)}
</StyledPriceContainer>
)
......
......@@ -2,13 +2,13 @@
exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
<DocumentFragment>
.c0 {
.c2 {
box-sizing: border-box;
margin: 0;
min-width: 0;
}
.c4 {
.c3 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
......@@ -25,140 +25,129 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
justify-content: flex-start;
}
.c5 {
.c4 {
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
}
.c6 {
.c5 {
width: -webkit-fit-content;
width: -moz-fit-content;
width: fit-content;
}
.c7 {
color: #7780A0;
}
.c8 {
color: #0D111C;
}
.c10 {
.c1 {
width: 100%;
height: 1px;
background-color: #D2D9EE;
}
.c1 {
width: 100%;
padding: 1rem;
border-radius: 16px;
}
.c3 {
display: grid;
grid-auto-rows: auto;
grid-row-gap: 8px;
.c0 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 12px;
}
.c7 {
.c6 {
display: inline-block;
height: inherit;
}
.c9 {
color: #7780A0;
}
.c2 {
padding: 0;
}
<div
class="c0 c1 c2"
class="c0"
>
<div
class="c3"
>
class="c1"
/>
<div
class="c0 c4 c5"
class="c2 c3 c4"
>
<div
class="c0 c4 c6"
class="c2 c3 c5"
>
<div
class="c7"
class="c6"
>
<div>
<div
class="css-zhpkf8"
class="c7 css-zhpkf8"
>
Expected Output
Minimum output
</div>
</div>
</div>
</div>
<div
class="c8 css-q4yjm0"
class="c8 css-zhpkf8"
>
0.000000000000001 DEF
0.00000000000000098 DEF
</div>
</div>
<div
class="c0 c4 c5"
class="c2 c3 c4"
>
<div
class="c0 c4 c6"
class="c2 c3 c5"
>
<div
class="c7"
class="c6"
>
<div>
<div
class="css-zhpkf8"
class="c7 css-zhpkf8"
>
Price Impact
Expected output
</div>
</div>
</div>
</div>
<div
class="c8 css-q4yjm0"
class="c8 css-zhpkf8"
>
<div
class="c9 css-1aekuku"
>
105567.37%
</div>
0.000000000000001 DEF
</div>
</div>
<div
class="c10"
class="c1"
/>
<div
class="c0 c4 c5"
class="c2 c3 c4"
>
<div
class="c0 c4 c6"
style="margin-right: 20px;"
class="c7 css-zhpkf8"
>
Order routing
</div>
<div
class="c7"
class="c6"
>
<div>
<div
class="css-zhpkf8"
class="c8 css-zhpkf8"
>
Minimum received after slippage (2.00%)
</div>
Uniswap API
</div>
</div>
</div>
<div
class="css-q4yjm0"
>
0.00000000000000098 DEF
</div>
</div>
</div>
</div>
</DocumentFragment>
......
......@@ -81,7 +81,7 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
margin: -0px;
}
.c21 {
.c22 {
width: -webkit-fit-content;
width: -moz-fit-content;
width: fit-content;
......@@ -91,11 +91,11 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
color: #0D111C;
}
.c26 {
.c24 {
color: #7780A0;
}
.c24 {
.c21 {
width: 100%;
height: 1px;
background-color: #D2D9EE;
......@@ -118,6 +118,21 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
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 {
display: grid;
grid-auto-rows: auto;
......@@ -152,7 +167,7 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
background-size: 400%;
}
.c22 {
.c23 {
display: inline-block;
height: inherit;
}
......@@ -205,17 +220,12 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
-webkit-flex-wrap: wrap;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
padding: 8px 0;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
}
.c23 {
color: #7780A0;
}
.c10 {
text-overflow: ellipsis;
max-width: 220px;
......@@ -223,10 +233,6 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
text-align: right;
}
.c20 {
padding: 0;
}
.c15 {
padding: 4px;
border-radius: 12px;
......@@ -415,92 +421,82 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
style="padding: .75rem; margin-top: 0.5rem;"
>
<div
class="c5 c19 c20"
class="c20"
>
<div
class="c4"
>
class="c21"
/>
<div
class="c5 c6 c7"
>
<div
class="c5 c6 c21"
class="c5 c6 c22"
>
<div
class="c22"
class="c23"
>
<div>
<div
class="css-zhpkf8"
class="c24 css-zhpkf8"
>
Expected Output
Minimum output
</div>
</div>
</div>
</div>
<div
class="c18 css-q4yjm0"
class="c18 css-zhpkf8"
>
0.000000000000001 DEF
0.00000000000000098 DEF
</div>
</div>
<div
class="c5 c6 c7"
>
<div
class="c5 c6 c21"
class="c5 c6 c22"
>
<div
class="c22"
class="c23"
>
<div>
<div
class="css-zhpkf8"
class="c24 css-zhpkf8"
>
Price Impact
Expected output
</div>
</div>
</div>
</div>
<div
class="c18 css-q4yjm0"
>
<div
class="c23 css-1aekuku"
class="c18 css-zhpkf8"
>
105567.37%
</div>
0.000000000000001 DEF
</div>
</div>
<div
class="c24"
class="c21"
/>
<div
class="c5 c6 c7"
>
<div
class="c5 c6 c21"
style="margin-right: 20px;"
class="c24 css-zhpkf8"
>
Order routing
</div>
<div
class="c22"
class="c23"
>
<div>
<div
class="css-zhpkf8"
class="c18 css-zhpkf8"
>
Minimum received after slippage (2.00%)
Uniswap API
</div>
</div>
</div>
</div>
<div
class="css-q4yjm0"
>
0.00000000000000098 DEF
</div>
</div>
</div>
</div>
</div>
<div
......@@ -508,7 +504,7 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
style="padding: .75rem 1rem;"
>
<div
class="c26 css-k51stg"
class="c24 css-k51stg"
style="width: 100%;"
>
Output is estimated. You will receive at least
......@@ -524,7 +520,7 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
style="padding: 12px 0px 0px 0px;"
>
<div
class="c26 css-8mokm4"
class="c24 css-8mokm4"
>
Output will be sent to
<b
......
import { TooltipContainer } from 'components/Tooltip'
import { SupportedChainId } from 'constants/chains'
import { transparentize } from 'polished'
import { ReactNode } from 'react'
......@@ -64,17 +63,6 @@ export const ArrowWrapper = styled.div<{ clickable: boolean }>`
: 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)`
text-overflow: ellipsis;
max-width: 220px;
......@@ -151,15 +139,3 @@ export const SwapShowAcceptChanges = styled(AutoColumn)`
border-radius: 12px;
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'
import { SupportedChainId } from 'constants/chains'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
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 { 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,
const amountOut = chainId ? STABLECOIN_AMOUNT_OUT[chainId] : undefined
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(() => {
if (!currency || !stablecoin) {
return undefined
......
......@@ -3,7 +3,7 @@ import { Currency, CurrencyAmount, Price, SupportedChainId, TradeType } from '@u
import { nativeOnChain } from 'constants/tokens'
import { Chain, useTokenSpotPriceQuery } from 'graphql/data/__generated__/types-and-hooks'
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 { useRoutingAPITrade } from 'state/routing/useRoutingAPITrade'
import { getNativeTokenDBAddress } from 'utils/nativeTokens'
......@@ -30,7 +30,7 @@ function useETHValue(currencyAmount?: CurrencyAmount<Currency>): {
TradeType.EXACT_OUTPUT,
amountOut,
currencyAmount?.currency,
RouterPreference.PRICE
INTERNAL_ROUTER_PREFERENCE_PRICE
)
// Get ETH value of ETH or WETH
......
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
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
......@@ -18,7 +18,7 @@ export function useRoutingAPIArguments({
tokenOut: Currency | undefined
amount: CurrencyAmount<Currency> | undefined
tradeType: TradeType
routerPreference: RouterPreference
routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE
}) {
return useMemo(
() =>
......
......@@ -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-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(
......@@ -641,9 +633,9 @@ export function Swap({
</TraceEvent>
</ArrowWrapper>
</div>
<AutoColumn gap="md">
<AutoColumn gap="xs">
<div>
<OutputSwapSection showDetailsDropdown={showDetailsDropdown}>
<OutputSwapSection>
<Trace section={InterfaceSectionName.CURRENCY_OUTPUT_PANEL}>
<SwapCurrencyInputPanel
value={formattedAmounts[Field.OUTPUT]}
......@@ -676,17 +668,15 @@ export function Swap({
</>
) : null}
</OutputSwapSection>
</div>
{showDetailsDropdown && (
<DetailsSwapSection>
<SwapDetailsDropdown
trade={trade}
syncing={routeIsSyncing}
loading={routeIsLoading}
allowedSlippage={allowedSlippage}
/>
</DetailsSwapSection>
)}
</div>
{showPriceImpactWarning && <PriceImpactWarning priceImpact={largerPriceImpact} />}
<div>
{swapIsUnsupported ? (
......
......@@ -13,11 +13,12 @@ export enum RouterPreference {
AUTO = 'auto',
API = 'api',
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>()
function getRouter(chainId: ChainId): AlphaRouter {
const router = routers.get(chainId)
......@@ -78,7 +79,7 @@ interface GetQuoteArgs {
tokenOutDecimals: number
tokenOutSymbol?: string
amount: string
routerPreference: RouterPreference
routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE
type: 'exactIn' | 'exactOut'
}
......@@ -110,7 +111,7 @@ export const routingApi = createApi({
{
data: {
...args,
isPrice: args.routerPreference === RouterPreference.PRICE,
isPrice: args.routerPreference === INTERNAL_ROUTER_PREFERENCE_PRICE,
isAutoRouter:
args.routerPreference === RouterPreference.AUTO || args.routerPreference === RouterPreference.API,
},
......
......@@ -7,7 +7,7 @@ import { useStablecoinAmountFromFiatValue } from 'hooks/useStablecoinPrice'
import { useRoutingAPIArguments } from 'lib/hooks/routing/useRoutingAPIArguments'
import ms from 'ms.macro'
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 { computeRoutes, transformRoutesToTrade } from './utils'
......@@ -22,7 +22,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
tradeType: TTradeType,
amountSpecified: CurrencyAmount<Currency> | undefined,
otherCurrency: Currency | undefined,
routerPreference: RouterPreference
routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE
): {
state: TradeState
trade: InterfaceTrade<Currency, Currency, TTradeType> | undefined
......@@ -50,7 +50,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
currentData,
} = useGetQuoteQuery(queryArgs ?? skipToken, {
// 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
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