feat: confirm price impact (#3288)

* refactor: action button naming

* feat: high price impact acknowledgement
This commit is contained in:
Zach Pomerantz 2022-02-10 19:33:51 -08:00 committed by GitHub
parent b4aac94c2c
commit 8404c6076c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 52 additions and 47 deletions

@ -15,7 +15,7 @@ const StyledButton = styled(Button)`
} }
` `
const UpdateRow = styled(Row)`` const ActionRow = styled(Row)``
const grow = keyframes` const grow = keyframes`
from { from {
@ -28,12 +28,12 @@ const grow = keyframes`
} }
` `
const updateCss = css` const actionCss = css`
border: 1px solid ${({ theme }) => theme.outline}; border: 1px solid ${({ theme }) => theme.outline};
padding: calc(0.25em - 1px); padding: calc(0.25em - 1px);
padding-left: calc(0.75em - 1px); padding-left: calc(0.75em - 1px);
${UpdateRow} { ${ActionRow} {
animation: ${grow} 0.25s ease-in; animation: ${grow} 0.25s ease-in;
white-space: nowrap; white-space: nowrap;
} }
@ -45,45 +45,37 @@ const updateCss = css`
} }
` `
export const Overlay = styled(Row)<{ update?: boolean }>` export const Overlay = styled(Row)<{ action?: boolean }>`
border-radius: ${({ theme }) => theme.borderRadius}em; border-radius: ${({ theme }) => theme.borderRadius}em;
flex-direction: row-reverse; flex-direction: row-reverse;
min-height: 3.5em; min-height: 3.5em;
transition: padding 0.25s ease-out; transition: padding 0.25s ease-out;
${({ update }) => update && updateCss} ${({ action }) => action && actionCss}
` `
export interface ActionButtonProps { export interface ActionButtonProps {
color?: Color color?: Color
disabled?: boolean disabled?: boolean
update?: { message: ReactNode; action: ReactNode; icon?: Icon } action?: { message: ReactNode; icon?: Icon; onClick: () => void; children: ReactNode }
onClick: () => void onClick: () => void
onUpdate?: () => void
children: ReactNode children: ReactNode
} }
export default function ActionButton({ export default function ActionButton({ color = 'accent', disabled, action, onClick, children }: ActionButtonProps) {
color = 'accent',
disabled,
update,
onClick,
onUpdate,
children,
}: ActionButtonProps) {
const textColor = useMemo(() => (color === 'accent' && !disabled ? 'onAccent' : 'currentColor'), [color, disabled]) const textColor = useMemo(() => (color === 'accent' && !disabled ? 'onAccent' : 'currentColor'), [color, disabled])
return ( return (
<Overlay update={Boolean(update)} flex align="stretch"> <Overlay action={Boolean(action)} flex align="stretch">
<StyledButton color={color} disabled={disabled} onClick={update ? onUpdate : onClick}> <StyledButton color={color} disabled={disabled} onClick={action ? action.onClick : onClick}>
<ThemedText.TransitionButton buttonSize={update ? 'medium' : 'large'} color={textColor}> <ThemedText.TransitionButton buttonSize={action ? 'medium' : 'large'} color={textColor}>
{update ? update.action : children} {action ? action.children : children}
</ThemedText.TransitionButton> </ThemedText.TransitionButton>
</StyledButton> </StyledButton>
{update && ( {action && (
<UpdateRow gap={0.5}> <ActionRow gap={0.5}>
<LargeIcon color="currentColor" icon={update.icon || AlertTriangle} /> <LargeIcon color="currentColor" icon={action.icon || AlertTriangle} />
<ThemedText.Subhead2>{update?.message}</ThemedText.Subhead2> <ThemedText.Subhead2>{action?.message}</ThemedText.Subhead2>
</UpdateRow> </ActionRow>
)} )}
</Overlay> </Overlay>
) )

@ -36,7 +36,7 @@ export default class ErrorBoundary extends React.Component<ErrorBoundaryProps, E
error={this.state.error} error={this.state.error}
header={<Trans>Something went wrong.</Trans>} header={<Trans>Something went wrong.</Trans>}
action={<Trans>Reload the page</Trans>} action={<Trans>Reload the page</Trans>}
onAction={() => window.location.reload()} onClick={() => window.location.reload()}
/> />
</Dialog> </Dialog>
) )

@ -87,10 +87,10 @@ interface ErrorDialogProps {
header?: ReactNode header?: ReactNode
error: Error error: Error
action: ReactNode action: ReactNode
onAction: () => void onClick: () => void
} }
export default function ErrorDialog({ header, error, action, onAction }: ErrorDialogProps) { export default function ErrorDialog({ header, error, action, onClick }: ErrorDialogProps) {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [details, setDetails] = useState<HTMLDivElement | null>(null) const [details, setDetails] = useState<HTMLDivElement | null>(null)
const scrollbar = useScrollbar(details) const scrollbar = useScrollbar(details)
@ -123,7 +123,7 @@ export default function ErrorDialog({ header, error, action, onAction }: ErrorDi
</ThemedText.Code> </ThemedText.Code>
</Column> </Column>
</ErrorColumn> </ErrorColumn>
<ActionButton onClick={onAction}>{action}</ActionButton> <ActionButton onClick={onClick}>{action}</ActionButton>
</ExpandoColumn> </ExpandoColumn>
</Column> </Column>
) )

@ -93,7 +93,7 @@ export default function TransactionStatusDialog({ tx, onClose }: TransactionStat
header={errorMessage} header={errorMessage}
error={new Error('TODO(zzmp)')} error={new Error('TODO(zzmp)')}
action={<Trans>Dismiss</Trans>} action={<Trans>Dismiss</Trans>}
onAction={onClose} onClick={onClose}
/> />
) : ( ) : (
<TransactionStatus tx={tx} onClose={onClose} /> <TransactionStatus tx={tx} onClose={onClose} />

@ -6,7 +6,7 @@ import { ALLOWED_PRICE_IMPACT_HIGH, ALLOWED_PRICE_IMPACT_MEDIUM } from 'constant
import { useAtomValue } from 'jotai/utils' import { useAtomValue } from 'jotai/utils'
import { IconButton } from 'lib/components/Button' import { IconButton } from 'lib/components/Button'
import useScrollbar from 'lib/hooks/useScrollbar' import useScrollbar from 'lib/hooks/useScrollbar'
import { AlertTriangle, Expando, Info } from 'lib/icons' import { AlertTriangle, BarChart, Expando, Info } from 'lib/icons'
import { MIN_HIGH_SLIPPAGE } from 'lib/state/settings' import { MIN_HIGH_SLIPPAGE } from 'lib/state/settings'
import { Field, independentFieldAtom } from 'lib/state/swap' import { Field, independentFieldAtom } from 'lib/state/swap'
import styled, { ThemedText } from 'lib/theme' import styled, { ThemedText } from 'lib/theme'
@ -79,8 +79,6 @@ const Body = styled(Column)<{ open: boolean }>`
} }
` `
const priceUpdate = { message: <Trans>Price updated</Trans>, action: <Trans>Accept</Trans> }
interface SummaryDialogProps { interface SummaryDialogProps {
trade: Trade<Currency, Currency, TradeType> trade: Trade<Currency, Currency, TradeType>
allowedSlippage: Percent allowedSlippage: Percent
@ -92,8 +90,12 @@ export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDial
const inputCurrency = inputAmount.currency const inputCurrency = inputAmount.currency
const outputCurrency = outputAmount.currency const outputCurrency = outputAmount.currency
const priceImpact = useMemo(() => computeRealizedPriceImpact(trade), [trade]) const priceImpact = useMemo(() => computeRealizedPriceImpact(trade), [trade])
const independentField = useAtomValue(independentFieldAtom) const independentField = useAtomValue(independentFieldAtom)
const { i18n } = useLingui()
const [open, setOpen] = useState(false)
const [details, setDetails] = useState<HTMLDivElement | null>(null)
const scrollbar = useScrollbar(details)
const warning = useMemo(() => { const warning = useMemo(() => {
if (priceImpact.greaterThan(ALLOWED_PRICE_IMPACT_HIGH)) return 'error' if (priceImpact.greaterThan(ALLOWED_PRICE_IMPACT_HIGH)) return 'error'
@ -102,18 +104,31 @@ export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDial
return return
}, [allowedSlippage, priceImpact]) }, [allowedSlippage, priceImpact])
const [ackPriceImpact, setAckPriceImpact] = useState(false)
const [confirmedTrade, setConfirmedTrade] = useState(trade) const [confirmedTrade, setConfirmedTrade] = useState(trade)
const doesTradeDiffer = useMemo( const doesTradeDiffer = useMemo(
() => Boolean(trade && confirmedTrade && tradeMeaningfullyDiffers(trade, confirmedTrade)), () => Boolean(trade && confirmedTrade && tradeMeaningfullyDiffers(trade, confirmedTrade)),
[confirmedTrade, trade] [confirmedTrade, trade]
) )
const [open, setOpen] = useState(false)
const [details, setDetails] = useState<HTMLDivElement | null>(null) const action = useMemo(() => {
if (doesTradeDiffer) {
const scrollbar = useScrollbar(details) return {
message: <Trans>Price updated</Trans>,
const { i18n } = useLingui() icon: BarChart,
onClick: () => setConfirmedTrade(trade),
children: <Trans>Accept</Trans>,
}
} else if (priceImpact.greaterThan(ALLOWED_PRICE_IMPACT_HIGH) && !ackPriceImpact) {
return {
message: <Trans>High price impact</Trans>,
onClick: () => setAckPriceImpact(true),
children: <Trans>Acknowledge</Trans>,
}
}
return
}, [ackPriceImpact, doesTradeDiffer, priceImpact, trade])
if (!(inputAmount && outputAmount && inputCurrency && outputCurrency)) { if (!(inputAmount && outputAmount && inputCurrency && outputCurrency)) {
return null return null
@ -163,11 +178,7 @@ export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDial
</Trans> </Trans>
)} )}
</Estimate> </Estimate>
<ActionButton <ActionButton onClick={onConfirm} action={action}>
onClick={onConfirm}
onUpdate={() => setConfirmedTrade(trade)}
update={doesTradeDiffer ? priceUpdate : undefined}
>
<Trans>Confirm swap</Trans> <Trans>Confirm swap</Trans>
</ActionButton> </ActionButton>
</ExpandoColumn> </ExpandoColumn>

@ -95,8 +95,9 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
</Row> </Row>
</EtherscanLink> </EtherscanLink>
), ),
action: <Trans>Approve</Trans>,
icon: Spinner, icon: Spinner,
onClick: addApprovalTransaction,
children: <Trans>Approve</Trans>,
}, },
} }
} else if (approval === ApprovalState.NOT_APPROVED) { } else if (approval === ApprovalState.NOT_APPROVED) {
@ -111,7 +112,7 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
} }
return { disabled: true } return { disabled: true }
}, [approval, approvalHash, chainId, disabled, inputCurrencyAmount, inputCurrencyBalance]) }, [addApprovalTransaction, approval, approvalHash, chainId, disabled, inputCurrencyAmount, inputCurrencyBalance])
const deadline = useTransactionDeadline() const deadline = useTransactionDeadline()
const { signatureData } = useERC20PermitFromTrade(optimizedTrade, allowedSlippage, deadline) const { signatureData } = useERC20PermitFromTrade(optimizedTrade, allowedSlippage, deadline)
@ -156,7 +157,6 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
<ActionButton <ActionButton
color={tokenColorExtraction ? 'interactive' : 'accent'} color={tokenColorExtraction ? 'interactive' : 'accent'}
onClick={() => setActiveTrade(trade.trade)} onClick={() => setActiveTrade(trade.trade)}
onUpdate={addApprovalTransaction}
{...actionProps} {...actionProps}
> >
<Trans>Review swap</Trans> <Trans>Review swap</Trans>

@ -12,6 +12,7 @@ import {
ArrowDown as ArrowDownIcon, ArrowDown as ArrowDownIcon,
ArrowRight as ArrowRightIcon, ArrowRight as ArrowRightIcon,
ArrowUp as ArrowUpIcon, ArrowUp as ArrowUpIcon,
BarChart2 as BarChart2Icon,
CheckCircle as CheckCircleIcon, CheckCircle as CheckCircleIcon,
ChevronDown as ChevronDownIcon, ChevronDown as ChevronDownIcon,
Clock as ClockIcon, Clock as ClockIcon,
@ -75,6 +76,7 @@ export const ArrowDown = icon(ArrowDownIcon)
export const ArrowRight = icon(ArrowRightIcon) export const ArrowRight = icon(ArrowRightIcon)
export const ArrowUp = icon(ArrowUpIcon) export const ArrowUp = icon(ArrowUpIcon)
export const CheckCircle = icon(CheckCircleIcon) export const CheckCircle = icon(CheckCircleIcon)
export const BarChart = icon(BarChart2Icon)
export const ChevronDown = icon(ChevronDownIcon) export const ChevronDown = icon(ChevronDownIcon)
export const Clock = icon(ClockIcon) export const Clock = icon(ClockIcon)
export const HelpCircle = icon(HelpCircleIcon) export const HelpCircle = icon(HelpCircleIcon)