feat: price impact warning (#3437)

* chore: mv usdc price impact to hook

* fix: popover padding

* feat: warn on high price impact from toolbar

* fix: display price impact on warning too

* chore: rename useUSDCValue params

* fix: conform uses of price impact color
This commit is contained in:
Zach Pomerantz 2022-03-08 09:53:40 -08:00 committed by GitHub
parent c3f12398cd
commit aefbb3d812
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 83 additions and 70 deletions

@ -14,7 +14,7 @@ const PopoverContainer = styled.div<{ show: boolean }>`
border: 1px solid ${({ theme }) => theme.outline}; border: 1px solid ${({ theme }) => theme.outline};
border-radius: 0.5em; border-radius: 0.5em;
opacity: ${(props) => (props.show ? 1 : 0)}; opacity: ${(props) => (props.show ? 1 : 0)};
padding: 8px; padding: 10px 12px;
transition: visibility 0.25s linear, opacity 0.25s linear; transition: visibility 0.25s linear, opacity 0.25s linear;
visibility: ${(props) => (props.show ? 'visible' : 'hidden')}; visibility: ${(props) => (props.show ? 'visible' : 'hidden')};
z-index: ${Layer.TOOLTIP}; z-index: ${Layer.TOOLTIP};

@ -1,16 +1,15 @@
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { useLingui } from '@lingui/react' import { useLingui } from '@lingui/react'
import { useUSDCValue } from 'hooks/useUSDCPrice'
import { atom } from 'jotai' import { atom } from 'jotai'
import { useAtomValue } from 'jotai/utils' import { useAtomValue } from 'jotai/utils'
import BrandedFooter from 'lib/components/BrandedFooter' import BrandedFooter from 'lib/components/BrandedFooter'
import { useIsSwapFieldIndependent, useSwapAmount, useSwapCurrency, useSwapInfo } from 'lib/hooks/swap' import { useIsSwapFieldIndependent, useSwapAmount, useSwapCurrency, useSwapInfo } from 'lib/hooks/swap'
import useCurrencyColor from 'lib/hooks/useCurrencyColor' import useCurrencyColor from 'lib/hooks/useCurrencyColor'
import useUSDCPriceImpact, { toHumanReadablePriceImpact } from 'lib/hooks/useUSDCPriceImpact'
import { Field } from 'lib/state/swap' import { Field } from 'lib/state/swap'
import styled, { DynamicThemeProvider, ThemedText } from 'lib/theme' import styled, { DynamicThemeProvider, ThemedText } from 'lib/theme'
import { PropsWithChildren, useMemo } from 'react' import { PropsWithChildren, useMemo } from 'react'
import { TradeState } from 'state/routing/types' import { TradeState } from 'state/routing/types'
import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount' import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
import { getPriceImpactWarning } from 'utils/prices' import { getPriceImpactWarning } from 'utils/prices'
@ -60,23 +59,8 @@ export default function Output({ disabled, focused, children }: PropsWithChildre
// different state true/null/false allow smoother color transition // different state true/null/false allow smoother color transition
const hasColor = swapOutputCurrency ? Boolean(color) || null : false const hasColor = swapOutputCurrency ? Boolean(color) || null : false
const inputUSDC = useUSDCValue(inputCurrencyAmount) const { outputUSDC, priceImpact } = useUSDCPriceImpact(inputCurrencyAmount, outputCurrencyAmount)
const outputUSDC = useUSDCValue(outputCurrencyAmount) const priceImpactWarning = useMemo(() => getPriceImpactWarning(priceImpact), [priceImpact])
const priceImpact = useMemo(() => {
const fiatValuePriceImpact = computeFiatValuePriceImpact(inputUSDC, outputUSDC)
if (!fiatValuePriceImpact) return null
const color = getPriceImpactWarning(fiatValuePriceImpact)
const sign = fiatValuePriceImpact.lessThan(0) ? '+' : ''
const displayedPriceImpact = parseFloat(fiatValuePriceImpact.multiply(-1)?.toSignificant(3))
return (
<ThemedText.Body2 color={color}>
({sign}
{displayedPriceImpact}%)
</ThemedText.Body2>
)
}, [inputUSDC, outputUSDC])
const amount = useFormattedFieldAmount({ const amount = useFormattedFieldAmount({
disabled, disabled,
@ -103,7 +87,12 @@ export default function Output({ disabled, focused, children }: PropsWithChildre
<ThemedText.Body2 color="secondary" userSelect> <ThemedText.Body2 color="secondary" userSelect>
<Row> <Row>
<USDC gap={0.5} isLoading={isRouteLoading}> <USDC gap={0.5} isLoading={isRouteLoading}>
{outputUSDC ? `$${outputUSDC.toFixed(2)}` : '-'} {priceImpact} {outputUSDC ? `$${outputUSDC.toFixed(2)}` : '-'}{' '}
{priceImpact && (
<ThemedText.Body2 color={priceImpactWarning}>
({toHumanReadablePriceImpact(priceImpact)})
</ThemedText.Body2>
)}
</USDC> </USDC>
{balance && ( {balance && (
<Balance focused={focused}> <Balance focused={focused}>

@ -1,39 +1,25 @@
import { useLingui } from '@lingui/react' import { useLingui } from '@lingui/react'
import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core'
import { useUSDCValue } from 'hooks/useUSDCPrice' import useUSDCPriceImpact, { toHumanReadablePriceImpact } from 'lib/hooks/useUSDCPriceImpact'
import { ArrowRight } from 'lib/icons' import { ArrowRight } from 'lib/icons'
import styled from 'lib/theme'
import { ThemedText } from 'lib/theme' import { ThemedText } from 'lib/theme'
import { useMemo } from 'react' import { useMemo } from 'react'
import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount' import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
import { getPriceImpactWarning } from 'utils/prices'
import Column from '../../Column' import Column from '../../Column'
import Row from '../../Row' import Row from '../../Row'
import TokenImg from '../../TokenImg' import TokenImg from '../../TokenImg'
const Percent = styled.span<{ gain: boolean }>`
color: ${({ gain, theme }) => (gain ? theme.success : theme.error)};
`
interface TokenValueProps { interface TokenValueProps {
input: CurrencyAmount<Currency> input: CurrencyAmount<Currency>
usdc?: boolean usdc?: CurrencyAmount<Token>
change?: number priceImpact?: Percent
} }
function TokenValue({ input, usdc, change }: TokenValueProps) { function TokenValue({ input, usdc, priceImpact }: TokenValueProps) {
const { i18n } = useLingui() const { i18n } = useLingui()
const percent = useMemo(() => { const priceImpactWarning = useMemo(() => getPriceImpactWarning(priceImpact), [priceImpact])
if (change) {
const percent = change.toPrecision(3)
return change > 0 ? `(+${percent}%)` : `(${percent}%)`
}
return undefined
}, [change])
const usdcAmount = useUSDCValue(input)
return ( return (
<Column justify="flex-start"> <Column justify="flex-start">
<Row gap={0.375} justify="flex-start"> <Row gap={0.375} justify="flex-start">
@ -42,11 +28,15 @@ function TokenValue({ input, usdc, change }: TokenValueProps) {
{formatCurrencyAmount(input, 6, i18n.locale)} {input.currency.symbol} {formatCurrencyAmount(input, 6, i18n.locale)} {input.currency.symbol}
</ThemedText.Body2> </ThemedText.Body2>
</Row> </Row>
{usdc && usdcAmount && ( {usdc && (
<Row justify="flex-start"> <Row justify="flex-start">
<ThemedText.Caption color="secondary" userSelect> <ThemedText.Caption color="secondary" userSelect>
${formatCurrencyAmount(usdcAmount, 2, i18n.locale)} ${formatCurrencyAmount(usdc, 2, i18n.locale)}
{change && <Percent gain={change > 0}> {percent}</Percent>} {priceImpact && (
<ThemedText.Caption color={priceImpactWarning}>
({toHumanReadablePriceImpact(priceImpact)})
</ThemedText.Caption>
)}
</ThemedText.Caption> </ThemedText.Caption>
</Row> </Row>
)} )}
@ -57,23 +47,17 @@ function TokenValue({ input, usdc, change }: TokenValueProps) {
interface SummaryProps { interface SummaryProps {
input: CurrencyAmount<Currency> input: CurrencyAmount<Currency>
output: CurrencyAmount<Currency> output: CurrencyAmount<Currency>
usdc?: boolean showUSDC?: true
} }
export default function Summary({ input, output, usdc }: SummaryProps) { export default function Summary({ input, output, showUSDC }: SummaryProps) {
const inputUSDCValue = useUSDCValue(input) const { inputUSDC, outputUSDC, priceImpact } = useUSDCPriceImpact(input, output)
const outputUSDCValue = useUSDCValue(output)
const priceImpact = useMemo(() => {
const computedChange = computeFiatValuePriceImpact(inputUSDCValue, outputUSDCValue)
return computedChange ? parseFloat(computedChange.multiply(-1)?.toSignificant(3)) : undefined
}, [inputUSDCValue, outputUSDCValue])
return ( return (
<Row gap={usdc ? 1 : 0.25}> <Row gap={showUSDC ? 1 : 0.25}>
<TokenValue input={input} usdc={usdc} /> <TokenValue input={input} usdc={showUSDC && inputUSDC} />
<ArrowRight /> <ArrowRight />
<TokenValue input={output} usdc={usdc} change={priceImpact} /> <TokenValue input={output} usdc={showUSDC && outputUSDC} priceImpact={priceImpact} />
</Row> </Row>
) )
} }

@ -134,7 +134,7 @@ export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDial
<Header title={<Trans>Swap summary</Trans>} ruled /> <Header title={<Trans>Swap summary</Trans>} ruled />
<Body flex align="stretch" gap={0.75} padded open={open}> <Body flex align="stretch" gap={0.75} padded open={open}>
<SummaryColumn gap={0.75} flex justify="center"> <SummaryColumn gap={0.75} flex justify="center">
<Summary input={inputAmount} output={outputAmount} usdc={true} /> <Summary input={inputAmount} output={outputAmount} showUSDC />
<Row> <Row>
<ThemedText.Caption userSelect> <ThemedText.Caption userSelect>
{formatLocaleNumber({ number: 1, sigFigs: 1, locale: i18n.locale })} {inputCurrency.symbol} ={' '} {formatLocaleNumber({ number: 1, sigFigs: 1, locale: i18n.locale })} {inputCurrency.symbol} ={' '}

@ -1,13 +1,16 @@
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { Currency, TradeType } from '@uniswap/sdk-core' import { Currency, TradeType } from '@uniswap/sdk-core'
import useUSDCPrice from 'hooks/useUSDCPrice' import Column from 'lib/components/Column'
import Rule from 'lib/components/Rule'
import Tooltip from 'lib/components/Tooltip' import Tooltip from 'lib/components/Tooltip'
import { loadingCss } from 'lib/css/loading' import { loadingCss } from 'lib/css/loading'
import { WrapType } from 'lib/hooks/swap/useWrapCallback' import { WrapType } from 'lib/hooks/swap/useWrapCallback'
import useUSDCPriceImpact, { toHumanReadablePriceImpact } from 'lib/hooks/useUSDCPriceImpact'
import { AlertTriangle, Icon, Info, InlineSpinner } from 'lib/icons' import { AlertTriangle, Icon, Info, InlineSpinner } from 'lib/icons'
import styled, { ThemedText } from 'lib/theme' import styled, { ThemedText } from 'lib/theme'
import { ReactNode, useCallback, useMemo, useState } from 'react' import { ReactNode, useCallback, useMemo, useState } from 'react'
import { InterfaceTrade } from 'state/routing/types' import { InterfaceTrade } from 'state/routing/types'
import { getPriceImpactWarning } from 'utils/prices'
import { TextButton } from '../../Button' import { TextButton } from '../../Button'
import Row from '../../Row' import Row from '../../Row'
@ -78,21 +81,21 @@ export function WrapCurrency({ loading, wrapType }: { loading: boolean; wrapType
export function Trade({ trade }: { trade: InterfaceTrade<Currency, Currency, TradeType> }) { export function Trade({ trade }: { trade: InterfaceTrade<Currency, Currency, TradeType> }) {
const [flip, setFlip] = useState(true) const [flip, setFlip] = useState(true)
const { inputAmount, outputAmount, executionPrice } = trade const { inputAmount: input, outputAmount: output, executionPrice } = trade
const fiatValueInput = useUSDCPrice(inputAmount.currency) const { inputUSDC, outputUSDC, priceImpact } = useUSDCPriceImpact(input, output)
const fiatValueOutput = useUSDCPrice(outputAmount.currency) const isPriceImpactHigh = priceImpact && getPriceImpactWarning(priceImpact)
const ratio = useMemo(() => { const ratio = useMemo(() => {
const [a, b] = flip ? [outputAmount, inputAmount] : [inputAmount, outputAmount] const [a, b] = flip ? [output, input] : [input, output]
const priceString = (!flip ? executionPrice : executionPrice?.invert())?.toSignificant(6) const priceString = (!flip ? executionPrice : executionPrice?.invert())?.toSignificant(6)
const ratio = `1 ${a.currency.symbol} = ${priceString} ${b.currency.symbol}` const ratio = `1 ${a.currency.symbol} = ${priceString} ${b.currency.symbol}`
const usdc = !flip const usdc = !flip
? fiatValueInput ? inputUSDC
? ` ($${fiatValueInput.toSignificant(6)})` ? ` ($${inputUSDC.toSignificant(6)})`
: null : null
: fiatValueOutput : outputUSDC
? ` ($${fiatValueOutput.toSignificant(6)})` ? ` ($${outputUSDC.toSignificant(6)})`
: null : null
return ( return (
@ -103,12 +106,23 @@ export function Trade({ trade }: { trade: InterfaceTrade<Currency, Currency, Tra
</Row> </Row>
</ThemedText.Caption> </ThemedText.Caption>
) )
}, [executionPrice, fiatValueInput, fiatValueOutput, flip, inputAmount, outputAmount]) }, [executionPrice, inputUSDC, outputUSDC, flip, input, output])
return ( return (
<> <>
<Tooltip placement="bottom" icon={Info}> <Tooltip placement="bottom" icon={isPriceImpactHigh ? AlertTriangle : Info}>
<Column gap={0.75}>
{isPriceImpactHigh && (
<>
<ThemedText.Caption>
The output amount is estimated at {toHumanReadablePriceImpact(priceImpact)} less than the input amount
due to high price impact
</ThemedText.Caption>
<Rule />
</>
)}
<RoutingDiagram trade={trade} /> <RoutingDiagram trade={trade} />
</Column>
</Tooltip> </Tooltip>
<TextButton color="primary" onClick={() => setFlip(!flip)}> <TextButton color="primary" onClick={() => setFlip(!flip)}>
{ratio} {ratio}

@ -0,0 +1,26 @@
import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core'
import { useUSDCValue } from 'hooks/useUSDCPrice'
import { useMemo } from 'react'
import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
export default function useUSDCPriceImpact(
inputAmount: CurrencyAmount<Currency> | undefined,
outputAmount: CurrencyAmount<Currency> | undefined
): {
inputUSDC?: CurrencyAmount<Token>
outputUSDC?: CurrencyAmount<Token>
priceImpact?: Percent
} {
const inputUSDC = useUSDCValue(inputAmount) ?? undefined
const outputUSDC = useUSDCValue(outputAmount) ?? undefined
return useMemo(() => {
const priceImpact = computeFiatValuePriceImpact(inputUSDC, outputUSDC)
return { inputUSDC, outputUSDC, priceImpact }
}, [inputUSDC, outputUSDC])
}
export function toHumanReadablePriceImpact(priceImpact: Percent): string {
const sign = priceImpact.lessThan(0) ? '+' : ''
const number = parseFloat(priceImpact.multiply(-1)?.toSignificant(3))
return `${sign}${number}%`
}