feat: pending approval ui (#3186)
* feat: track approval txs * refactor: update transactions * feat: pending approval ui * chore: fix pending approval doc * fix: clarify optimized trade * fix: use relative path for data uri assets
This commit is contained in:
parent
c7633d910b
commit
8d145b908e
@ -1,9 +1,9 @@
|
|||||||
import ethereumLogoUrl from 'assets/images/ethereum-logo.png'
|
|
||||||
import arbitrumLogoUrl from 'assets/svg/arbitrum_logo.svg'
|
|
||||||
import optimismLogoUrl from 'assets/svg/optimistic_ethereum.svg'
|
|
||||||
import polygonMaticLogo from 'assets/svg/polygon-matic-logo.svg'
|
|
||||||
import ms from 'ms.macro'
|
import ms from 'ms.macro'
|
||||||
|
|
||||||
|
import ethereumLogoUrl from '../assets/images/ethereum-logo.png'
|
||||||
|
import arbitrumLogoUrl from '../assets/svg/arbitrum_logo.svg'
|
||||||
|
import optimismLogoUrl from '../assets/svg/optimistic_ethereum.svg'
|
||||||
|
import polygonMaticLogo from '../assets/svg/polygon-matic-logo.svg'
|
||||||
import { SupportedChainId, SupportedL1ChainId, SupportedL2ChainId } from './chains'
|
import { SupportedChainId, SupportedL1ChainId, SupportedL2ChainId } from './chains'
|
||||||
import { ARBITRUM_LIST, OPTIMISM_LIST } from './lists'
|
import { ARBITRUM_LIST, OPTIMISM_LIST } from './lists'
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { AlertTriangle, LargeIcon } from 'lib/icons'
|
import { AlertTriangle, Icon, LargeIcon } from 'lib/icons'
|
||||||
import styled, { Color, css, keyframes, ThemedText } from 'lib/theme'
|
import styled, { Color, css, keyframes, ThemedText } from 'lib/theme'
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
@ -9,6 +9,10 @@ const StyledButton = styled(Button)`
|
|||||||
border-radius: ${({ theme }) => theme.borderRadius}em;
|
border-radius: ${({ theme }) => theme.borderRadius}em;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
transition: background-color 0.25s ease-out, flex-grow 0.25s ease-out, padding 0.25s ease-out;
|
transition: background-color 0.25s ease-out, flex-grow 0.25s ease-out, padding 0.25s ease-out;
|
||||||
|
|
||||||
|
:disabled {
|
||||||
|
margin: -1px;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const UpdateRow = styled(Row)``
|
const UpdateRow = styled(Row)``
|
||||||
@ -24,7 +28,7 @@ const grow = keyframes`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const updatedCss = css`
|
const updateCss = 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);
|
||||||
@ -41,19 +45,19 @@ const updatedCss = css`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export const Overlay = styled(Row)<{ updated?: boolean }>`
|
export const Overlay = styled(Row)<{ update?: 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;
|
||||||
|
|
||||||
${({ updated }) => updated && updatedCss}
|
${({ update }) => update && updateCss}
|
||||||
`
|
`
|
||||||
|
|
||||||
export interface ActionButtonProps {
|
export interface ActionButtonProps {
|
||||||
color?: Color
|
color?: Color
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
updated?: { message: ReactNode; action: ReactNode }
|
update?: { message: ReactNode; action: ReactNode; icon?: Icon }
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
onUpdate?: () => void
|
onUpdate?: () => void
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
@ -62,22 +66,22 @@ export interface ActionButtonProps {
|
|||||||
export default function ActionButton({
|
export default function ActionButton({
|
||||||
color = 'accent',
|
color = 'accent',
|
||||||
disabled,
|
disabled,
|
||||||
updated,
|
update,
|
||||||
onClick,
|
onClick,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
children,
|
children,
|
||||||
}: ActionButtonProps) {
|
}: ActionButtonProps) {
|
||||||
return (
|
return (
|
||||||
<Overlay updated={Boolean(updated)} flex align="stretch">
|
<Overlay update={Boolean(update)} flex align="stretch">
|
||||||
<StyledButton color={color} disabled={disabled} onClick={updated ? onUpdate : onClick}>
|
<StyledButton color={color} disabled={disabled} onClick={update ? onUpdate : onClick}>
|
||||||
<ThemedText.TransitionButton buttonSize={updated ? 'medium' : 'large'} color="currentColor">
|
<ThemedText.TransitionButton buttonSize={update ? 'medium' : 'large'} color="currentColor">
|
||||||
{updated ? updated.action : children}
|
{update ? update.action : children}
|
||||||
</ThemedText.TransitionButton>
|
</ThemedText.TransitionButton>
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
{updated && (
|
{update && (
|
||||||
<UpdateRow gap={0.5}>
|
<UpdateRow gap={0.5}>
|
||||||
<LargeIcon icon={AlertTriangle} />
|
<LargeIcon color="currentColor" icon={update.icon || AlertTriangle} />
|
||||||
<ThemedText.Subhead2>{updated?.message}</ThemedText.Subhead2>
|
<ThemedText.Subhead2>{update?.message}</ThemedText.Subhead2>
|
||||||
</UpdateRow>
|
</UpdateRow>
|
||||||
)}
|
)}
|
||||||
</Overlay>
|
</Overlay>
|
||||||
|
@ -147,7 +147,7 @@ export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDial
|
|||||||
<ActionButton
|
<ActionButton
|
||||||
onClick={onConfirm}
|
onClick={onConfirm}
|
||||||
onUpdate={() => setConfirmedTrade(trade)}
|
onUpdate={() => setConfirmedTrade(trade)}
|
||||||
updated={doesTradeDiffer ? priceUpdate : undefined}
|
update={doesTradeDiffer ? priceUpdate : undefined}
|
||||||
>
|
>
|
||||||
<Trans>Confirm swap</Trans>
|
<Trans>Confirm swap</Trans>
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
@ -1,29 +1,50 @@
|
|||||||
import { Trans } from '@lingui/macro'
|
import { Trans } from '@lingui/macro'
|
||||||
|
import { Token } from '@uniswap/sdk-core'
|
||||||
|
import { CHAIN_INFO } from 'constants/chainInfo'
|
||||||
import { useSwapInfo } from 'lib/hooks/swap'
|
import { useSwapInfo } from 'lib/hooks/swap'
|
||||||
import useSwapApproval, { ApprovalState, useSwapApprovalOptimizedTrade } from 'lib/hooks/swap/useSwapApproval'
|
import useSwapApproval, {
|
||||||
|
ApprovalState,
|
||||||
|
useSwapApprovalOptimizedTrade,
|
||||||
|
useSwapRouterAddress,
|
||||||
|
} from 'lib/hooks/swap/useSwapApproval'
|
||||||
import { useAddTransaction } from 'lib/hooks/transactions'
|
import { useAddTransaction } from 'lib/hooks/transactions'
|
||||||
import { useIsPendingApproval } from 'lib/hooks/transactions'
|
import { usePendingApproval } from 'lib/hooks/transactions'
|
||||||
|
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
|
||||||
|
import { Link, Spinner } from 'lib/icons'
|
||||||
import { Field } from 'lib/state/swap'
|
import { Field } from 'lib/state/swap'
|
||||||
import { TransactionType } from 'lib/state/transactions'
|
import { TransactionType } from 'lib/state/transactions'
|
||||||
|
import styled from 'lib/theme'
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
|
|
||||||
import ActionButton from '../ActionButton'
|
import ActionButton from '../ActionButton'
|
||||||
import Dialog from '../Dialog'
|
import Dialog from '../Dialog'
|
||||||
|
import Row from '../Row'
|
||||||
import { SummaryDialog } from './Summary'
|
import { SummaryDialog } from './Summary'
|
||||||
|
|
||||||
interface SwapButtonProps {
|
interface SwapButtonProps {
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const EtherscanA = styled.a`
|
||||||
|
color: currentColor;
|
||||||
|
text-decoration: none;
|
||||||
|
`
|
||||||
|
|
||||||
|
function useIsPendingApproval(token?: Token, spender?: string): boolean {
|
||||||
|
return Boolean(usePendingApproval(token, spender))
|
||||||
|
}
|
||||||
|
|
||||||
export default function SwapButton({ disabled }: SwapButtonProps) {
|
export default function SwapButton({ disabled }: SwapButtonProps) {
|
||||||
|
const { chainId } = useActiveWeb3React()
|
||||||
const {
|
const {
|
||||||
trade,
|
trade,
|
||||||
allowedSlippage,
|
allowedSlippage,
|
||||||
|
currencies: { [Field.INPUT]: inputCurrency },
|
||||||
currencyBalances: { [Field.INPUT]: inputCurrencyBalance },
|
currencyBalances: { [Field.INPUT]: inputCurrencyBalance },
|
||||||
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount },
|
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount },
|
||||||
} = useSwapInfo()
|
} = useSwapInfo()
|
||||||
|
|
||||||
const [activeTrade, setActiveTrade] = useState<typeof trade.trade | undefined>(undefined)
|
const [activeTrade, setActiveTrade] = useState<typeof trade.trade | undefined>()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setActiveTrade((activeTrade) => activeTrade && trade.trade)
|
setActiveTrade((activeTrade) => activeTrade && trade.trade)
|
||||||
}, [trade])
|
}, [trade])
|
||||||
@ -33,6 +54,10 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
|
|||||||
// Use trade.trade if there is no swap optimized trade. This occurs if approvals are still pending.
|
// Use trade.trade if there is no swap optimized trade. This occurs if approvals are still pending.
|
||||||
useSwapApprovalOptimizedTrade(trade.trade, allowedSlippage, useIsPendingApproval) || trade.trade
|
useSwapApprovalOptimizedTrade(trade.trade, allowedSlippage, useIsPendingApproval) || trade.trade
|
||||||
const [approval, getApproval] = useSwapApproval(optimizedTrade, allowedSlippage, useIsPendingApproval)
|
const [approval, getApproval] = useSwapApproval(optimizedTrade, allowedSlippage, useIsPendingApproval)
|
||||||
|
const approvalHash = usePendingApproval(
|
||||||
|
inputCurrency?.isToken ? inputCurrency : undefined,
|
||||||
|
useSwapRouterAddress(optimizedTrade)
|
||||||
|
)
|
||||||
|
|
||||||
const addTransaction = useAddTransaction()
|
const addTransaction = useAddTransaction()
|
||||||
const addApprovalTransaction = useCallback(() => {
|
const addApprovalTransaction = useCallback(() => {
|
||||||
@ -46,13 +71,27 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
|
|||||||
const actionProps = useMemo(() => {
|
const actionProps = useMemo(() => {
|
||||||
if (disabled) return { disabled: true }
|
if (disabled) return { disabled: true }
|
||||||
|
|
||||||
if (inputCurrencyAmount && inputCurrencyBalance?.greaterThan(inputCurrencyAmount)) {
|
if (chainId && inputCurrencyAmount && inputCurrencyBalance?.greaterThan(inputCurrencyAmount)) {
|
||||||
// TODO(zzmp): Update UI for pending approvals.
|
|
||||||
if (approval === ApprovalState.PENDING) {
|
if (approval === ApprovalState.PENDING) {
|
||||||
return { disabled: true }
|
return {
|
||||||
|
disabled: true,
|
||||||
|
update: {
|
||||||
|
message: (
|
||||||
|
<EtherscanA href={approvalHash && `${CHAIN_INFO[chainId].explorer}tx/${approvalHash}`} target="_blank">
|
||||||
|
<Row gap={0.25}>
|
||||||
|
<Trans>
|
||||||
|
Approval pending <Link />
|
||||||
|
</Trans>
|
||||||
|
</Row>
|
||||||
|
</EtherscanA>
|
||||||
|
),
|
||||||
|
action: <Trans>Approve</Trans>,
|
||||||
|
icon: Spinner,
|
||||||
|
},
|
||||||
|
}
|
||||||
} else if (approval === ApprovalState.NOT_APPROVED) {
|
} else if (approval === ApprovalState.NOT_APPROVED) {
|
||||||
return {
|
return {
|
||||||
updated: {
|
update: {
|
||||||
message: <Trans>Approve {inputCurrencyAmount.currency.symbol} first</Trans>,
|
message: <Trans>Approve {inputCurrencyAmount.currency.symbol} first</Trans>,
|
||||||
action: <Trans>Approve</Trans>,
|
action: <Trans>Approve</Trans>,
|
||||||
},
|
},
|
||||||
@ -62,7 +101,7 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return { disabled: true }
|
return { disabled: true }
|
||||||
}, [approval, disabled, inputCurrencyAmount, inputCurrencyBalance])
|
}, [approval, approvalHash, chainId, disabled, inputCurrencyAmount, inputCurrencyBalance])
|
||||||
|
|
||||||
const onConfirm = useCallback(() => {
|
const onConfirm = useCallback(() => {
|
||||||
// TODO(zzmp): Transact the trade.
|
// TODO(zzmp): Transact the trade.
|
||||||
|
@ -34,22 +34,15 @@ function useSwapApprovalStates(
|
|||||||
return useMemo(() => ({ v2, v3, v2V3 }), [v2, v2V3, v3])
|
return useMemo(() => ({ v2, v3, v2V3 }), [v2, v2V3, v3])
|
||||||
}
|
}
|
||||||
|
|
||||||
// wraps useApproveCallback in the context of a swap
|
export function useSwapRouterAddress(
|
||||||
export default function useSwapApproval(
|
|
||||||
trade:
|
trade:
|
||||||
| V2Trade<Currency, Currency, TradeType>
|
| V2Trade<Currency, Currency, TradeType>
|
||||||
| V3Trade<Currency, Currency, TradeType>
|
| V3Trade<Currency, Currency, TradeType>
|
||||||
| Trade<Currency, Currency, TradeType>
|
| Trade<Currency, Currency, TradeType>
|
||||||
| undefined,
|
| undefined
|
||||||
allowedSlippage: Percent,
|
|
||||||
useIsPendingApproval: (token?: Token, spender?: string) => boolean
|
|
||||||
) {
|
) {
|
||||||
const { chainId } = useActiveWeb3React()
|
const { chainId } = useActiveWeb3React()
|
||||||
const amountToApprove = useMemo(
|
return useMemo(
|
||||||
() => (trade && trade.inputAmount.currency.isToken ? trade.maximumAmountIn(allowedSlippage) : undefined),
|
|
||||||
[trade, allowedSlippage]
|
|
||||||
)
|
|
||||||
const spender = useMemo(
|
|
||||||
() =>
|
() =>
|
||||||
chainId
|
chainId
|
||||||
? trade instanceof V2Trade
|
? trade instanceof V2Trade
|
||||||
@ -60,6 +53,23 @@ export default function useSwapApproval(
|
|||||||
: undefined,
|
: undefined,
|
||||||
[chainId, trade]
|
[chainId, trade]
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// wraps useApproveCallback in the context of a swap
|
||||||
|
export default function useSwapApproval(
|
||||||
|
trade:
|
||||||
|
| V2Trade<Currency, Currency, TradeType>
|
||||||
|
| V3Trade<Currency, Currency, TradeType>
|
||||||
|
| Trade<Currency, Currency, TradeType>
|
||||||
|
| undefined,
|
||||||
|
allowedSlippage: Percent,
|
||||||
|
useIsPendingApproval: (token?: Token, spender?: string) => boolean
|
||||||
|
) {
|
||||||
|
const amountToApprove = useMemo(
|
||||||
|
() => (trade && trade.inputAmount.currency.isToken ? trade.maximumAmountIn(allowedSlippage) : undefined),
|
||||||
|
[trade, allowedSlippage]
|
||||||
|
)
|
||||||
|
const spender = useSwapRouterAddress(trade)
|
||||||
|
|
||||||
const approval = useApproval(amountToApprove, spender, useIsPendingApproval)
|
const approval = useApproval(amountToApprove, spender, useIsPendingApproval)
|
||||||
if (trade instanceof V2Trade || trade instanceof V3Trade) {
|
if (trade instanceof V2Trade || trade instanceof V3Trade) {
|
||||||
|
@ -40,15 +40,16 @@ export function useAddTransaction() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useIsPendingApproval(token?: Token, spender?: string) {
|
/** Returns the hash of a pending approval transaction, if it exists. */
|
||||||
|
export function usePendingApproval(token?: Token, spender?: string): string | undefined {
|
||||||
const { chainId } = useActiveWeb3React()
|
const { chainId } = useActiveWeb3React()
|
||||||
const txs = useAtomValue(transactionsAtom)
|
const txs = useAtomValue(transactionsAtom)
|
||||||
if (!chainId || !token || !spender) return false
|
if (!chainId || !token || !spender) return undefined
|
||||||
|
|
||||||
const chainTxs = txs[chainId]
|
const chainTxs = txs[chainId]
|
||||||
if (!chainTxs) return false
|
if (!chainTxs) return undefined
|
||||||
|
|
||||||
return Object.values(chainTxs).some(
|
return Object.values(chainTxs).find(
|
||||||
(tx) =>
|
(tx) =>
|
||||||
tx &&
|
tx &&
|
||||||
tx.receipt === undefined &&
|
tx.receipt === undefined &&
|
||||||
@ -56,7 +57,7 @@ export function useIsPendingApproval(token?: Token, spender?: string) {
|
|||||||
tx.info.tokenAddress === token.address &&
|
tx.info.tokenAddress === token.address &&
|
||||||
tx.info.spenderAddress === spender &&
|
tx.info.spenderAddress === spender &&
|
||||||
isTransactionRecent(tx)
|
isTransactionRecent(tx)
|
||||||
)
|
)?.info.response.hash
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TransactionsUpdater() {
|
export function TransactionsUpdater() {
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
ChevronDown as ChevronDownIcon,
|
ChevronDown as ChevronDownIcon,
|
||||||
Clock as ClockIcon,
|
Clock as ClockIcon,
|
||||||
CreditCard as CreditCardIcon,
|
CreditCard as CreditCardIcon,
|
||||||
|
ExternalLink as LinkIcon,
|
||||||
HelpCircle as HelpCircleIcon,
|
HelpCircle as HelpCircleIcon,
|
||||||
Info as InfoIcon,
|
Info as InfoIcon,
|
||||||
Settings as SettingsIcon,
|
Settings as SettingsIcon,
|
||||||
@ -77,6 +78,7 @@ export const Clock = icon(ClockIcon)
|
|||||||
export const CreditCard = icon(CreditCardIcon)
|
export const CreditCard = icon(CreditCardIcon)
|
||||||
export const HelpCircle = icon(HelpCircleIcon)
|
export const HelpCircle = icon(HelpCircleIcon)
|
||||||
export const Info = icon(InfoIcon)
|
export const Info = icon(InfoIcon)
|
||||||
|
export const Link = icon(LinkIcon)
|
||||||
export const Settings = icon(SettingsIcon)
|
export const Settings = icon(SettingsIcon)
|
||||||
export const Slash = icon(SlashIcon)
|
export const Slash = icon(SlashIcon)
|
||||||
export const Trash2 = icon(Trash2Icon)
|
export const Trash2 = icon(Trash2Icon)
|
||||||
|
Loading…
Reference in New Issue
Block a user