Skip to content

Commit

Permalink
fix: await syncing allowance to update permit state (#339)
Browse files Browse the repository at this point in the history
* fix: await syncing allowance to update permit state

* fix: clarify pending permit2 usage
  • Loading branch information
zzmp authored Dec 15, 2022
1 parent 57ecbae commit b762666
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 61 deletions.
38 changes: 20 additions & 18 deletions src/components/Swap/SwapActionButton/Permit2Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,64 +3,66 @@ import { PERMIT2_ADDRESS } from '@uniswap/permit2-sdk'
import ActionButton from 'components/ActionButton'
import EtherscanLink from 'components/EtherscanLink'
import { usePendingApproval } from 'hooks/transactions'
import { Permit } from 'hooks/usePermit2'
import { Spinner } from 'icons'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { InterfaceTrade } from 'state/routing/types'
import { ApprovalTransactionInfo } from 'state/transactions'
import { Colors } from 'theme'
import { ExplorerDataType } from 'utils/getExplorerLink'

interface PermitButtonProps extends Permit {
color: keyof Colors
onSubmit: (submit: () => Promise<ApprovalTransactionInfo | void>) => Promise<void>
}

/**
* An approving PermitButton.
* Should only be rendered if a valid trade exists that is not yet permitted.
*/
export default function PermitButton({
color,
trade,
token,
isSyncing: isApprovalPending,
callback,
color,
onSubmit,
}: {
color: keyof Colors
trade?: InterfaceTrade
callback?: (isPendingApproval: boolean) => Promise<ApprovalTransactionInfo | void>
onSubmit: (submit: () => Promise<ApprovalTransactionInfo | void>) => Promise<void>
}) {
const currency = trade?.inputAmount?.currency
}: PermitButtonProps) {
const [isPending, setIsPending] = useState(false)
const [isFailed, setIsFailed] = useState(false)
const pendingApproval = usePendingApproval(currency?.isToken ? currency : undefined, PERMIT2_ADDRESS)
const pendingApproval = usePendingApproval(token, PERMIT2_ADDRESS)
useEffect(() => {
// Reset pending/failed state if currency changes.
setIsPending(false)
setIsFailed(false)
}, [currency])
}, [token])

const onClick = useCallback(async () => {
setIsPending(true)
try {
await onSubmit(async () => await callback?.(Boolean(pendingApproval)))
await onSubmit(async () => await callback?.())
setIsFailed(false)
} catch (e) {
console.error(e)
setIsFailed(true)
} finally {
setIsPending(false)
}
}, [callback, onSubmit, pendingApproval])
}, [callback, onSubmit])

const action = useMemo(() => {
if (isPending) {
return {
icon: Spinner,
message: t`Approve in your wallet`,
}
} else if (pendingApproval) {
} else if (isApprovalPending) {
return {
icon: Spinner,
message: (
message: pendingApproval ? (
<EtherscanLink type={ExplorerDataType.TRANSACTION} data={pendingApproval}>
<Trans>Approval pending</Trans>
</EtherscanLink>
) : (
<Trans>Approval pending</Trans>
),
}
} else if (isFailed) {
Expand All @@ -71,11 +73,11 @@ export default function PermitButton({
} else {
return {
tooltipContent: t`Permission is required for Uniswap to swap each token. This will expire after one month for your security.`,
message: t`Approve use of ${currency?.symbol ?? 'token'}`,
message: t`Approve use of ${token ?? 'token'}`,
onClick,
}
}
}, [currency?.symbol, isFailed, isPending, onClick, pendingApproval])
}, [isApprovalPending, isFailed, isPending, onClick, pendingApproval, token])

return (
<ActionButton color={color} disabled={!action?.onClick} action={action}>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Swap/SwapActionButton/SwapButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export default function SwapButton({

if (permit2Enabled) {
if (!disabled && permit.state === PermitState.PERMIT_NEEDED) {
return <PermitButton color={color} onSubmit={onSubmit} trade={trade} {...permit} />
return <PermitButton color={color} onSubmit={onSubmit} {...permit} />
}
} else {
if (!disabled && approval.state !== SwapApprovalState.APPROVED) {
Expand Down
10 changes: 5 additions & 5 deletions src/hooks/useApproval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,22 @@ export 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(
Expand Down
89 changes: 58 additions & 31 deletions src/hooks/usePermit2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@ import { STANDARD_L1_BLOCK_TIME } from 'constants/chainInfo'
import { useCallback, useEffect, useMemo, useState } from 'react'

import { ApprovalTransactionInfo } from '..'
import { usePendingApproval } from './transactions'
import useInterval from './useInterval'
import { PermitSignature, usePermitAllowance, useUpdatePermitAllowance } from './usePermitAllowance'
import { useTokenAllowance, useUpdateTokenAllowance } from './useTokenAllowance'

enum SyncState {
PENDING,
SYNCING,
SYNCED,
}

export enum PermitState {
INVALID,
LOADING,
Expand All @@ -17,21 +24,24 @@ export enum PermitState {
}

export interface Permit {
token?: Token
state: PermitState
isSyncing?: boolean
signature?: PermitSignature
callback?: (isPendingApproval: boolean) => Promise<ApprovalTransactionInfo | void>
callback?: () => Promise<ApprovalTransactionInfo | void>
}

export default function usePermit(amount?: CurrencyAmount<Token>, spender?: string): Permit {
const { account } = useWeb3React()
const tokenAllowance = useTokenAllowance(amount?.currency, account, PERMIT2_ADDRESS)
const token = amount?.currency
const { tokenAllowance, isSyncing: isApprovalSyncing } = useTokenAllowance(token, account, PERMIT2_ADDRESS)
const updateTokenAllowance = useUpdateTokenAllowance(amount, PERMIT2_ADDRESS)
const isAllowed = useMemo(
() => amount && (tokenAllowance?.greaterThan(amount) || tokenAllowance?.equalTo(amount)),
[amount, tokenAllowance]
)

const permitAllowance = usePermitAllowance(amount?.currency, spender)
const permitAllowance = usePermitAllowance(token, spender)
const [permitAllowanceAmount, setPermitAllowanceAmount] = useState(permitAllowance?.amount)
useEffect(() => setPermitAllowanceAmount(permitAllowance?.amount), [permitAllowance?.amount])
const isPermitted = useMemo(
Expand All @@ -40,15 +50,10 @@ export default function usePermit(amount?: CurrencyAmount<Token>, spender?: stri
)

const [signature, setSignature] = useState<PermitSignature>()
const updatePermitAllowance = useUpdatePermitAllowance(
amount?.currency,
spender,
permitAllowance?.nonce,
setSignature
)
const updatePermitAllowance = useUpdatePermitAllowance(token, spender, permitAllowance?.nonce, setSignature)
const isSigned = useMemo(
() => amount && signature?.details.token === amount?.currency.address && signature?.spender === spender,
[amount, signature?.details.token, signature?.spender, spender]
() => amount && signature?.details.token === token?.address && signature?.spender === spender,
[amount, signature?.details.token, signature?.spender, spender, token?.address]
)

// Trigger a re-render if either tokenAllowance or signature expire.
Expand All @@ -67,32 +72,54 @@ export default function usePermit(amount?: CurrencyAmount<Token>, spender?: stri
true
)

const callback = useCallback(
async (isPendingApproval: boolean) => {
let info: ApprovalTransactionInfo | undefined
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 = Boolean(usePendingApproval(token, 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: ApprovalTransactionInfo | undefined
if (!isAllowed && !hasPendingApproval) {
info = await updateTokenAllowance()
}
if (!isPermitted && !isSigned) {
await updatePermitAllowance()
}
return info
}, [hasPendingApproval, isAllowed, isPermitted, isSigned, updatePermitAllowance, updateTokenAllowance])

return useMemo(() => {
if (!amount) {
if (!token) {
return { state: PermitState.INVALID }
} else if (!tokenAllowance || !permitAllowance) {
return { state: PermitState.LOADING }
}

if (!tokenAllowance || !permitAllowance) {
return { token, state: PermitState.LOADING }
} else if (isAllowed) {
if (isPermitted) {
return { state: PermitState.PERMITTED }
return { token, state: PermitState.PERMITTED }
} else if (isSigned) {
return { state: PermitState.PERMITTED, signature }
return { token, state: PermitState.PERMITTED, signature }
}
}
return { state: PermitState.PERMIT_NEEDED, callback }
}, [amount, callback, isAllowed, isPermitted, isSigned, permitAllowance, signature, tokenAllowance])

return { token, state: PermitState.PERMIT_NEEDED, isSyncing, callback }
}, [callback, isAllowed, isPermitted, isSigned, isSyncing, permitAllowance, signature, token, tokenAllowance])
}
19 changes: 13 additions & 6 deletions src/hooks/useTokenAllowance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,23 @@ import { calculateGasMargin } from 'utils/calculateGasMargin'

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 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<Token> | undefined, spender: string) {
Expand Down

1 comment on commit b762666

@vercel
Copy link

@vercel vercel bot commented on b762666 Dec 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

widgets – ./

widgets-uniswap.vercel.app
widgets-seven-tau.vercel.app
widgets-git-main-uniswap.vercel.app

Please sign in to comment.