Commit e19e8492 authored by Zach Pomerantz's avatar Zach Pomerantz Committed by GitHub

feat: ux warnings (#3220)

* chore: mv Toolbar to a directory

* refactor: clean up Toolbar

* refactor: simplify Toolbar Caption

* feat: warn on price impact in Summary

* refactor: add computeRealizedPriceImpact util
parent 800b5e0b
import { t } from '@lingui/macro'
import { Trade } from '@uniswap/router-sdk'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { ALLOWED_PRICE_IMPACT_HIGH, ALLOWED_PRICE_IMPACT_MEDIUM } from 'constants/misc'
import { useAtom } from 'jotai'
import { integratorFeeAtom, MIN_HIGH_SLIPPAGE } from 'lib/state/settings'
import { Color, ThemedText } from 'lib/theme'
import { useMemo } from 'react'
import { currencyId } from 'utils/currencyId'
import { computeRealizedLPFeePercent } from 'utils/prices'
import { computeRealizedPriceImpact } from 'utils/prices'
import Row from '../../Row'
......@@ -36,21 +37,25 @@ export default function Details({ trade, allowedSlippage }: DetailsProps) {
const { inputAmount, outputAmount } = trade
const inputCurrency = inputAmount.currency
const outputCurrency = outputAmount.currency
const priceImpact = useMemo(() => computeRealizedPriceImpact(trade), [trade])
const integrator = window.location.hostname
const [integratorFee] = useAtom(integratorFeeAtom)
const priceImpact = useMemo(() => {
const realizedLpFeePercent = computeRealizedLPFeePercent(trade)
return trade.priceImpact.subtract(realizedLpFeePercent)
}, [trade])
const details = useMemo(() => {
// @TODO(ianlapham): Check that provider fee is even a valid list item
return [
// [t`Liquidity provider fee`, `${swap.lpFee} ${inputSymbol}`],
[t`${integrator} fee`, integratorFee && `${integratorFee} ${currencyId(inputCurrency)}`],
[t`Price impact`, `${priceImpact.toFixed(2)}%`],
[
t`Price impact`,
`${priceImpact.toFixed(2)}%`,
priceImpact >= ALLOWED_PRICE_IMPACT_HIGH
? 'error'
: priceImpact >= ALLOWED_PRICE_IMPACT_MEDIUM
? 'warning'
: undefined,
],
trade.tradeType === TradeType.EXACT_INPUT
? [t`Maximum sent`, `${trade.maximumAmountIn(allowedSlippage).toSignificant(6)} ${inputCurrency.symbol}`]
: [],
......
import { Trans } from '@lingui/macro'
import { Trade } from '@uniswap/router-sdk'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { ALLOWED_PRICE_IMPACT_HIGH, ALLOWED_PRICE_IMPACT_MEDIUM } from 'constants/misc'
import { useAtomValue } from 'jotai/utils'
import { IconButton } from 'lib/components/Button'
import useScrollbar from 'lib/hooks/useScrollbar'
......@@ -9,6 +10,7 @@ import { MIN_HIGH_SLIPPAGE } from 'lib/state/settings'
import { Field, independentFieldAtom } from 'lib/state/swap'
import styled, { ThemedText } from 'lib/theme'
import { useMemo, useState } from 'react'
import { computeRealizedPriceImpact } from 'utils/prices'
import { tradeMeaningfullyDiffers } from 'utils/tradeMeaningFullyDiffer'
import ActionButton from '../../ActionButton'
......@@ -83,14 +85,19 @@ interface SummaryDialogProps {
}
export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDialogProps) {
const { inputAmount, outputAmount } = trade
const { inputAmount, outputAmount, executionPrice } = trade
const inputCurrency = inputAmount.currency
const outputCurrency = outputAmount.currency
const price = trade.executionPrice
const priceImpact = useMemo(() => computeRealizedPriceImpact(trade), [trade])
const independentField = useAtomValue(independentFieldAtom)
const slippageWarning = useMemo(() => allowedSlippage.greaterThan(MIN_HIGH_SLIPPAGE), [allowedSlippage])
const warning = useMemo(() => {
if (priceImpact >= ALLOWED_PRICE_IMPACT_HIGH) return 'error'
if (priceImpact >= ALLOWED_PRICE_IMPACT_MEDIUM) return 'warning'
if (allowedSlippage.greaterThan(MIN_HIGH_SLIPPAGE)) return 'warning'
return
}, [allowedSlippage, priceImpact])
const [confirmedTrade, setConfirmedTrade] = useState(trade)
const doesTradeDiffer = useMemo(
......@@ -114,13 +121,13 @@ export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDial
<SummaryColumn gap={0.75} flex justify="center">
<Summary input={inputAmount} output={outputAmount} usdc={true} />
<ThemedText.Caption>
1 {inputCurrency.symbol} = {price?.toSignificant(6)} {outputCurrency.symbol}
1 {inputCurrency.symbol} = {executionPrice?.toSignificant(6)} {outputCurrency.symbol}
</ThemedText.Caption>
</SummaryColumn>
<Rule />
<Row>
<Row gap={0.5}>
{slippageWarning ? <AlertTriangle color="warning" /> : <Info color="secondary" />}
{warning ? <AlertTriangle color={warning} /> : <Info color="secondary" />}
<ThemedText.Subhead2 color="secondary">
<Trans>Swap details</Trans>
</ThemedText.Subhead2>
......
import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { ALL_SUPPORTED_CHAIN_IDS } from 'constants/chains'
import useUSDCPrice from 'hooks/useUSDCPrice'
import { useSwapInfo } from 'lib/hooks/swap'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import { AlertTriangle, Info, largeIconCss, Spinner } from 'lib/icons'
import { Field } from 'lib/state/swap'
import styled, { ThemedText } from 'lib/theme'
import { useMemo, useState } from 'react'
import { InterfaceTrade, TradeState } from 'state/routing/types'
import { TextButton } from '../Button'
import Row from '../Row'
import Rule from '../Rule'
const ToolbarRow = styled(Row)`
padding: 0.5em 0;
${largeIconCss}
`
function RoutingTooltip() {
return <Info color="secondary" />
/* TODO(zzmp): Implement post-beta launch.
return (
<Tooltip icon={Info} placement="bottom">
<ThemeProvider>
<ThemedText.Subhead2>TODO: Routing Tooltip</ThemedText.Subhead2>
</ThemeProvider>
</Tooltip>
)
*/
}
interface LoadedStateProps {
inputAmount: CurrencyAmount<Currency>
outputAmount: CurrencyAmount<Currency>
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
}
function LoadedState({ inputAmount, outputAmount, trade }: LoadedStateProps) {
const [flip, setFlip] = useState(true)
const executionPrice = trade?.executionPrice
const fiatValueInput = useUSDCPrice(inputAmount.currency)
const fiatValueOutput = useUSDCPrice(outputAmount.currency)
const ratio = useMemo(() => {
const [a, b] = flip ? [outputAmount, inputAmount] : [inputAmount, outputAmount]
const priceString = (!flip ? executionPrice : executionPrice?.invert())?.toSignificant(6)
const ratio = `1 ${a.currency.symbol} = ${priceString}} ${b.currency.symbol}`
const usdc = !flip
? fiatValueInput
? ` ($${fiatValueInput.toSignificant(2)})`
: null
: fiatValueOutput
? ` ($${fiatValueOutput.toSignificant(2)})`
: null
return (
<Row gap={0.25} style={{ userSelect: 'text' }}>
{ratio}
{usdc && <ThemedText.Caption color="secondary">{usdc}</ThemedText.Caption>}
</Row>
)
}, [executionPrice, fiatValueInput, fiatValueOutput, flip, inputAmount, outputAmount])
return (
<TextButton color="primary" onClick={() => setFlip(!flip)}>
{ratio}
</TextButton>
)
}
export default function Toolbar({ disabled }: { disabled?: boolean }) {
const { chainId } = useActiveWeb3React()
const {
trade,
currencies: { [Field.INPUT]: inputCurrency, [Field.OUTPUT]: outputCurency },
currencyBalances: { [Field.INPUT]: balance },
currencyAmounts: { [Field.INPUT]: inputAmount, [Field.OUTPUT]: outputAmount },
} = useSwapInfo()
const [routeFound, routeIsLoading, routeIsSyncing] = useMemo(
() => [Boolean(trade?.trade?.swaps), TradeState.LOADING === trade?.state, TradeState.SYNCING === trade?.state],
[trade]
)
const caption = useMemo(() => {
if (disabled) {
return (
<>
<AlertTriangle color="secondary" />
<Trans>Connect wallet to swap</Trans>
</>
)
}
if (chainId && !ALL_SUPPORTED_CHAIN_IDS.includes(chainId)) {
return (
<>
<AlertTriangle color="secondary" />
<Trans>Unsupported network&#8211;switch to another to trade.</Trans>
</>
)
}
if (inputCurrency && outputCurency) {
if (!trade?.trade || routeIsLoading || routeIsSyncing) {
return (
<>
<Spinner color="secondary" />
<Trans>Fetching best price…</Trans>
</>
)
}
if (inputAmount && balance && inputAmount.greaterThan(balance)) {
return (
<>
<AlertTriangle color="secondary" />
<Trans>Insufficient {inputCurrency?.symbol}</Trans>
</>
)
}
if (inputCurrency && outputCurency && !routeFound && !routeIsLoading && !routeIsSyncing) {
return (
<>
<AlertTriangle color="secondary" />
<Trans>Insufficient liquidity for this trade.</Trans>
</>
)
}
if (inputCurrency && inputAmount && outputCurency && outputAmount) {
return (
<>
<RoutingTooltip />
<LoadedState inputAmount={inputAmount} outputAmount={outputAmount} trade={trade?.trade} />
</>
)
}
}
return (
<>
<Info color="secondary" />
<Trans>Enter an amount</Trans>
</>
)
}, [
balance,
chainId,
disabled,
inputAmount,
inputCurrency,
outputAmount,
outputCurency,
routeFound,
routeIsLoading,
routeIsSyncing,
trade?.trade,
])
return (
<>
<Rule />
<ThemedText.Caption>
<ToolbarRow justify="flex-start" gap={0.5} iconSize={4 / 3}>
{caption}
</ToolbarRow>
</ThemedText.Caption>
</>
)
}
import { Trans } from '@lingui/macro'
import { Currency, TradeType } from '@uniswap/sdk-core'
import useUSDCPrice from 'hooks/useUSDCPrice'
import { AlertTriangle, Icon, Info, Spinner } from 'lib/icons'
import { ThemedText } from 'lib/theme'
import { ReactNode, useMemo, useState } from 'react'
import { InterfaceTrade } from 'state/routing/types'
import { TextButton } from '../../Button'
import Row from '../../Row'
import RoutingTooltip from './RoutingTooltip'
interface CaptionProps {
icon?: Icon
caption: ReactNode
}
function Caption({ icon: Icon = AlertTriangle, caption }: CaptionProps) {
return (
<>
<Icon color="secondary" />
{caption}
</>
)
}
export function ConnectWallet() {
return <Caption caption={<Trans>Connect wallet to swap</Trans>} />
}
export function UnsupportedNetwork() {
return <Caption caption={<Trans>Unsupported network - switch to another network to swap</Trans>} />
}
export function InsufficientBalance({ currency }: { currency: Currency }) {
return <Caption caption={<Trans>Insufficient {currency?.symbol} balance</Trans>} />
}
export function InsufficientLiquidity() {
return <Caption caption={<Trans>Insufficient liquidity in the pool for your trade</Trans>} />
}
export function Empty() {
return <Caption icon={Info} caption={<Trans>Enter an amount</Trans>} />
}
export function LoadingTrade() {
return <Caption icon={Spinner} caption={<Trans>Fetching best price…</Trans>} />
}
export function Trade({ trade }: { trade: InterfaceTrade<Currency, Currency, TradeType> }) {
const [flip, setFlip] = useState(true)
const { inputAmount, outputAmount, executionPrice } = trade
const fiatValueInput = useUSDCPrice(inputAmount.currency)
const fiatValueOutput = useUSDCPrice(outputAmount.currency)
const ratio = useMemo(() => {
const [a, b] = flip ? [outputAmount, inputAmount] : [inputAmount, outputAmount]
const priceString = (!flip ? executionPrice : executionPrice?.invert())?.toSignificant(6)
const ratio = `1 ${a.currency.symbol} = ${priceString}} ${b.currency.symbol}`
const usdc = !flip
? fiatValueInput
? ` ($${fiatValueInput.toSignificant(2)})`
: null
: fiatValueOutput
? ` ($${fiatValueOutput.toSignificant(2)})`
: null
return (
<Row gap={0.25} style={{ userSelect: 'text' }}>
{ratio}
{usdc && <ThemedText.Caption color="secondary">{usdc}</ThemedText.Caption>}
</Row>
)
}, [executionPrice, fiatValueInput, fiatValueOutput, flip, inputAmount, outputAmount])
return (
<>
<RoutingTooltip />
<TextButton color="primary" onClick={() => setFlip(!flip)}>
{ratio}
</TextButton>
</>
)
}
import { Info } from 'lib/icons'
export default function RoutingTooltip() {
return <Info color="secondary" />
/* TODO(zzmp): Implement post-beta launch.
return (
<Tooltip icon={Info} placement="bottom">
<ThemeProvider>
<ThemedText.Subhead2>TODO: Routing Tooltip</ThemedText.Subhead2>
</ThemeProvider>
</Tooltip>
)
*/
}
import { ALL_SUPPORTED_CHAIN_IDS } from 'constants/chains'
import { useSwapInfo } from 'lib/hooks/swap'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import { largeIconCss } from 'lib/icons'
import { Field } from 'lib/state/swap'
import styled, { ThemedText } from 'lib/theme'
import { useMemo } from 'react'
import { TradeState } from 'state/routing/types'
import Row from '../../Row'
import Rule from '../../Rule'
import * as Caption from './Caption'
const ToolbarRow = styled(Row)`
padding: 0.5em 0;
${largeIconCss}
`
export default function Toolbar({ disabled }: { disabled?: boolean }) {
const { chainId } = useActiveWeb3React()
const {
trade: { trade, state },
currencies: { [Field.INPUT]: inputCurrency, [Field.OUTPUT]: outputCurrency },
currencyBalances: { [Field.INPUT]: balance },
} = useSwapInfo()
const [routeFound, routeIsLoading] = useMemo(
() => [Boolean(trade?.swaps), TradeState.LOADING === state || TradeState.SYNCING === state],
[state, trade?.swaps]
)
const caption = useMemo(() => {
if (disabled) {
return <Caption.ConnectWallet />
}
if (chainId && !ALL_SUPPORTED_CHAIN_IDS.includes(chainId)) {
return <Caption.UnsupportedNetwork />
}
if (balance && trade?.inputAmount.greaterThan(balance)) {
return <Caption.InsufficientBalance currency={trade.inputAmount.currency} />
}
if (inputCurrency && outputCurrency) {
if (!trade || routeIsLoading) {
return <Caption.LoadingTrade />
}
if (!routeFound) {
return <Caption.InsufficientLiquidity />
}
if (trade.inputAmount && trade.outputAmount) {
return <Caption.Trade trade={trade} />
}
}
return <Caption.Empty />
}, [balance, chainId, disabled, inputCurrency, outputCurrency, routeFound, routeIsLoading, trade])
return (
<>
<Rule />
<ThemedText.Caption>
<ToolbarRow justify="flex-start" gap={0.5} iconSize={4 / 3}>
{caption}
</ToolbarRow>
</ThemedText.Caption>
</>
)
}
......@@ -16,6 +16,11 @@ import {
const THIRTY_BIPS_FEE = new Percent(JSBI.BigInt(30), JSBI.BigInt(10000))
const INPUT_FRACTION_AFTER_FEE = ONE_HUNDRED_PERCENT.subtract(THIRTY_BIPS_FEE)
export function computeRealizedPriceImpact(trade: Trade<Currency, Currency, TradeType>): Percent {
const realizedLpFeePercent = computeRealizedLPFeePercent(trade)
return trade.priceImpact.subtract(realizedLpFeePercent)
}
// computes realized lp fee as a percent
export function computeRealizedLPFeePercent(trade: Trade<Currency, Currency, TradeType>): Percent {
let percent: Percent
......
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