Commit 79a72d6f authored by eddie's avatar eddie Committed by GitHub

feat: update permit2 hook to allow revoking (#6727)

* feat: update permit2 hook to allow revoking

* fix: types

* fix: rename reset to revoke

* fix: re-use useUpdateTokenAllowance

* fix: add tests

* fix: improve code style

* fix: undefined nit
parent f3bfd4ad
......@@ -3,10 +3,10 @@ import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { AVERAGE_L1_BLOCK_TIME } from 'constants/chainInfo'
import { PermitSignature, usePermitAllowance, useUpdatePermitAllowance } from 'hooks/usePermitAllowance'
import { useTokenAllowance, useUpdateTokenAllowance } from 'hooks/useTokenAllowance'
import { useRevokeTokenAllowance, useTokenAllowance, useUpdateTokenAllowance } from 'hooks/useTokenAllowance'
import useInterval from 'lib/hooks/useInterval'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useHasPendingApproval, useTransactionAdder } from 'state/transactions/hooks'
import { useHasPendingApproval, useHasPendingRevocation, useTransactionAdder } from 'state/transactions/hooks'
enum ApprovalState {
PENDING,
......@@ -25,11 +25,14 @@ interface AllowanceRequired {
token: Token
isApprovalLoading: boolean
isApprovalPending: boolean
isRevocationPending: boolean
approveAndPermit: () => Promise<void>
approve: () => Promise<void>
permit: () => Promise<void>
revoke: () => Promise<void>
needsPermit2Approval: boolean
needsSignature: boolean
allowedAmount: CurrencyAmount<Token>
}
export type Allowance =
......@@ -46,6 +49,7 @@ export default function usePermit2Allowance(amount?: CurrencyAmount<Token>, spen
const { tokenAllowance, isSyncing: isApprovalSyncing } = useTokenAllowance(token, account, PERMIT2_ADDRESS)
const updateTokenAllowance = useUpdateTokenAllowance(amount, PERMIT2_ADDRESS)
const revokeTokenAllowance = useRevokeTokenAllowance(token, PERMIT2_ADDRESS)
const isApproved = useMemo(() => {
if (!amount || !tokenAllowance) return false
return tokenAllowance.greaterThan(amount) || tokenAllowance.equalTo(amount)
......@@ -57,6 +61,8 @@ export default function usePermit2Allowance(amount?: CurrencyAmount<Token>, spen
const [approvalState, setApprovalState] = useState(ApprovalState.SYNCED)
const isApprovalLoading = approvalState !== ApprovalState.SYNCED
const isApprovalPending = useHasPendingApproval(token, PERMIT2_ADDRESS)
const isRevocationPending = useHasPendingRevocation(token, PERMIT2_ADDRESS)
useEffect(() => {
if (isApprovalPending) {
setApprovalState(ApprovalState.PENDING)
......@@ -107,17 +113,14 @@ export default function usePermit2Allowance(amount?: CurrencyAmount<Token>, spen
}, [addTransaction, shouldRequestApproval, shouldRequestSignature, updatePermitAllowance, updateTokenAllowance])
const approve = useCallback(async () => {
if (shouldRequestApproval) {
const { response, info } = await updateTokenAllowance()
addTransaction(response, info)
}
}, [addTransaction, shouldRequestApproval, updateTokenAllowance])
const { response, info } = await updateTokenAllowance()
addTransaction(response, info)
}, [addTransaction, updateTokenAllowance])
const permit = useCallback(async () => {
if (shouldRequestSignature) {
await updatePermitAllowance()
}
}, [shouldRequestSignature, updatePermitAllowance])
const revoke = useCallback(async () => {
const { response, info } = await revokeTokenAllowance()
addTransaction(response, info)
}, [addTransaction, revokeTokenAllowance])
return useMemo(() => {
if (token) {
......@@ -129,11 +132,14 @@ export default function usePermit2Allowance(amount?: CurrencyAmount<Token>, spen
state: AllowanceState.REQUIRED,
isApprovalLoading: false,
isApprovalPending,
isRevocationPending,
approveAndPermit,
approve,
permit,
permit: updatePermitAllowance,
revoke,
needsPermit2Approval: !isApproved,
needsSignature: shouldRequestSignature,
allowedAmount: tokenAllowance,
}
} else if (!isApproved) {
return {
......@@ -141,11 +147,14 @@ export default function usePermit2Allowance(amount?: CurrencyAmount<Token>, spen
state: AllowanceState.REQUIRED,
isApprovalLoading,
isApprovalPending,
isRevocationPending,
approveAndPermit,
approve,
permit,
permit: updatePermitAllowance,
revoke,
needsPermit2Approval: true,
needsSignature: shouldRequestSignature,
allowedAmount: tokenAllowance,
}
}
}
......@@ -153,8 +162,8 @@ export default function usePermit2Allowance(amount?: CurrencyAmount<Token>, spen
token,
state: AllowanceState.ALLOWED,
permitSignature: !isPermitted && isSigned ? signature : undefined,
needsPermit2Approval: false,
needsSignature: false,
needsSetupApproval: false,
needsPermitSignature: false,
}
}, [
approve,
......@@ -164,8 +173,10 @@ export default function usePermit2Allowance(amount?: CurrencyAmount<Token>, spen
isApproved,
isPermitted,
isSigned,
permit,
updatePermitAllowance,
permitAllowance,
revoke,
isRevocationPending,
shouldRequestSignature,
signature,
token,
......
......@@ -68,3 +68,10 @@ export function useUpdateTokenAllowance(
}
}, [amount, contract, spender])
}
export function useRevokeTokenAllowance(
token: Token | undefined,
spender: string
): () => Promise<{ response: ContractTransaction; info: ApproveTransactionInfo }> {
return useUpdateTokenAllowance(token ? CurrencyAmount.fromRawAmount(token, 0) : undefined, spender)
}
import { BigNumber } from '@ethersproject/bignumber'
import { parseEther } from '@ethersproject/units'
import { Percent, SupportedChainId } from '@uniswap/sdk-core'
import { CurrencyAmount, Percent, SupportedChainId } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { nativeOnChain } from 'constants/tokens'
import { useNftUniversalRouterAddress } from 'graphql/data/nft/NftUniversalRouterAddress'
......@@ -347,8 +347,11 @@ describe('BagFooter.tsx', () => {
approveAndPermit: () => Promise.resolve(),
approve: () => Promise.resolve(),
permit: () => Promise.resolve(),
revoke: () => Promise.resolve(),
needsPermit2Approval: false,
needsSignature: false,
isRevocationPending: false,
allowedAmount: CurrencyAmount.fromRawAmount(TEST_TOKEN_1, 0),
},
isAllowancePending: false,
isApprovalLoading: true,
......@@ -372,8 +375,11 @@ describe('BagFooter.tsx', () => {
approveAndPermit: () => Promise.resolve(),
approve: () => Promise.resolve(),
permit: () => Promise.resolve(),
revoke: () => Promise.resolve(),
needsPermit2Approval: false,
needsSignature: false,
isRevocationPending: false,
allowedAmount: CurrencyAmount.fromRawAmount(TEST_TOKEN_1, 0),
},
isAllowancePending: true,
isApprovalLoading: false,
......@@ -397,8 +403,11 @@ describe('BagFooter.tsx', () => {
approveAndPermit: () => Promise.resolve(),
approve: () => Promise.resolve(),
permit: () => Promise.resolve(),
revoke: () => Promise.resolve(),
needsPermit2Approval: false,
needsSignature: false,
isRevocationPending: false,
allowedAmount: CurrencyAmount.fromRawAmount(TEST_TOKEN_1, 0),
},
isAllowancePending: false,
isApprovalLoading: false,
......
import { BigNumber } from '@ethersproject/bignumber'
import { PERMIT2_ADDRESS } from '@uniswap/permit2-sdk'
import { useWeb3React } from '@web3-react/core'
import { SupportedChainId } from 'constants/chains'
import { USDC_MAINNET } from 'constants/tokens'
import store from 'state'
import { mocked } from 'test-utils/mocked'
import { act, renderHook } from 'test-utils/render'
import { useHasPendingApproval, useHasPendingRevocation, useTransactionAdder, useTransactionRemover } from './hooks'
import { clearAllTransactions, finalizeTransaction } from './reducer'
import { ApproveTransactionInfo, TransactionInfo, TransactionType } from './types'
const pendingTransactionResponse = {
hash: '0x123',
timestamp: 1000,
from: '0x123',
wait: jest.fn(),
nonce: 1,
gasLimit: BigNumber.from(1000),
data: '0x',
value: BigNumber.from(0),
chainId: SupportedChainId.MAINNET,
confirmations: 0,
blockNumber: undefined,
blockHash: undefined,
}
const mockApprovalTransactionInfo: ApproveTransactionInfo = {
type: TransactionType.APPROVAL,
tokenAddress: USDC_MAINNET.address,
spender: PERMIT2_ADDRESS,
amount: '10000',
}
const mockRevocationTransactionInfo: TransactionInfo = {
...mockApprovalTransactionInfo,
amount: '0',
}
describe('Transactions hooks', () => {
beforeEach(() => {
mocked(useWeb3React).mockReturnValue({ chainId: 1, account: '0x123' } as ReturnType<typeof useWeb3React>)
jest.useFakeTimers()
store.dispatch(clearAllTransactions({ chainId: SupportedChainId.MAINNET }))
})
function addPendingTransaction(txInfo: TransactionInfo) {
const { result } = renderHook(() => useTransactionAdder())
act(() => {
result.current(pendingTransactionResponse, txInfo)
})
}
function addConfirmedTransaction(txInfo: TransactionInfo) {
addPendingTransaction(txInfo)
act(() => {
store.dispatch(
finalizeTransaction({
chainId: SupportedChainId.MAINNET,
hash: pendingTransactionResponse.hash,
receipt: {
status: 1,
transactionIndex: 1,
transactionHash: pendingTransactionResponse.hash,
to: '0x0',
from: '0x0',
contractAddress: '0x0',
blockHash: '0x0',
blockNumber: 1,
},
})
)
})
}
it('useTransactionAdder adds a transaction', () => {
addPendingTransaction(mockApprovalTransactionInfo)
expect(store.getState().transactions[SupportedChainId.MAINNET][pendingTransactionResponse.hash]).toEqual({
hash: pendingTransactionResponse.hash,
info: mockApprovalTransactionInfo,
from: pendingTransactionResponse.from,
addedTime: Date.now(),
nonce: pendingTransactionResponse.nonce,
deadline: undefined,
})
})
it('useTransactionRemover removes a transaction', () => {
addPendingTransaction(mockApprovalTransactionInfo)
const { result: remover } = renderHook(() => useTransactionRemover())
act(() => {
remover.current(pendingTransactionResponse.hash)
})
expect(store.getState().transactions[SupportedChainId.MAINNET][pendingTransactionResponse.hash]).toBeUndefined()
})
describe('useHasPendingApproval', () => {
it('returns true when there is a pending transaction', () => {
addPendingTransaction(mockApprovalTransactionInfo)
const { result } = renderHook(() => useHasPendingApproval(USDC_MAINNET, PERMIT2_ADDRESS))
expect(result.current).toBe(true)
})
it('returns false when there is a pending transaction but it is not an approval', () => {
addPendingTransaction({
type: TransactionType.SUBMIT_PROPOSAL,
})
const { result } = renderHook(() => useHasPendingApproval(USDC_MAINNET, PERMIT2_ADDRESS))
expect(result.current).toBe(false)
})
it('returns false when there is a pending approval but it is not for the current chain', () => {
mocked(useWeb3React).mockReturnValue({ chainId: 2 } as ReturnType<typeof useWeb3React>)
addPendingTransaction(mockApprovalTransactionInfo)
const { result } = renderHook(() => useHasPendingApproval(USDC_MAINNET, PERMIT2_ADDRESS))
expect(result.current).toBe(false)
})
it('returns false when there is a confirmed approval transaction', () => {
addConfirmedTransaction(mockApprovalTransactionInfo)
const { result } = renderHook(() => useHasPendingApproval(USDC_MAINNET, PERMIT2_ADDRESS))
expect(result.current).toBe(false)
})
it('returns false when there are no pending transactions', () => {
const { result } = renderHook(() => useHasPendingApproval(USDC_MAINNET, PERMIT2_ADDRESS))
expect(result.current).toBe(false)
})
it('returns false when there is a pending revocation', () => {
addPendingTransaction(mockRevocationTransactionInfo)
const { result } = renderHook(() => useHasPendingApproval(USDC_MAINNET, PERMIT2_ADDRESS))
expect(result.current).toBe(false)
})
})
describe('useHasPendingRevocation', () => {
it('returns true when there is a pending revocation', () => {
addPendingTransaction(mockRevocationTransactionInfo)
const { result } = renderHook(() => useHasPendingRevocation(USDC_MAINNET, PERMIT2_ADDRESS))
expect(result.current).toBe(true)
})
it('returns false when there is a pending transaction but it is not a revocation', () => {
addPendingTransaction({
type: TransactionType.SUBMIT_PROPOSAL,
})
const { result } = renderHook(() => useHasPendingRevocation(USDC_MAINNET, PERMIT2_ADDRESS))
expect(result.current).toBe(false)
})
it('returns false when there is a pending revocation but it is not for the current chain', () => {
mocked(useWeb3React).mockReturnValue({ chainId: 2 } as ReturnType<typeof useWeb3React>)
addPendingTransaction(mockRevocationTransactionInfo)
const { result } = renderHook(() => useHasPendingRevocation(USDC_MAINNET, PERMIT2_ADDRESS))
expect(result.current).toBe(false)
})
it('returns false when there is a confirmed approval transaction', () => {
addConfirmedTransaction(mockRevocationTransactionInfo)
const { result } = renderHook(() => useHasPendingRevocation(USDC_MAINNET, PERMIT2_ADDRESS))
expect(result.current).toBe(false)
})
it('returns false when there are no pending transactions', () => {
const { result } = renderHook(() => useHasPendingRevocation(USDC_MAINNET, PERMIT2_ADDRESS))
expect(result.current).toBe(false)
})
it('returns false when there is a pending approval', () => {
addPendingTransaction(mockApprovalTransactionInfo)
const { result } = renderHook(() => useHasPendingRevocation(USDC_MAINNET, PERMIT2_ADDRESS))
expect(result.current).toBe(false)
})
})
})
import { BigNumber } from '@ethersproject/bignumber'
import type { TransactionResponse } from '@ethersproject/providers'
import { Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
......@@ -99,23 +100,28 @@ export function isTransactionRecent(tx: TransactionDetails): boolean {
return new Date().getTime() - tx.addedTime < 86_400_000
}
function usePendingApprovalAmount(token?: Token, spender?: string): BigNumber | undefined {
const allTransactions = useAllTransactions()
return useMemo(() => {
if (typeof token?.address !== 'string' || typeof spender !== 'string') {
return undefined
}
for (const txHash in allTransactions) {
const tx = allTransactions[txHash]
if (!tx || tx.receipt || tx.info.type !== TransactionType.APPROVAL) continue
if (tx.info.spender === spender && tx.info.tokenAddress === token.address && isTransactionRecent(tx)) {
return BigNumber.from(tx.info.amount)
}
}
return undefined
}, [allTransactions, spender, token?.address])
}
// returns whether a token has a pending approval transaction
export function useHasPendingApproval(token?: Token, spender?: string): boolean {
const allTransactions = useAllTransactions()
return useMemo(
() =>
typeof token?.address === 'string' &&
typeof spender === 'string' &&
Object.keys(allTransactions).some((hash) => {
const tx = allTransactions[hash]
if (!tx) return false
if (tx.receipt) {
return false
} else {
if (tx.info.type !== TransactionType.APPROVAL) return false
return tx.info.spender === spender && tx.info.tokenAddress === token.address && isTransactionRecent(tx)
}
}),
[allTransactions, spender, token?.address]
)
return usePendingApprovalAmount(token, spender)?.gt(0) ?? false
}
export function useHasPendingRevocation(token?: Token, spender?: string): boolean {
return usePendingApprovalAmount(token, spender)?.eq(0) ?? false
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment