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:
parent
c3f12398cd
commit
aefbb3d812
@ -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}
|
||||||
|
26
src/lib/hooks/useUSDCPriceImpact.ts
Normal file
26
src/lib/hooks/useUSDCPriceImpact.ts
Normal file
@ -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}%`
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user