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:
Zach Pomerantz 2022-01-25 16:24:36 -08:00 committed by GitHub
parent c7633d910b
commit 8d145b908e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 97 additions and 41 deletions

@ -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 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 { 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 { ReactNode } from 'react'
@ -9,6 +9,10 @@ const StyledButton = styled(Button)`
border-radius: ${({ theme }) => theme.borderRadius}em;
flex-grow: 1;
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)``
@ -24,7 +28,7 @@ const grow = keyframes`
}
`
const updatedCss = css`
const updateCss = css`
border: 1px solid ${({ theme }) => theme.outline};
padding: calc(0.25em - 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;
flex-direction: row-reverse;
min-height: 3.5em;
transition: padding 0.25s ease-out;
${({ updated }) => updated && updatedCss}
${({ update }) => update && updateCss}
`
export interface ActionButtonProps {
color?: Color
disabled?: boolean
updated?: { message: ReactNode; action: ReactNode }
update?: { message: ReactNode; action: ReactNode; icon?: Icon }
onClick: () => void
onUpdate?: () => void
children: ReactNode
@ -62,22 +66,22 @@ export interface ActionButtonProps {
export default function ActionButton({
color = 'accent',
disabled,
updated,
update,
onClick,
onUpdate,
children,
}: ActionButtonProps) {
return (
<Overlay updated={Boolean(updated)} flex align="stretch">
<StyledButton color={color} disabled={disabled} onClick={updated ? onUpdate : onClick}>
<ThemedText.TransitionButton buttonSize={updated ? 'medium' : 'large'} color="currentColor">
{updated ? updated.action : children}
<Overlay update={Boolean(update)} flex align="stretch">
<StyledButton color={color} disabled={disabled} onClick={update ? onUpdate : onClick}>
<ThemedText.TransitionButton buttonSize={update ? 'medium' : 'large'} color="currentColor">
{update ? update.action : children}
</ThemedText.TransitionButton>
</StyledButton>
{updated && (
{update && (
<UpdateRow gap={0.5}>
<LargeIcon icon={AlertTriangle} />
<ThemedText.Subhead2>{updated?.message}</ThemedText.Subhead2>
<LargeIcon color="currentColor" icon={update.icon || AlertTriangle} />
<ThemedText.Subhead2>{update?.message}</ThemedText.Subhead2>
</UpdateRow>
)}
</Overlay>

@ -147,7 +147,7 @@ export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDial
<ActionButton
onClick={onConfirm}
onUpdate={() => setConfirmedTrade(trade)}
updated={doesTradeDiffer ? priceUpdate : undefined}
update={doesTradeDiffer ? priceUpdate : undefined}
>
<Trans>Confirm swap</Trans>
</ActionButton>

@ -1,29 +1,50 @@
import { Trans } from '@lingui/macro'
import { Token } from '@uniswap/sdk-core'
import { CHAIN_INFO } from 'constants/chainInfo'
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 { 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 { TransactionType } from 'lib/state/transactions'
import styled from 'lib/theme'
import { useCallback, useEffect, useMemo, useState } from 'react'
import ActionButton from '../ActionButton'
import Dialog from '../Dialog'
import Row from '../Row'
import { SummaryDialog } from './Summary'
interface SwapButtonProps {
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) {
const { chainId } = useActiveWeb3React()
const {
trade,
allowedSlippage,
currencies: { [Field.INPUT]: inputCurrency },
currencyBalances: { [Field.INPUT]: inputCurrencyBalance },
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount },
} = useSwapInfo()
const [activeTrade, setActiveTrade] = useState<typeof trade.trade | undefined>(undefined)
const [activeTrade, setActiveTrade] = useState<typeof trade.trade | undefined>()
useEffect(() => {
setActiveTrade((activeTrade) => activeTrade && 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.
useSwapApprovalOptimizedTrade(trade.trade, allowedSlippage, useIsPendingApproval) || trade.trade
const [approval, getApproval] = useSwapApproval(optimizedTrade, allowedSlippage, useIsPendingApproval)
const approvalHash = usePendingApproval(
inputCurrency?.isToken ? inputCurrency : undefined,
useSwapRouterAddress(optimizedTrade)
)
const addTransaction = useAddTransaction()
const addApprovalTransaction = useCallback(() => {
@ -46,13 +71,27 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
const actionProps = useMemo(() => {
if (disabled) return { disabled: true }
if (inputCurrencyAmount && inputCurrencyBalance?.greaterThan(inputCurrencyAmount)) {
// TODO(zzmp): Update UI for pending approvals.
if (chainId && inputCurrencyAmount && inputCurrencyBalance?.greaterThan(inputCurrencyAmount)) {
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) {
return {
updated: {
update: {
message: <Trans>Approve {inputCurrencyAmount.currency.symbol} first</Trans>,
action: <Trans>Approve</Trans>,
},
@ -62,7 +101,7 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
}
return { disabled: true }
}, [approval, disabled, inputCurrencyAmount, inputCurrencyBalance])
}, [approval, approvalHash, chainId, disabled, inputCurrencyAmount, inputCurrencyBalance])
const onConfirm = useCallback(() => {
// TODO(zzmp): Transact the trade.

@ -34,22 +34,15 @@ function useSwapApprovalStates(
return useMemo(() => ({ v2, v3, v2V3 }), [v2, v2V3, v3])
}
// wraps useApproveCallback in the context of a swap
export default function useSwapApproval(
export function useSwapRouterAddress(
trade:
| V2Trade<Currency, Currency, TradeType>
| V3Trade<Currency, Currency, TradeType>
| Trade<Currency, Currency, TradeType>
| undefined,
allowedSlippage: Percent,
useIsPendingApproval: (token?: Token, spender?: string) => boolean
| undefined
) {
const { chainId } = useActiveWeb3React()
const amountToApprove = useMemo(
() => (trade && trade.inputAmount.currency.isToken ? trade.maximumAmountIn(allowedSlippage) : undefined),
[trade, allowedSlippage]
)
const spender = useMemo(
return useMemo(
() =>
chainId
? trade instanceof V2Trade
@ -60,6 +53,23 @@ export default function useSwapApproval(
: undefined,
[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)
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 txs = useAtomValue(transactionsAtom)
if (!chainId || !token || !spender) return false
if (!chainId || !token || !spender) return undefined
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.receipt === undefined &&
@ -56,7 +57,7 @@ export function useIsPendingApproval(token?: Token, spender?: string) {
tx.info.tokenAddress === token.address &&
tx.info.spenderAddress === spender &&
isTransactionRecent(tx)
)
)?.info.response.hash
}
export function TransactionsUpdater() {

@ -11,6 +11,7 @@ import {
ChevronDown as ChevronDownIcon,
Clock as ClockIcon,
CreditCard as CreditCardIcon,
ExternalLink as LinkIcon,
HelpCircle as HelpCircleIcon,
Info as InfoIcon,
Settings as SettingsIcon,
@ -77,6 +78,7 @@ export const Clock = icon(ClockIcon)
export const CreditCard = icon(CreditCardIcon)
export const HelpCircle = icon(HelpCircleIcon)
export const Info = icon(InfoIcon)
export const Link = icon(LinkIcon)
export const Settings = icon(SettingsIcon)
export const Slash = icon(SlashIcon)
export const Trash2 = icon(Trash2Icon)