From 8b1bf09ff13fafee42365800eae26621d0340274 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Thu, 15 Dec 2022 09:48:18 -0800 Subject: [PATCH] fix: await syncing allowance to update permit state (#5689) * fix: await syncing allowance to update permit state * fix: clarify isSyncing on Permit2 * fix: further clarify isApprovalSyncing --- src/hooks/usePermit2.ts | 61 ++++++++++++++++++++++++---------- src/hooks/useTokenAllowance.ts | 19 +++++++---- src/lib/hooks/useApproval.ts | 10 +++--- src/pages/Swap/index.tsx | 16 +++------ 4 files changed, 66 insertions(+), 40 deletions(-) diff --git a/src/hooks/usePermit2.ts b/src/hooks/usePermit2.ts index aac9b6c4a3..6e754b4f6c 100644 --- a/src/hooks/usePermit2.ts +++ b/src/hooks/usePermit2.ts @@ -5,11 +5,18 @@ import { useWeb3React } from '@web3-react/core' import { AVERAGE_L1_BLOCK_TIME } from 'constants/chainInfo' import useInterval from 'lib/hooks/useInterval' import { useCallback, useEffect, useMemo, useState } from 'react' +import { useHasPendingApproval } from 'state/transactions/hooks' import { ApproveTransactionInfo } from 'state/transactions/types' import { PermitSignature, usePermitAllowance, useUpdatePermitAllowance } from './usePermitAllowance' import { useTokenAllowance, useUpdateTokenAllowance } from './useTokenAllowance' +enum SyncState { + PENDING, + SYNCING, + SYNCED, +} + export enum PermitState { INVALID, LOADING, @@ -19,8 +26,9 @@ export enum PermitState { export interface Permit { state: PermitState + isSyncing?: boolean signature?: PermitSignature - callback?: (sPendingApproval: boolean) => Promise<{ + callback?: () => Promise<{ response: ContractTransaction info: ApproveTransactionInfo } | void> @@ -28,7 +36,7 @@ export interface Permit { export default function usePermit(amount?: CurrencyAmount, spender?: string): Permit { const { account } = useWeb3React() - const tokenAllowance = useTokenAllowance(amount?.currency, account, PERMIT2_ADDRESS) + const { tokenAllowance, isSyncing: isApprovalSyncing } = useTokenAllowance(amount?.currency, account, PERMIT2_ADDRESS) const updateTokenAllowance = useUpdateTokenAllowance(amount, PERMIT2_ADDRESS) const isAllowed = useMemo( () => amount && (tokenAllowance?.greaterThan(amount) || tokenAllowance?.equalTo(amount)), @@ -71,19 +79,38 @@ export default function usePermit(amount?: CurrencyAmount, spender?: stri true ) - const callback = useCallback( - async (isPendingApproval: boolean) => { - let info - if (!isAllowed && !isPendingApproval) { - info = await updateTokenAllowance() - } - if (!isPermitted && !isSigned) { - await updatePermitAllowance() - } - return info - }, - [isAllowed, isPermitted, isSigned, updatePermitAllowance, updateTokenAllowance] - ) + // Permit2 should be marked syncing from the time approval is submitted (pending) until it is + // synced in tokenAllowance, to avoid re-prompting the user for an already-submitted approval. + // It should *not* be marked syncing if not permitted, because the user must still take action. + const [syncState, setSyncState] = useState(SyncState.SYNCED) + const isSyncing = isPermitted || isSigned ? false : syncState !== SyncState.SYNCED + const hasPendingApproval = useHasPendingApproval(amount?.currency, PERMIT2_ADDRESS) + useEffect(() => { + if (hasPendingApproval) { + setSyncState(SyncState.PENDING) + } else { + setSyncState((state) => { + if (state === SyncState.PENDING && isApprovalSyncing) { + return SyncState.SYNCING + } else if (state === SyncState.SYNCING && !isApprovalSyncing) { + return SyncState.SYNCED + } else { + return state + } + }) + } + }, [hasPendingApproval, isApprovalSyncing]) + + const callback = useCallback(async () => { + let info + if (!isAllowed && !hasPendingApproval) { + info = await updateTokenAllowance() + } + if (!isPermitted && !isSigned) { + await updatePermitAllowance() + } + return info + }, [hasPendingApproval, isAllowed, isPermitted, isSigned, updatePermitAllowance, updateTokenAllowance]) return useMemo(() => { if (!amount) { @@ -97,6 +124,6 @@ export default function usePermit(amount?: CurrencyAmount, spender?: stri return { state: PermitState.PERMITTED, signature } } } - return { state: PermitState.PERMIT_NEEDED, callback } - }, [amount, callback, isAllowed, isPermitted, isSigned, permitAllowance, signature, tokenAllowance]) + return { state: PermitState.PERMIT_NEEDED, isSyncing, callback } + }, [amount, callback, isAllowed, isPermitted, isSigned, isSyncing, permitAllowance, signature, tokenAllowance]) } diff --git a/src/hooks/useTokenAllowance.ts b/src/hooks/useTokenAllowance.ts index e4f8018f56..4a68c2d625 100644 --- a/src/hooks/useTokenAllowance.ts +++ b/src/hooks/useTokenAllowance.ts @@ -8,16 +8,23 @@ import { calculateGasMargin } from 'utils/calculateGasMargin' import { useTokenContract } from './useContract' -export function useTokenAllowance(token?: Token, owner?: string, spender?: string): CurrencyAmount | undefined { +export function useTokenAllowance( + token?: Token, + owner?: string, + spender?: string +): { + tokenAllowance: CurrencyAmount | undefined + isSyncing: boolean +} { const contract = useTokenContract(token?.address, false) const inputs = useMemo(() => [owner, spender], [owner, spender]) - const allowance = useSingleCallResult(contract, 'allowance', inputs).result + const { result, syncing: isSyncing } = useSingleCallResult(contract, 'allowance', inputs) - return useMemo( - () => (token && allowance ? CurrencyAmount.fromRawAmount(token, allowance.toString()) : undefined), - [token, allowance] - ) + return useMemo(() => { + const tokenAllowance = token && result && CurrencyAmount.fromRawAmount(token, result.toString()) + return { tokenAllowance, isSyncing } + }, [isSyncing, result, token]) } export function useUpdateTokenAllowance(amount: CurrencyAmount | undefined, spender: string) { diff --git a/src/lib/hooks/useApproval.ts b/src/lib/hooks/useApproval.ts index 71f128d796..77ca27947d 100644 --- a/src/lib/hooks/useApproval.ts +++ b/src/lib/hooks/useApproval.ts @@ -25,22 +25,22 @@ function useApprovalStateForSpender( const { account } = useWeb3React() const token = amountToApprove?.currency?.isToken ? amountToApprove.currency : undefined - const currentAllowance = useTokenAllowance(token, account ?? undefined, spender) + const { tokenAllowance } = useTokenAllowance(token, account ?? undefined, spender) const pendingApproval = useIsPendingApproval(token, spender) return useMemo(() => { if (!amountToApprove || !spender) return ApprovalState.UNKNOWN if (amountToApprove.currency.isNative) return ApprovalState.APPROVED // we might not have enough data to know whether or not we need to approve - if (!currentAllowance) return ApprovalState.UNKNOWN + if (!tokenAllowance) return ApprovalState.UNKNOWN - // amountToApprove will be defined if currentAllowance is - return currentAllowance.lessThan(amountToApprove) + // amountToApprove will be defined if tokenAllowance is + return tokenAllowance.lessThan(amountToApprove) ? pendingApproval ? ApprovalState.PENDING : ApprovalState.NOT_APPROVED : ApprovalState.APPROVED - }, [amountToApprove, currentAllowance, pendingApproval, spender]) + }, [amountToApprove, pendingApproval, spender, tokenAllowance]) } export function useApproval( diff --git a/src/pages/Swap/index.tsx b/src/pages/Swap/index.tsx index 77b22c0ece..9e079e681d 100644 --- a/src/pages/Swap/index.tsx +++ b/src/pages/Swap/index.tsx @@ -1,7 +1,6 @@ import { Trans } from '@lingui/macro' import { sendAnalyticsEvent, Trace, TraceEvent } from '@uniswap/analytics' import { BrowserEvent, ElementName, EventName, PageName, SectionName } from '@uniswap/analytics-events' -import { PERMIT2_ADDRESS } from '@uniswap/permit2-sdk' import { Trade } from '@uniswap/router-sdk' import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core' import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk' @@ -28,7 +27,7 @@ import { Text } from 'rebass' import { useToggleWalletModal } from 'state/application/hooks' import { InterfaceTrade } from 'state/routing/types' import { TradeState } from 'state/routing/types' -import { useHasPendingApproval, useTransactionAdder } from 'state/transactions/hooks' +import { useTransactionAdder } from 'state/transactions/hooks' import styled, { useTheme } from 'styled-components/macro' import { currencyAmountToPreciseFloat, formatTransactionAmount } from 'utils/formatNumbers' @@ -300,14 +299,14 @@ export default function Swap({ className }: { className?: string }) { permit2Enabled ? maximumAmountIn : undefined, permit2Enabled && chainId ? UNIVERSAL_ROUTER_ADDRESS(chainId) : undefined ) + const isApprovalPending = permit.isSyncing const [isPermitPending, setIsPermitPending] = useState(false) const [isPermitFailed, setIsPermitFailed] = useState(false) const addTransaction = useTransactionAdder() - const isApprovalPending = useHasPendingApproval(maximumAmountIn?.currency, PERMIT2_ADDRESS) const updatePermit = useCallback(async () => { setIsPermitPending(true) try { - const approval = await permit.callback?.(isApprovalPending) + const approval = await permit.callback?.() if (approval) { sendAnalyticsEvent(EventName.APPROVE_TOKEN_TXN_SUBMITTED, { chain_id: chainId, @@ -325,14 +324,7 @@ export default function Swap({ className }: { className?: string }) { } finally { setIsPermitPending(false) } - }, [ - addTransaction, - chainId, - isApprovalPending, - maximumAmountIn?.currency.address, - maximumAmountIn?.currency.symbol, - permit, - ]) + }, [addTransaction, chainId, maximumAmountIn?.currency.address, maximumAmountIn?.currency.symbol, permit]) // check whether the user has approved the router on the input token const [approvalState, approveCallback] = useApproveCallbackFromTrade(