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
This commit is contained in:
Zach Pomerantz 2022-12-15 09:48:18 -08:00 committed by GitHub
parent 6383e9e4bf
commit 8b1bf09ff1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 66 additions and 40 deletions

@ -5,11 +5,18 @@ import { useWeb3React } from '@web3-react/core'
import { AVERAGE_L1_BLOCK_TIME } from 'constants/chainInfo' import { AVERAGE_L1_BLOCK_TIME } from 'constants/chainInfo'
import useInterval from 'lib/hooks/useInterval' import useInterval from 'lib/hooks/useInterval'
import { useCallback, useEffect, useMemo, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import { useHasPendingApproval } from 'state/transactions/hooks'
import { ApproveTransactionInfo } from 'state/transactions/types' import { ApproveTransactionInfo } from 'state/transactions/types'
import { PermitSignature, usePermitAllowance, useUpdatePermitAllowance } from './usePermitAllowance' import { PermitSignature, usePermitAllowance, useUpdatePermitAllowance } from './usePermitAllowance'
import { useTokenAllowance, useUpdateTokenAllowance } from './useTokenAllowance' import { useTokenAllowance, useUpdateTokenAllowance } from './useTokenAllowance'
enum SyncState {
PENDING,
SYNCING,
SYNCED,
}
export enum PermitState { export enum PermitState {
INVALID, INVALID,
LOADING, LOADING,
@ -19,8 +26,9 @@ export enum PermitState {
export interface Permit { export interface Permit {
state: PermitState state: PermitState
isSyncing?: boolean
signature?: PermitSignature signature?: PermitSignature
callback?: (sPendingApproval: boolean) => Promise<{ callback?: () => Promise<{
response: ContractTransaction response: ContractTransaction
info: ApproveTransactionInfo info: ApproveTransactionInfo
} | void> } | void>
@ -28,7 +36,7 @@ export interface Permit {
export default function usePermit(amount?: CurrencyAmount<Token>, spender?: string): Permit { export default function usePermit(amount?: CurrencyAmount<Token>, spender?: string): Permit {
const { account } = useWeb3React() 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 updateTokenAllowance = useUpdateTokenAllowance(amount, PERMIT2_ADDRESS)
const isAllowed = useMemo( const isAllowed = useMemo(
() => amount && (tokenAllowance?.greaterThan(amount) || tokenAllowance?.equalTo(amount)), () => amount && (tokenAllowance?.greaterThan(amount) || tokenAllowance?.equalTo(amount)),
@ -71,19 +79,38 @@ export default function usePermit(amount?: CurrencyAmount<Token>, spender?: stri
true true
) )
const callback = useCallback( // Permit2 should be marked syncing from the time approval is submitted (pending) until it is
async (isPendingApproval: boolean) => { // synced in tokenAllowance, to avoid re-prompting the user for an already-submitted approval.
let info // It should *not* be marked syncing if not permitted, because the user must still take action.
if (!isAllowed && !isPendingApproval) { const [syncState, setSyncState] = useState(SyncState.SYNCED)
info = await updateTokenAllowance() const isSyncing = isPermitted || isSigned ? false : syncState !== SyncState.SYNCED
} const hasPendingApproval = useHasPendingApproval(amount?.currency, PERMIT2_ADDRESS)
if (!isPermitted && !isSigned) { useEffect(() => {
await updatePermitAllowance() if (hasPendingApproval) {
} setSyncState(SyncState.PENDING)
return info } else {
}, setSyncState((state) => {
[isAllowed, isPermitted, isSigned, updatePermitAllowance, updateTokenAllowance] 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(() => { return useMemo(() => {
if (!amount) { if (!amount) {
@ -97,6 +124,6 @@ export default function usePermit(amount?: CurrencyAmount<Token>, spender?: stri
return { state: PermitState.PERMITTED, signature } return { state: PermitState.PERMITTED, signature }
} }
} }
return { state: PermitState.PERMIT_NEEDED, callback } return { state: PermitState.PERMIT_NEEDED, isSyncing, callback }
}, [amount, callback, isAllowed, isPermitted, isSigned, permitAllowance, signature, tokenAllowance]) }, [amount, callback, isAllowed, isPermitted, isSigned, isSyncing, permitAllowance, signature, tokenAllowance])
} }

@ -8,16 +8,23 @@ import { calculateGasMargin } from 'utils/calculateGasMargin'
import { useTokenContract } from './useContract' import { useTokenContract } from './useContract'
export function useTokenAllowance(token?: Token, owner?: string, spender?: string): CurrencyAmount<Token> | undefined { export function useTokenAllowance(
token?: Token,
owner?: string,
spender?: string
): {
tokenAllowance: CurrencyAmount<Token> | undefined
isSyncing: boolean
} {
const contract = useTokenContract(token?.address, false) const contract = useTokenContract(token?.address, false)
const inputs = useMemo(() => [owner, spender], [owner, spender]) const inputs = useMemo(() => [owner, spender], [owner, spender])
const allowance = useSingleCallResult(contract, 'allowance', inputs).result const { result, syncing: isSyncing } = useSingleCallResult(contract, 'allowance', inputs)
return useMemo( return useMemo(() => {
() => (token && allowance ? CurrencyAmount.fromRawAmount(token, allowance.toString()) : undefined), const tokenAllowance = token && result && CurrencyAmount.fromRawAmount(token, result.toString())
[token, allowance] return { tokenAllowance, isSyncing }
) }, [isSyncing, result, token])
} }
export function useUpdateTokenAllowance(amount: CurrencyAmount<Token> | undefined, spender: string) { export function useUpdateTokenAllowance(amount: CurrencyAmount<Token> | undefined, spender: string) {

@ -25,22 +25,22 @@ function useApprovalStateForSpender(
const { account } = useWeb3React() const { account } = useWeb3React()
const token = amountToApprove?.currency?.isToken ? amountToApprove.currency : undefined 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) const pendingApproval = useIsPendingApproval(token, spender)
return useMemo(() => { return useMemo(() => {
if (!amountToApprove || !spender) return ApprovalState.UNKNOWN if (!amountToApprove || !spender) return ApprovalState.UNKNOWN
if (amountToApprove.currency.isNative) return ApprovalState.APPROVED if (amountToApprove.currency.isNative) return ApprovalState.APPROVED
// we might not have enough data to know whether or not we need to approve // 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 // amountToApprove will be defined if tokenAllowance is
return currentAllowance.lessThan(amountToApprove) return tokenAllowance.lessThan(amountToApprove)
? pendingApproval ? pendingApproval
? ApprovalState.PENDING ? ApprovalState.PENDING
: ApprovalState.NOT_APPROVED : ApprovalState.NOT_APPROVED
: ApprovalState.APPROVED : ApprovalState.APPROVED
}, [amountToApprove, currentAllowance, pendingApproval, spender]) }, [amountToApprove, pendingApproval, spender, tokenAllowance])
} }
export function useApproval( export function useApproval(

@ -1,7 +1,6 @@
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { sendAnalyticsEvent, Trace, TraceEvent } from '@uniswap/analytics' import { sendAnalyticsEvent, Trace, TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, ElementName, EventName, PageName, SectionName } from '@uniswap/analytics-events' import { BrowserEvent, ElementName, EventName, PageName, SectionName } from '@uniswap/analytics-events'
import { PERMIT2_ADDRESS } from '@uniswap/permit2-sdk'
import { Trade } from '@uniswap/router-sdk' import { Trade } from '@uniswap/router-sdk'
import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core' import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk' import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk'
@ -28,7 +27,7 @@ import { Text } from 'rebass'
import { useToggleWalletModal } from 'state/application/hooks' import { useToggleWalletModal } from 'state/application/hooks'
import { InterfaceTrade } from 'state/routing/types' import { InterfaceTrade } from 'state/routing/types'
import { TradeState } 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 styled, { useTheme } from 'styled-components/macro'
import { currencyAmountToPreciseFloat, formatTransactionAmount } from 'utils/formatNumbers' import { currencyAmountToPreciseFloat, formatTransactionAmount } from 'utils/formatNumbers'
@ -300,14 +299,14 @@ export default function Swap({ className }: { className?: string }) {
permit2Enabled ? maximumAmountIn : undefined, permit2Enabled ? maximumAmountIn : undefined,
permit2Enabled && chainId ? UNIVERSAL_ROUTER_ADDRESS(chainId) : undefined permit2Enabled && chainId ? UNIVERSAL_ROUTER_ADDRESS(chainId) : undefined
) )
const isApprovalPending = permit.isSyncing
const [isPermitPending, setIsPermitPending] = useState(false) const [isPermitPending, setIsPermitPending] = useState(false)
const [isPermitFailed, setIsPermitFailed] = useState(false) const [isPermitFailed, setIsPermitFailed] = useState(false)
const addTransaction = useTransactionAdder() const addTransaction = useTransactionAdder()
const isApprovalPending = useHasPendingApproval(maximumAmountIn?.currency, PERMIT2_ADDRESS)
const updatePermit = useCallback(async () => { const updatePermit = useCallback(async () => {
setIsPermitPending(true) setIsPermitPending(true)
try { try {
const approval = await permit.callback?.(isApprovalPending) const approval = await permit.callback?.()
if (approval) { if (approval) {
sendAnalyticsEvent(EventName.APPROVE_TOKEN_TXN_SUBMITTED, { sendAnalyticsEvent(EventName.APPROVE_TOKEN_TXN_SUBMITTED, {
chain_id: chainId, chain_id: chainId,
@ -325,14 +324,7 @@ export default function Swap({ className }: { className?: string }) {
} finally { } finally {
setIsPermitPending(false) setIsPermitPending(false)
} }
}, [ }, [addTransaction, chainId, maximumAmountIn?.currency.address, maximumAmountIn?.currency.symbol, permit])
addTransaction,
chainId,
isApprovalPending,
maximumAmountIn?.currency.address,
maximumAmountIn?.currency.symbol,
permit,
])
// check whether the user has approved the router on the input token // check whether the user has approved the router on the input token
const [approvalState, approveCallback] = useApproveCallbackFromTrade( const [approvalState, approveCallback] = useApproveCallbackFromTrade(