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-radius: 0.5em;
|
||||
opacity: ${(props) => (props.show ? 1 : 0)};
|
||||
padding: 8px;
|
||||
padding: 10px 12px;
|
||||
transition: visibility 0.25s linear, opacity 0.25s linear;
|
||||
visibility: ${(props) => (props.show ? 'visible' : 'hidden')};
|
||||
z-index: ${Layer.TOOLTIP};
|
||||
|
@ -1,16 +1,15 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useLingui } from '@lingui/react'
|
||||
import { useUSDCValue } from 'hooks/useUSDCPrice'
|
||||
import { atom } from 'jotai'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import BrandedFooter from 'lib/components/BrandedFooter'
|
||||
import { useIsSwapFieldIndependent, useSwapAmount, useSwapCurrency, useSwapInfo } from 'lib/hooks/swap'
|
||||
import useCurrencyColor from 'lib/hooks/useCurrencyColor'
|
||||
import useUSDCPriceImpact, { toHumanReadablePriceImpact } from 'lib/hooks/useUSDCPriceImpact'
|
||||
import { Field } from 'lib/state/swap'
|
||||
import styled, { DynamicThemeProvider, ThemedText } from 'lib/theme'
|
||||
import { PropsWithChildren, useMemo } from 'react'
|
||||
import { TradeState } from 'state/routing/types'
|
||||
import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
|
||||
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
|
||||
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
|
||||
const hasColor = swapOutputCurrency ? Boolean(color) || null : false
|
||||
|
||||
const inputUSDC = useUSDCValue(inputCurrencyAmount)
|
||||
const outputUSDC = useUSDCValue(outputCurrencyAmount)
|
||||
|
||||
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 { outputUSDC, priceImpact } = useUSDCPriceImpact(inputCurrencyAmount, outputCurrencyAmount)
|
||||
const priceImpactWarning = useMemo(() => getPriceImpactWarning(priceImpact), [priceImpact])
|
||||
|
||||
const amount = useFormattedFieldAmount({
|
||||
disabled,
|
||||
@ -103,7 +87,12 @@ export default function Output({ disabled, focused, children }: PropsWithChildre
|
||||
<ThemedText.Body2 color="secondary" userSelect>
|
||||
<Row>
|
||||
<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>
|
||||
{balance && (
|
||||
<Balance focused={focused}>
|
||||
|
@ -1,39 +1,25 @@
|
||||
import { useLingui } from '@lingui/react'
|
||||
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
|
||||
import { useUSDCValue } from 'hooks/useUSDCPrice'
|
||||
import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core'
|
||||
import useUSDCPriceImpact, { toHumanReadablePriceImpact } from 'lib/hooks/useUSDCPriceImpact'
|
||||
import { ArrowRight } from 'lib/icons'
|
||||
import styled from 'lib/theme'
|
||||
import { ThemedText } from 'lib/theme'
|
||||
import { useMemo } from 'react'
|
||||
import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
|
||||
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
|
||||
import { getPriceImpactWarning } from 'utils/prices'
|
||||
|
||||
import Column from '../../Column'
|
||||
import Row from '../../Row'
|
||||
import TokenImg from '../../TokenImg'
|
||||
|
||||
const Percent = styled.span<{ gain: boolean }>`
|
||||
color: ${({ gain, theme }) => (gain ? theme.success : theme.error)};
|
||||
`
|
||||
|
||||
interface TokenValueProps {
|
||||
input: CurrencyAmount<Currency>
|
||||
usdc?: boolean
|
||||
change?: number
|
||||
usdc?: CurrencyAmount<Token>
|
||||
priceImpact?: Percent
|
||||
}
|
||||
|
||||
function TokenValue({ input, usdc, change }: TokenValueProps) {
|
||||
function TokenValue({ input, usdc, priceImpact }: TokenValueProps) {
|
||||
const { i18n } = useLingui()
|
||||
const percent = useMemo(() => {
|
||||
if (change) {
|
||||
const percent = change.toPrecision(3)
|
||||
return change > 0 ? `(+${percent}%)` : `(${percent}%)`
|
||||
}
|
||||
return undefined
|
||||
}, [change])
|
||||
|
||||
const usdcAmount = useUSDCValue(input)
|
||||
|
||||
const priceImpactWarning = useMemo(() => getPriceImpactWarning(priceImpact), [priceImpact])
|
||||
return (
|
||||
<Column 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}
|
||||
</ThemedText.Body2>
|
||||
</Row>
|
||||
{usdc && usdcAmount && (
|
||||
{usdc && (
|
||||
<Row justify="flex-start">
|
||||
<ThemedText.Caption color="secondary" userSelect>
|
||||
${formatCurrencyAmount(usdcAmount, 2, i18n.locale)}
|
||||
{change && <Percent gain={change > 0}> {percent}</Percent>}
|
||||
${formatCurrencyAmount(usdc, 2, i18n.locale)}
|
||||
{priceImpact && (
|
||||
<ThemedText.Caption color={priceImpactWarning}>
|
||||
({toHumanReadablePriceImpact(priceImpact)})
|
||||
</ThemedText.Caption>
|
||||
)}
|
||||
</ThemedText.Caption>
|
||||
</Row>
|
||||
)}
|
||||
@ -57,23 +47,17 @@ function TokenValue({ input, usdc, change }: TokenValueProps) {
|
||||
interface SummaryProps {
|
||||
input: CurrencyAmount<Currency>
|
||||
output: CurrencyAmount<Currency>
|
||||
usdc?: boolean
|
||||
showUSDC?: true
|
||||
}
|
||||
|
||||
export default function Summary({ input, output, usdc }: SummaryProps) {
|
||||
const inputUSDCValue = useUSDCValue(input)
|
||||
const outputUSDCValue = useUSDCValue(output)
|
||||
|
||||
const priceImpact = useMemo(() => {
|
||||
const computedChange = computeFiatValuePriceImpact(inputUSDCValue, outputUSDCValue)
|
||||
return computedChange ? parseFloat(computedChange.multiply(-1)?.toSignificant(3)) : undefined
|
||||
}, [inputUSDCValue, outputUSDCValue])
|
||||
export default function Summary({ input, output, showUSDC }: SummaryProps) {
|
||||
const { inputUSDC, outputUSDC, priceImpact } = useUSDCPriceImpact(input, output)
|
||||
|
||||
return (
|
||||
<Row gap={usdc ? 1 : 0.25}>
|
||||
<TokenValue input={input} usdc={usdc} />
|
||||
<Row gap={showUSDC ? 1 : 0.25}>
|
||||
<TokenValue input={input} usdc={showUSDC && inputUSDC} />
|
||||
<ArrowRight />
|
||||
<TokenValue input={output} usdc={usdc} change={priceImpact} />
|
||||
<TokenValue input={output} usdc={showUSDC && outputUSDC} priceImpact={priceImpact} />
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
@ -134,7 +134,7 @@ export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDial
|
||||
<Header title={<Trans>Swap summary</Trans>} ruled />
|
||||
<Body flex align="stretch" gap={0.75} padded open={open}>
|
||||
<SummaryColumn gap={0.75} flex justify="center">
|
||||
<Summary input={inputAmount} output={outputAmount} usdc={true} />
|
||||
<Summary input={inputAmount} output={outputAmount} showUSDC />
|
||||
<Row>
|
||||
<ThemedText.Caption userSelect>
|
||||
{formatLocaleNumber({ number: 1, sigFigs: 1, locale: i18n.locale })} {inputCurrency.symbol} ={' '}
|
||||
|
@ -1,13 +1,16 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
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 { loadingCss } from 'lib/css/loading'
|
||||
import { WrapType } from 'lib/hooks/swap/useWrapCallback'
|
||||
import useUSDCPriceImpact, { toHumanReadablePriceImpact } from 'lib/hooks/useUSDCPriceImpact'
|
||||
import { AlertTriangle, Icon, Info, InlineSpinner } from 'lib/icons'
|
||||
import styled, { ThemedText } from 'lib/theme'
|
||||
import { ReactNode, useCallback, useMemo, useState } from 'react'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
import { getPriceImpactWarning } from 'utils/prices'
|
||||
|
||||
import { TextButton } from '../../Button'
|
||||
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> }) {
|
||||
const [flip, setFlip] = useState(true)
|
||||
const { inputAmount, outputAmount, executionPrice } = trade
|
||||
const fiatValueInput = useUSDCPrice(inputAmount.currency)
|
||||
const fiatValueOutput = useUSDCPrice(outputAmount.currency)
|
||||
const { inputAmount: input, outputAmount: output, executionPrice } = trade
|
||||
const { inputUSDC, outputUSDC, priceImpact } = useUSDCPriceImpact(input, output)
|
||||
const isPriceImpactHigh = priceImpact && getPriceImpactWarning(priceImpact)
|
||||
|
||||
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 ratio = `1 ${a.currency.symbol} = ${priceString} ${b.currency.symbol}`
|
||||
const usdc = !flip
|
||||
? fiatValueInput
|
||||
? ` ($${fiatValueInput.toSignificant(6)})`
|
||||
? inputUSDC
|
||||
? ` ($${inputUSDC.toSignificant(6)})`
|
||||
: null
|
||||
: fiatValueOutput
|
||||
? ` ($${fiatValueOutput.toSignificant(6)})`
|
||||
: outputUSDC
|
||||
? ` ($${outputUSDC.toSignificant(6)})`
|
||||
: null
|
||||
|
||||
return (
|
||||
@ -103,12 +106,23 @@ export function Trade({ trade }: { trade: InterfaceTrade<Currency, Currency, Tra
|
||||
</Row>
|
||||
</ThemedText.Caption>
|
||||
)
|
||||
}, [executionPrice, fiatValueInput, fiatValueOutput, flip, inputAmount, outputAmount])
|
||||
}, [executionPrice, inputUSDC, outputUSDC, flip, input, output])
|
||||
|
||||
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} />
|
||||
</Column>
|
||||
</Tooltip>
|
||||
<TextButton color="primary" onClick={() => setFlip(!flip)}>
|
||||
{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