fix: update permit2 to match widget implementation again (#5826)
* fix: update permit2 to match widget implementation (#5821) * refactor: usePermit2->usePermit2Allowance * fix: update permit2 logic to match widgets * fix: lint issues * fix: memoize the interval callback
This commit is contained in:
parent
14b02eda0f
commit
9719af66e5
41
src/abis/permit2.json
Normal file
41
src/abis/permit2.json
Normal file
@ -0,0 +1,41 @@
|
||||
[
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "allowance",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint160",
|
||||
"name": "amount",
|
||||
"type": "uint160"
|
||||
},
|
||||
{
|
||||
"internalType": "uint48",
|
||||
"name": "expiration",
|
||||
"type": "uint48"
|
||||
},
|
||||
{
|
||||
"internalType": "uint48",
|
||||
"name": "nonce",
|
||||
"type": "uint48"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}
|
||||
]
|
@ -1,140 +0,0 @@
|
||||
import { ContractTransaction } from '@ethersproject/contracts'
|
||||
import { PERMIT2_ADDRESS } from '@uniswap/permit2-sdk'
|
||||
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
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,
|
||||
APPROVAL_OR_PERMIT_NEEDED,
|
||||
APPROVAL_LOADING,
|
||||
APPROVED_AND_PERMITTED,
|
||||
}
|
||||
|
||||
export interface Permit {
|
||||
state: PermitState
|
||||
signature?: PermitSignature
|
||||
callback?: () => Promise<{
|
||||
response: ContractTransaction
|
||||
info: ApproveTransactionInfo
|
||||
} | void>
|
||||
}
|
||||
|
||||
export default function usePermit(amount?: CurrencyAmount<Token>, spender?: string): Permit {
|
||||
const { account } = useWeb3React()
|
||||
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)),
|
||||
[amount, tokenAllowance]
|
||||
)
|
||||
|
||||
const permitAllowance = usePermitAllowance(amount?.currency, spender)
|
||||
const [permitAllowanceAmount, setPermitAllowanceAmount] = useState(permitAllowance?.amount)
|
||||
useEffect(() => setPermitAllowanceAmount(permitAllowance?.amount), [permitAllowance?.amount])
|
||||
const isPermitted = useMemo(
|
||||
() => amount && permitAllowanceAmount?.gte(amount.quotient.toString()),
|
||||
[amount, permitAllowanceAmount]
|
||||
)
|
||||
|
||||
const [signature, setSignature] = useState<PermitSignature>()
|
||||
const updatePermitAllowance = useUpdatePermitAllowance(
|
||||
amount?.currency,
|
||||
spender,
|
||||
permitAllowance?.nonce,
|
||||
setSignature
|
||||
)
|
||||
const isSigned = useMemo(
|
||||
() => amount && signature?.details.token === amount?.currency.address && signature?.spender === spender,
|
||||
[amount, signature?.details.token, signature?.spender, spender]
|
||||
)
|
||||
|
||||
// Trigger a re-render if either tokenAllowance or signature expire.
|
||||
useInterval(
|
||||
() => {
|
||||
// Calculate now such that the signature will still be valid for the next block.
|
||||
const now = (Date.now() - AVERAGE_L1_BLOCK_TIME) / 1000
|
||||
if (signature && signature.sigDeadline < now) {
|
||||
setSignature(undefined)
|
||||
}
|
||||
if (permitAllowance && permitAllowance.expiration < now) {
|
||||
setPermitAllowanceAmount(undefined)
|
||||
}
|
||||
},
|
||||
AVERAGE_L1_BLOCK_TIME,
|
||||
true
|
||||
)
|
||||
|
||||
// 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.
|
||||
const [syncState, setSyncState] = useState(SyncState.SYNCED)
|
||||
const isApprovalLoading = 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) {
|
||||
return { state: PermitState.INVALID }
|
||||
} else if (!tokenAllowance || !permitAllowance) {
|
||||
return { state: PermitState.LOADING }
|
||||
} else if (!(isPermitted || isSigned)) {
|
||||
return { state: PermitState.APPROVAL_OR_PERMIT_NEEDED, callback }
|
||||
} else if (!isAllowed) {
|
||||
return {
|
||||
state: isApprovalLoading ? PermitState.APPROVAL_LOADING : PermitState.APPROVAL_OR_PERMIT_NEEDED,
|
||||
callback,
|
||||
}
|
||||
} else {
|
||||
return { state: PermitState.APPROVED_AND_PERMITTED, signature: isPermitted ? undefined : signature }
|
||||
}
|
||||
}, [
|
||||
amount,
|
||||
callback,
|
||||
isAllowed,
|
||||
isApprovalLoading,
|
||||
isPermitted,
|
||||
isSigned,
|
||||
permitAllowance,
|
||||
signature,
|
||||
tokenAllowance,
|
||||
])
|
||||
}
|
126
src/hooks/usePermit2Allowance.ts
Normal file
126
src/hooks/usePermit2Allowance.ts
Normal file
@ -0,0 +1,126 @@
|
||||
import { PERMIT2_ADDRESS } from '@uniswap/permit2-sdk'
|
||||
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 useInterval from 'lib/hooks/useInterval'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useHasPendingApproval, useTransactionAdder } from 'state/transactions/hooks'
|
||||
|
||||
enum ApprovalState {
|
||||
PENDING,
|
||||
SYNCING,
|
||||
SYNCED,
|
||||
}
|
||||
|
||||
export enum AllowanceState {
|
||||
LOADING,
|
||||
REQUIRED,
|
||||
ALLOWED,
|
||||
}
|
||||
|
||||
interface AllowanceRequired {
|
||||
state: AllowanceState.REQUIRED
|
||||
token: Token
|
||||
isApprovalLoading: boolean
|
||||
approveAndPermit: () => Promise<void>
|
||||
}
|
||||
|
||||
type Allowance =
|
||||
| { state: AllowanceState.LOADING }
|
||||
| {
|
||||
state: AllowanceState.ALLOWED
|
||||
permitSignature?: PermitSignature
|
||||
}
|
||||
| AllowanceRequired
|
||||
|
||||
export default function usePermit2Allowance(amount?: CurrencyAmount<Token>, spender?: string): Allowance {
|
||||
const { account } = useWeb3React()
|
||||
const token = amount?.currency
|
||||
|
||||
const { tokenAllowance, isSyncing: isApprovalSyncing } = useTokenAllowance(token, account, PERMIT2_ADDRESS)
|
||||
const updateTokenAllowance = useUpdateTokenAllowance(amount, PERMIT2_ADDRESS)
|
||||
const isApproved = useMemo(() => {
|
||||
if (!amount || !tokenAllowance) return false
|
||||
return tokenAllowance.greaterThan(amount) || tokenAllowance.equalTo(amount)
|
||||
}, [amount, tokenAllowance])
|
||||
|
||||
// Marks approval as loading from the time it is submitted (pending), until it has confirmed and another block synced.
|
||||
// This avoids re-prompting the user for an already-submitted but not-yet-observed approval, by marking it loading
|
||||
// until it has been re-observed. It wll sync immediately, because confirmation fast-forwards the block number.
|
||||
const [approvalState, setApprovalState] = useState(ApprovalState.SYNCED)
|
||||
const isApprovalLoading = approvalState !== ApprovalState.SYNCED
|
||||
const isApprovalPending = useHasPendingApproval(token, PERMIT2_ADDRESS)
|
||||
useEffect(() => {
|
||||
if (isApprovalPending) {
|
||||
setApprovalState(ApprovalState.PENDING)
|
||||
} else {
|
||||
setApprovalState((state) => {
|
||||
if (state === ApprovalState.PENDING && isApprovalSyncing) {
|
||||
return ApprovalState.SYNCING
|
||||
} else if (state === ApprovalState.SYNCING && !isApprovalSyncing) {
|
||||
return ApprovalState.SYNCED
|
||||
}
|
||||
return state
|
||||
})
|
||||
}
|
||||
}, [isApprovalPending, isApprovalSyncing])
|
||||
|
||||
// Signature and PermitAllowance will expire, so they should be rechecked at an interval.
|
||||
// Calculate now such that the signature will still be valid for the submitting block.
|
||||
const [now, setNow] = useState(Date.now() + AVERAGE_L1_BLOCK_TIME)
|
||||
useInterval(
|
||||
useCallback(() => setNow((Date.now() + AVERAGE_L1_BLOCK_TIME) / 1000), []),
|
||||
AVERAGE_L1_BLOCK_TIME
|
||||
)
|
||||
|
||||
const [signature, setSignature] = useState<PermitSignature>()
|
||||
const isSigned = useMemo(() => {
|
||||
if (!amount || !signature) return false
|
||||
return signature.details.token === token?.address && signature.spender === spender && signature.sigDeadline >= now
|
||||
}, [amount, now, signature, spender, token?.address])
|
||||
|
||||
const { permitAllowance, expiration: permitExpiration, nonce } = usePermitAllowance(token, account, spender)
|
||||
const updatePermitAllowance = useUpdatePermitAllowance(token, spender, nonce, setSignature)
|
||||
const isPermitted = useMemo(() => {
|
||||
if (!amount || !permitAllowance || !permitExpiration) return false
|
||||
return (permitAllowance.greaterThan(amount) || permitAllowance.equalTo(amount)) && permitExpiration >= now
|
||||
}, [amount, now, permitAllowance, permitExpiration])
|
||||
|
||||
const shouldRequestApproval = !(isApproved || isApprovalLoading)
|
||||
const shouldRequestSignature = !(isPermitted || isSigned)
|
||||
const addTransaction = useTransactionAdder()
|
||||
const approveAndPermit = useCallback(async () => {
|
||||
if (shouldRequestApproval) {
|
||||
const { response, info } = await updateTokenAllowance()
|
||||
addTransaction(response, info)
|
||||
}
|
||||
if (shouldRequestSignature) {
|
||||
await updatePermitAllowance()
|
||||
}
|
||||
}, [addTransaction, shouldRequestApproval, shouldRequestSignature, updatePermitAllowance, updateTokenAllowance])
|
||||
|
||||
return useMemo(() => {
|
||||
if (token) {
|
||||
if (!tokenAllowance || !permitAllowance) {
|
||||
return { state: AllowanceState.LOADING }
|
||||
} else if (!(isPermitted || isSigned)) {
|
||||
return { token, state: AllowanceState.REQUIRED, isApprovalLoading: false, approveAndPermit }
|
||||
} else if (!isApproved) {
|
||||
return { token, state: AllowanceState.REQUIRED, isApprovalLoading, approveAndPermit }
|
||||
}
|
||||
}
|
||||
return { token, state: AllowanceState.ALLOWED, permitSignature: !isPermitted && isSigned ? signature : undefined }
|
||||
}, [
|
||||
approveAndPermit,
|
||||
isApprovalLoading,
|
||||
isApproved,
|
||||
isPermitted,
|
||||
isSigned,
|
||||
permitAllowance,
|
||||
signature,
|
||||
token,
|
||||
tokenAllowance,
|
||||
])
|
||||
}
|
@ -1,14 +1,10 @@
|
||||
import {
|
||||
AllowanceData,
|
||||
AllowanceProvider,
|
||||
AllowanceTransfer,
|
||||
MaxAllowanceTransferAmount,
|
||||
PERMIT2_ADDRESS,
|
||||
PermitSingle,
|
||||
} from '@uniswap/permit2-sdk'
|
||||
import { Token } from '@uniswap/sdk-core'
|
||||
import { AllowanceTransfer, MaxAllowanceTransferAmount, PERMIT2_ADDRESS, PermitSingle } from '@uniswap/permit2-sdk'
|
||||
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import useBlockNumber from 'lib/hooks/useBlockNumber'
|
||||
import PERMIT2_ABI from 'abis/permit2.json'
|
||||
import { Permit2 } from 'abis/types'
|
||||
import { useContract } from 'hooks/useContract'
|
||||
import { useSingleCallResult } from 'lib/hooks/multicall'
|
||||
import ms from 'ms.macro'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
@ -19,35 +15,28 @@ function toDeadline(expiration: number): number {
|
||||
return Math.floor((Date.now() + expiration) / 1000)
|
||||
}
|
||||
|
||||
export function usePermitAllowance(token?: Token, spender?: string) {
|
||||
const { account, provider } = useWeb3React()
|
||||
const allowanceProvider = useMemo(() => provider && new AllowanceProvider(provider, PERMIT2_ADDRESS), [provider])
|
||||
const [allowanceData, setAllowanceData] = useState<AllowanceData>()
|
||||
export function usePermitAllowance(token?: Token, owner?: string, spender?: string) {
|
||||
const contract = useContract<Permit2>(PERMIT2_ADDRESS, PERMIT2_ABI)
|
||||
const inputs = useMemo(() => [owner, token?.address, spender], [owner, spender, token?.address])
|
||||
|
||||
// If there is no allowanceData, recheck every block so a submitted allowance is immediately observed.
|
||||
const blockNumber = useBlockNumber()
|
||||
const shouldUpdate = allowanceData ? false : blockNumber
|
||||
// If there is no allowance yet, re-check next observed block.
|
||||
// This guarantees that the permitAllowance is synced upon submission and updated upon being synced.
|
||||
const [blocksPerFetch, setBlocksPerFetch] = useState<1>()
|
||||
const result = useSingleCallResult(contract, 'allowance', inputs, {
|
||||
blocksPerFetch,
|
||||
}).result as Awaited<ReturnType<Permit2['allowance']>> | undefined
|
||||
|
||||
useEffect(() => {
|
||||
if (!account || !token || !spender) return
|
||||
const rawAmount = result?.amount.toString() // convert to a string before using in a hook, to avoid spurious rerenders
|
||||
const allowance = useMemo(
|
||||
() => (token && rawAmount ? CurrencyAmount.fromRawAmount(token, rawAmount) : undefined),
|
||||
[token, rawAmount]
|
||||
)
|
||||
useEffect(() => setBlocksPerFetch(allowance?.equalTo(0) ? 1 : undefined), [allowance])
|
||||
|
||||
allowanceProvider
|
||||
?.getAllowanceData(token.address, account, spender)
|
||||
.then((data) => {
|
||||
if (stale) return
|
||||
setAllowanceData(data)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn(`Failed to fetch allowance data: ${e}`)
|
||||
})
|
||||
|
||||
let stale = false
|
||||
return () => {
|
||||
stale = true
|
||||
}
|
||||
}, [account, allowanceProvider, shouldUpdate, spender, token])
|
||||
|
||||
return allowanceData
|
||||
return useMemo(
|
||||
() => ({ permitAllowance: allowance, expiration: result?.expiration, nonce: result?.nonce }),
|
||||
[allowance, result?.expiration, result?.nonce]
|
||||
)
|
||||
}
|
||||
|
||||
interface Permit extends PermitSingle {
|
||||
@ -91,7 +80,7 @@ export function useUpdatePermitAllowance(
|
||||
return
|
||||
} catch (e: unknown) {
|
||||
const symbol = token?.symbol ?? 'Token'
|
||||
throw new Error(`${symbol} permit failed: ${e instanceof Error ? e.message : e}`)
|
||||
throw new Error(`${symbol} permit allowance failed: ${e instanceof Error ? e.message : e}`)
|
||||
}
|
||||
}, [account, chainId, nonce, onPermitSignature, provider, spender, token])
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { Trade } from '@uniswap/router-sdk'
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { usePermit2Enabled } from 'featureFlags/flags/permit2'
|
||||
import { PermitSignature } from 'hooks/usePermitAllowance'
|
||||
import { SwapCallbackState, useSwapCallback as useLibSwapCallBack } from 'lib/hooks/swap/useSwapCallback'
|
||||
import { ReactNode, useMemo } from 'react'
|
||||
|
||||
@ -10,7 +11,6 @@ import { TransactionType } from '../state/transactions/types'
|
||||
import { currencyId } from '../utils/currencyId'
|
||||
import useENS from './useENS'
|
||||
import { SignatureData } from './useERC20Permit'
|
||||
import { Permit } from './usePermit2'
|
||||
import useTransactionDeadline from './useTransactionDeadline'
|
||||
import { useUniversalRouterSwapCallback } from './useUniversalRouter'
|
||||
|
||||
@ -21,7 +21,7 @@ export function useSwapCallback(
|
||||
allowedSlippage: Percent, // in bips
|
||||
recipientAddressOrName: string | null, // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender
|
||||
signatureData: SignatureData | undefined | null,
|
||||
permit: Permit | undefined
|
||||
permitSignature: PermitSignature | undefined
|
||||
): { state: SwapCallbackState; callback: null | (() => Promise<string>); error: ReactNode | null } {
|
||||
const { account } = useWeb3React()
|
||||
|
||||
@ -47,7 +47,7 @@ export function useSwapCallback(
|
||||
const universalRouterSwapCallback = useUniversalRouterSwapCallback(permit2Enabled ? trade : undefined, {
|
||||
slippageTolerance: allowedSlippage,
|
||||
deadline,
|
||||
permit: permit?.signature,
|
||||
permit: permitSignature,
|
||||
})
|
||||
const swapCallback = permit2Enabled ? universalRouterSwapCallback : libCallback
|
||||
|
||||
|
@ -1,13 +1,12 @@
|
||||
import { BigNumberish } from '@ethersproject/bignumber'
|
||||
import { ContractTransaction } from '@ethersproject/contracts'
|
||||
import { CurrencyAmount, MaxUint256, Token } from '@uniswap/sdk-core'
|
||||
import { useTokenContract } from 'hooks/useContract'
|
||||
import { useSingleCallResult } from 'lib/hooks/multicall'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { ApproveTransactionInfo, TransactionType } from 'state/transactions/types'
|
||||
import { calculateGasMargin } from 'utils/calculateGasMargin'
|
||||
|
||||
import { useTokenContract } from './useContract'
|
||||
|
||||
export function useTokenAllowance(
|
||||
token?: Token,
|
||||
owner?: string,
|
||||
@ -17,23 +16,33 @@ export function useTokenAllowance(
|
||||
isSyncing: boolean
|
||||
} {
|
||||
const contract = useTokenContract(token?.address, false)
|
||||
|
||||
const inputs = useMemo(() => [owner, spender], [owner, spender])
|
||||
const { result, syncing: isSyncing } = useSingleCallResult(contract, 'allowance', inputs)
|
||||
|
||||
return useMemo(() => {
|
||||
const tokenAllowance = token && result && CurrencyAmount.fromRawAmount(token, result.toString())
|
||||
return { tokenAllowance, isSyncing }
|
||||
}, [isSyncing, result, token])
|
||||
// If there is no allowance yet, re-check next observed block.
|
||||
// This guarantees that the tokenAllowance is marked isSyncing upon approval and updated upon being synced.
|
||||
const [blocksPerFetch, setBlocksPerFetch] = useState<1>()
|
||||
const { result, syncing: isSyncing } = useSingleCallResult(contract, 'allowance', inputs, { blocksPerFetch }) as {
|
||||
result: Awaited<ReturnType<NonNullable<typeof contract>['allowance']>> | undefined
|
||||
syncing: boolean
|
||||
}
|
||||
|
||||
const rawAmount = result?.toString() // convert to a string before using in a hook, to avoid spurious rerenders
|
||||
const allowance = useMemo(
|
||||
() => (token && rawAmount ? CurrencyAmount.fromRawAmount(token, rawAmount) : undefined),
|
||||
[token, rawAmount]
|
||||
)
|
||||
useEffect(() => setBlocksPerFetch(allowance?.equalTo(0) ? 1 : undefined), [allowance])
|
||||
|
||||
return useMemo(() => ({ tokenAllowance: allowance, isSyncing }), [allowance, isSyncing])
|
||||
}
|
||||
|
||||
export function useUpdateTokenAllowance(amount: CurrencyAmount<Token> | undefined, spender: string) {
|
||||
export function useUpdateTokenAllowance(
|
||||
amount: CurrencyAmount<Token> | undefined,
|
||||
spender: string
|
||||
): () => Promise<{ response: ContractTransaction; info: ApproveTransactionInfo }> {
|
||||
const contract = useTokenContract(amount?.currency.address)
|
||||
|
||||
return useCallback(async (): Promise<{
|
||||
response: ContractTransaction
|
||||
info: ApproveTransactionInfo
|
||||
}> => {
|
||||
return useCallback(async () => {
|
||||
try {
|
||||
if (!amount) throw new Error('missing amount')
|
||||
if (!contract) throw new Error('missing contract')
|
||||
@ -58,7 +67,7 @@ export function useUpdateTokenAllowance(amount: CurrencyAmount<Token> | undefine
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
const symbol = amount?.currency.symbol ?? 'Token'
|
||||
throw new Error(`${symbol} approval failed: ${e instanceof Error ? e.message : e}`)
|
||||
throw new Error(`${symbol} token allowance failed: ${e instanceof Error ? e.message : e}`)
|
||||
}
|
||||
}, [amount, contract, spender])
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { TransactionReceipt } from '@ethersproject/abstract-provider'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import useBlockNumber from 'lib/hooks/useBlockNumber'
|
||||
import useBlockNumber, { useFastForwardBlockNumber } from 'lib/hooks/useBlockNumber'
|
||||
import ms from 'ms.macro'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { retry, RetryableError, RetryOptions } from 'utils/retry'
|
||||
@ -48,6 +48,7 @@ export default function Updater({ pendingTransactions, onCheck, onReceipt }: Upd
|
||||
const { chainId, provider } = useWeb3React()
|
||||
|
||||
const lastBlockNumber = useBlockNumber()
|
||||
const fastForwardBlockNumber = useFastForwardBlockNumber()
|
||||
|
||||
const getReceipt = useCallback(
|
||||
(hash: string) => {
|
||||
@ -78,6 +79,7 @@ export default function Updater({ pendingTransactions, onCheck, onReceipt }: Upd
|
||||
promise
|
||||
.then((receipt) => {
|
||||
if (receipt) {
|
||||
fastForwardBlockNumber(receipt.blockNumber)
|
||||
onReceipt({ chainId, hash, receipt })
|
||||
} else {
|
||||
onCheck({ chainId, hash, blockNumber: lastBlockNumber })
|
||||
@ -94,7 +96,7 @@ export default function Updater({ pendingTransactions, onCheck, onReceipt }: Upd
|
||||
return () => {
|
||||
cancels.forEach((cancel) => cancel())
|
||||
}
|
||||
}, [chainId, provider, lastBlockNumber, getReceipt, onReceipt, onCheck, pendingTransactions])
|
||||
}, [chainId, provider, lastBlockNumber, getReceipt, onReceipt, onCheck, pendingTransactions, fastForwardBlockNumber])
|
||||
|
||||
return null
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ const MISSING_PROVIDER = Symbol()
|
||||
const BlockNumberContext = createContext<
|
||||
| {
|
||||
value?: number
|
||||
fastForward(block: number): void
|
||||
}
|
||||
| typeof MISSING_PROVIDER
|
||||
>(MISSING_PROVIDER)
|
||||
@ -23,6 +24,10 @@ export default function useBlockNumber(): number | undefined {
|
||||
return useBlockNumberContext().value
|
||||
}
|
||||
|
||||
export function useFastForwardBlockNumber(): (block: number) => void {
|
||||
return useBlockNumberContext().fastForward
|
||||
}
|
||||
|
||||
export function BlockNumberProvider({ children }: { children: ReactNode }) {
|
||||
const { chainId: activeChainId, provider } = useWeb3React()
|
||||
const [{ chainId, block }, setChainBlock] = useState<{ chainId?: number; block?: number }>({ chainId: activeChainId })
|
||||
@ -68,7 +73,16 @@ export function BlockNumberProvider({ children }: { children: ReactNode }) {
|
||||
return void 0
|
||||
}, [activeChainId, provider, onBlock, setChainBlock, windowVisible])
|
||||
|
||||
const blockValue = useMemo(() => (chainId === activeChainId ? block : undefined), [activeChainId, block, chainId])
|
||||
const value = useMemo(() => ({ value: blockValue }), [blockValue])
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
value: chainId === activeChainId ? block : undefined,
|
||||
fastForward: (update: number) => {
|
||||
if (block && update > block) {
|
||||
setChainBlock({ chainId: activeChainId, block: update })
|
||||
}
|
||||
},
|
||||
}),
|
||||
[activeChainId, block, chainId]
|
||||
)
|
||||
return <BlockNumberContext.Provider value={value}>{children}</BlockNumberContext.Provider>
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import { isSupportedChain } from 'constants/chains'
|
||||
import { usePermit2Enabled } from 'featureFlags/flags/permit2'
|
||||
import usePermit, { PermitState } from 'hooks/usePermit2'
|
||||
import usePermit2Allowance, { AllowanceState } from 'hooks/usePermit2Allowance'
|
||||
import { useSwapCallback } from 'hooks/useSwapCallback'
|
||||
import useTransactionDeadline from 'hooks/useTransactionDeadline'
|
||||
import JSBI from 'jsbi'
|
||||
@ -34,8 +34,8 @@ import { Text } from 'rebass'
|
||||
import { useToggleWalletModal } from 'state/application/hooks'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
import { TradeState } from 'state/routing/types'
|
||||
import { useTransactionAdder } from 'state/transactions/hooks'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import invariant from 'tiny-invariant'
|
||||
import { currencyAmountToPreciseFloat, formatTransactionAmount } from 'utils/formatNumbers'
|
||||
|
||||
import AddressInputPanel from '../../components/AddressInputPanel'
|
||||
@ -302,36 +302,31 @@ export default function Swap({ className }: { className?: string }) {
|
||||
const maximumAmountIn = trade?.maximumAmountIn(allowedSlippage)
|
||||
return maximumAmountIn?.currency.isToken ? (maximumAmountIn as CurrencyAmount<Token>) : undefined
|
||||
}, [allowedSlippage, trade])
|
||||
const permit = usePermit(
|
||||
const allowance = usePermit2Allowance(
|
||||
permit2Enabled ? maximumAmountIn : undefined,
|
||||
permit2Enabled && chainId ? UNIVERSAL_ROUTER_ADDRESS(chainId) : undefined
|
||||
)
|
||||
const isApprovalLoading = permit.state === PermitState.APPROVAL_LOADING
|
||||
const [isPermitPending, setIsPermitPending] = useState(false)
|
||||
const [isPermitFailed, setIsPermitFailed] = useState(false)
|
||||
const addTransaction = useTransactionAdder()
|
||||
const updatePermit = useCallback(async () => {
|
||||
setIsPermitPending(true)
|
||||
const isApprovalLoading = allowance.state === AllowanceState.REQUIRED && allowance.isApprovalLoading
|
||||
const [isAllowancePending, setIsAllowancePending] = useState(false)
|
||||
const [isAllowanceFailed, setIsAllowanceFailed] = useState(false)
|
||||
const updateAllowance = useCallback(async () => {
|
||||
invariant(allowance.state === AllowanceState.REQUIRED)
|
||||
setIsAllowancePending(true)
|
||||
try {
|
||||
const approval = await permit.callback?.()
|
||||
if (approval) {
|
||||
sendAnalyticsEvent(InterfaceEventName.APPROVE_TOKEN_TXN_SUBMITTED, {
|
||||
chain_id: chainId,
|
||||
token_symbol: maximumAmountIn?.currency.symbol,
|
||||
token_address: maximumAmountIn?.currency.address,
|
||||
})
|
||||
|
||||
const { response, info } = approval
|
||||
addTransaction(response, info)
|
||||
}
|
||||
setIsPermitFailed(false)
|
||||
await allowance.approveAndPermit()
|
||||
sendAnalyticsEvent(InterfaceEventName.APPROVE_TOKEN_TXN_SUBMITTED, {
|
||||
chain_id: chainId,
|
||||
token_symbol: maximumAmountIn?.currency.symbol,
|
||||
token_address: maximumAmountIn?.currency.address,
|
||||
})
|
||||
setIsAllowanceFailed(false)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
setIsPermitFailed(true)
|
||||
setIsAllowanceFailed(true)
|
||||
} finally {
|
||||
setIsPermitPending(false)
|
||||
setIsAllowancePending(false)
|
||||
}
|
||||
}, [addTransaction, chainId, maximumAmountIn?.currency.address, maximumAmountIn?.currency.symbol, permit])
|
||||
}, [allowance, chainId, maximumAmountIn?.currency.address, maximumAmountIn?.currency.symbol])
|
||||
|
||||
// check whether the user has approved the router on the input token
|
||||
const [approvalState, approveCallback] = useApproveCallbackFromTrade(
|
||||
@ -394,7 +389,7 @@ export default function Swap({ className }: { className?: string }) {
|
||||
allowedSlippage,
|
||||
recipient,
|
||||
signatureData,
|
||||
permit
|
||||
allowance.state === AllowanceState.ALLOWED ? allowance.permitSignature : undefined
|
||||
)
|
||||
|
||||
const handleSwap = useCallback(() => {
|
||||
@ -790,22 +785,20 @@ export default function Swap({ className }: { className?: string }) {
|
||||
</ButtonError>
|
||||
</AutoColumn>
|
||||
</AutoRow>
|
||||
) : isValid &&
|
||||
(permit.state === PermitState.APPROVAL_OR_PERMIT_NEEDED ||
|
||||
permit.state === PermitState.APPROVAL_LOADING) ? (
|
||||
) : isValid && allowance.state === AllowanceState.REQUIRED ? (
|
||||
<ButtonYellow
|
||||
onClick={updatePermit}
|
||||
disabled={isPermitPending || isApprovalLoading}
|
||||
onClick={updateAllowance}
|
||||
disabled={isAllowancePending || isApprovalLoading}
|
||||
style={{ gap: 14 }}
|
||||
>
|
||||
{isPermitPending ? (
|
||||
{isAllowancePending ? (
|
||||
<>
|
||||
<Loader size="20px" stroke={theme.accentWarning} />
|
||||
<ThemedText.SubHeader color="accentWarning">
|
||||
<Trans>Approve in your wallet</Trans>
|
||||
</ThemedText.SubHeader>
|
||||
</>
|
||||
) : isPermitFailed ? (
|
||||
) : isAllowanceFailed ? (
|
||||
<>
|
||||
<AlertTriangle size={20} stroke={theme.accentWarning} />
|
||||
<ThemedText.SubHeader color="accentWarning">
|
||||
@ -860,7 +853,7 @@ export default function Swap({ className }: { className?: string }) {
|
||||
routeIsSyncing ||
|
||||
routeIsLoading ||
|
||||
priceImpactTooHigh ||
|
||||
(permit2Enabled ? permit.state === PermitState.LOADING : Boolean(swapCallbackError))
|
||||
(permit2Enabled ? allowance.state !== AllowanceState.ALLOWED : Boolean(swapCallbackError))
|
||||
}
|
||||
error={isValid && priceImpactSeverity > 2 && (permit2Enabled || !swapCallbackError)}
|
||||
>
|
||||
|
Loading…
Reference in New Issue
Block a user