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 {
|
import { AllowanceTransfer, MaxAllowanceTransferAmount, PERMIT2_ADDRESS, PermitSingle } from '@uniswap/permit2-sdk'
|
||||||
AllowanceData,
|
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||||
AllowanceProvider,
|
|
||||||
AllowanceTransfer,
|
|
||||||
MaxAllowanceTransferAmount,
|
|
||||||
PERMIT2_ADDRESS,
|
|
||||||
PermitSingle,
|
|
||||||
} from '@uniswap/permit2-sdk'
|
|
||||||
import { Token } from '@uniswap/sdk-core'
|
|
||||||
import { useWeb3React } from '@web3-react/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 ms from 'ms.macro'
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
|
|
||||||
@ -19,35 +15,28 @@ function toDeadline(expiration: number): number {
|
|||||||
return Math.floor((Date.now() + expiration) / 1000)
|
return Math.floor((Date.now() + expiration) / 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePermitAllowance(token?: Token, spender?: string) {
|
export function usePermitAllowance(token?: Token, owner?: string, spender?: string) {
|
||||||
const { account, provider } = useWeb3React()
|
const contract = useContract<Permit2>(PERMIT2_ADDRESS, PERMIT2_ABI)
|
||||||
const allowanceProvider = useMemo(() => provider && new AllowanceProvider(provider, PERMIT2_ADDRESS), [provider])
|
const inputs = useMemo(() => [owner, token?.address, spender], [owner, spender, token?.address])
|
||||||
const [allowanceData, setAllowanceData] = useState<AllowanceData>()
|
|
||||||
|
|
||||||
// If there is no allowanceData, recheck every block so a submitted allowance is immediately observed.
|
// If there is no allowance yet, re-check next observed block.
|
||||||
const blockNumber = useBlockNumber()
|
// This guarantees that the permitAllowance is synced upon submission and updated upon being synced.
|
||||||
const shouldUpdate = allowanceData ? false : blockNumber
|
const [blocksPerFetch, setBlocksPerFetch] = useState<1>()
|
||||||
|
const result = useSingleCallResult(contract, 'allowance', inputs, {
|
||||||
|
blocksPerFetch,
|
||||||
|
}).result as Awaited<ReturnType<Permit2['allowance']>> | undefined
|
||||||
|
|
||||||
useEffect(() => {
|
const rawAmount = result?.amount.toString() // convert to a string before using in a hook, to avoid spurious rerenders
|
||||||
if (!account || !token || !spender) return
|
const allowance = useMemo(
|
||||||
|
() => (token && rawAmount ? CurrencyAmount.fromRawAmount(token, rawAmount) : undefined),
|
||||||
|
[token, rawAmount]
|
||||||
|
)
|
||||||
|
useEffect(() => setBlocksPerFetch(allowance?.equalTo(0) ? 1 : undefined), [allowance])
|
||||||
|
|
||||||
allowanceProvider
|
return useMemo(
|
||||||
?.getAllowanceData(token.address, account, spender)
|
() => ({ permitAllowance: allowance, expiration: result?.expiration, nonce: result?.nonce }),
|
||||||
.then((data) => {
|
[allowance, result?.expiration, result?.nonce]
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Permit extends PermitSingle {
|
interface Permit extends PermitSingle {
|
||||||
@ -91,7 +80,7 @@ export function useUpdatePermitAllowance(
|
|||||||
return
|
return
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
const symbol = token?.symbol ?? 'Token'
|
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])
|
}, [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 { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||||
import { useWeb3React } from '@web3-react/core'
|
import { useWeb3React } from '@web3-react/core'
|
||||||
import { usePermit2Enabled } from 'featureFlags/flags/permit2'
|
import { usePermit2Enabled } from 'featureFlags/flags/permit2'
|
||||||
|
import { PermitSignature } from 'hooks/usePermitAllowance'
|
||||||
import { SwapCallbackState, useSwapCallback as useLibSwapCallBack } from 'lib/hooks/swap/useSwapCallback'
|
import { SwapCallbackState, useSwapCallback as useLibSwapCallBack } from 'lib/hooks/swap/useSwapCallback'
|
||||||
import { ReactNode, useMemo } from 'react'
|
import { ReactNode, useMemo } from 'react'
|
||||||
|
|
||||||
@ -10,7 +11,6 @@ import { TransactionType } from '../state/transactions/types'
|
|||||||
import { currencyId } from '../utils/currencyId'
|
import { currencyId } from '../utils/currencyId'
|
||||||
import useENS from './useENS'
|
import useENS from './useENS'
|
||||||
import { SignatureData } from './useERC20Permit'
|
import { SignatureData } from './useERC20Permit'
|
||||||
import { Permit } from './usePermit2'
|
|
||||||
import useTransactionDeadline from './useTransactionDeadline'
|
import useTransactionDeadline from './useTransactionDeadline'
|
||||||
import { useUniversalRouterSwapCallback } from './useUniversalRouter'
|
import { useUniversalRouterSwapCallback } from './useUniversalRouter'
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ export function useSwapCallback(
|
|||||||
allowedSlippage: Percent, // in bips
|
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
|
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,
|
signatureData: SignatureData | undefined | null,
|
||||||
permit: Permit | undefined
|
permitSignature: PermitSignature | undefined
|
||||||
): { state: SwapCallbackState; callback: null | (() => Promise<string>); error: ReactNode | null } {
|
): { state: SwapCallbackState; callback: null | (() => Promise<string>); error: ReactNode | null } {
|
||||||
const { account } = useWeb3React()
|
const { account } = useWeb3React()
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ export function useSwapCallback(
|
|||||||
const universalRouterSwapCallback = useUniversalRouterSwapCallback(permit2Enabled ? trade : undefined, {
|
const universalRouterSwapCallback = useUniversalRouterSwapCallback(permit2Enabled ? trade : undefined, {
|
||||||
slippageTolerance: allowedSlippage,
|
slippageTolerance: allowedSlippage,
|
||||||
deadline,
|
deadline,
|
||||||
permit: permit?.signature,
|
permit: permitSignature,
|
||||||
})
|
})
|
||||||
const swapCallback = permit2Enabled ? universalRouterSwapCallback : libCallback
|
const swapCallback = permit2Enabled ? universalRouterSwapCallback : libCallback
|
||||||
|
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import { BigNumberish } from '@ethersproject/bignumber'
|
import { BigNumberish } from '@ethersproject/bignumber'
|
||||||
import { ContractTransaction } from '@ethersproject/contracts'
|
import { ContractTransaction } from '@ethersproject/contracts'
|
||||||
import { CurrencyAmount, MaxUint256, Token } from '@uniswap/sdk-core'
|
import { CurrencyAmount, MaxUint256, Token } from '@uniswap/sdk-core'
|
||||||
|
import { useTokenContract } from 'hooks/useContract'
|
||||||
import { useSingleCallResult } from 'lib/hooks/multicall'
|
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 { ApproveTransactionInfo, TransactionType } from 'state/transactions/types'
|
||||||
import { calculateGasMargin } from 'utils/calculateGasMargin'
|
import { calculateGasMargin } from 'utils/calculateGasMargin'
|
||||||
|
|
||||||
import { useTokenContract } from './useContract'
|
|
||||||
|
|
||||||
export function useTokenAllowance(
|
export function useTokenAllowance(
|
||||||
token?: Token,
|
token?: Token,
|
||||||
owner?: string,
|
owner?: string,
|
||||||
@ -17,23 +16,33 @@ export function useTokenAllowance(
|
|||||||
isSyncing: boolean
|
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 { result, syncing: isSyncing } = useSingleCallResult(contract, 'allowance', inputs)
|
|
||||||
|
|
||||||
return useMemo(() => {
|
// If there is no allowance yet, re-check next observed block.
|
||||||
const tokenAllowance = token && result && CurrencyAmount.fromRawAmount(token, result.toString())
|
// This guarantees that the tokenAllowance is marked isSyncing upon approval and updated upon being synced.
|
||||||
return { tokenAllowance, isSyncing }
|
const [blocksPerFetch, setBlocksPerFetch] = useState<1>()
|
||||||
}, [isSyncing, result, token])
|
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)
|
const contract = useTokenContract(amount?.currency.address)
|
||||||
|
|
||||||
return useCallback(async (): Promise<{
|
return useCallback(async () => {
|
||||||
response: ContractTransaction
|
|
||||||
info: ApproveTransactionInfo
|
|
||||||
}> => {
|
|
||||||
try {
|
try {
|
||||||
if (!amount) throw new Error('missing amount')
|
if (!amount) throw new Error('missing amount')
|
||||||
if (!contract) throw new Error('missing contract')
|
if (!contract) throw new Error('missing contract')
|
||||||
@ -58,7 +67,7 @@ export function useUpdateTokenAllowance(amount: CurrencyAmount<Token> | undefine
|
|||||||
}
|
}
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
const symbol = amount?.currency.symbol ?? 'Token'
|
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])
|
}, [amount, contract, spender])
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { TransactionReceipt } from '@ethersproject/abstract-provider'
|
import { TransactionReceipt } from '@ethersproject/abstract-provider'
|
||||||
import { useWeb3React } from '@web3-react/core'
|
import { useWeb3React } from '@web3-react/core'
|
||||||
import { SupportedChainId } from 'constants/chains'
|
import { SupportedChainId } from 'constants/chains'
|
||||||
import useBlockNumber from 'lib/hooks/useBlockNumber'
|
import useBlockNumber, { useFastForwardBlockNumber } from 'lib/hooks/useBlockNumber'
|
||||||
import ms from 'ms.macro'
|
import ms from 'ms.macro'
|
||||||
import { useCallback, useEffect } from 'react'
|
import { useCallback, useEffect } from 'react'
|
||||||
import { retry, RetryableError, RetryOptions } from 'utils/retry'
|
import { retry, RetryableError, RetryOptions } from 'utils/retry'
|
||||||
@ -48,6 +48,7 @@ export default function Updater({ pendingTransactions, onCheck, onReceipt }: Upd
|
|||||||
const { chainId, provider } = useWeb3React()
|
const { chainId, provider } = useWeb3React()
|
||||||
|
|
||||||
const lastBlockNumber = useBlockNumber()
|
const lastBlockNumber = useBlockNumber()
|
||||||
|
const fastForwardBlockNumber = useFastForwardBlockNumber()
|
||||||
|
|
||||||
const getReceipt = useCallback(
|
const getReceipt = useCallback(
|
||||||
(hash: string) => {
|
(hash: string) => {
|
||||||
@ -78,6 +79,7 @@ export default function Updater({ pendingTransactions, onCheck, onReceipt }: Upd
|
|||||||
promise
|
promise
|
||||||
.then((receipt) => {
|
.then((receipt) => {
|
||||||
if (receipt) {
|
if (receipt) {
|
||||||
|
fastForwardBlockNumber(receipt.blockNumber)
|
||||||
onReceipt({ chainId, hash, receipt })
|
onReceipt({ chainId, hash, receipt })
|
||||||
} else {
|
} else {
|
||||||
onCheck({ chainId, hash, blockNumber: lastBlockNumber })
|
onCheck({ chainId, hash, blockNumber: lastBlockNumber })
|
||||||
@ -94,7 +96,7 @@ export default function Updater({ pendingTransactions, onCheck, onReceipt }: Upd
|
|||||||
return () => {
|
return () => {
|
||||||
cancels.forEach((cancel) => cancel())
|
cancels.forEach((cancel) => cancel())
|
||||||
}
|
}
|
||||||
}, [chainId, provider, lastBlockNumber, getReceipt, onReceipt, onCheck, pendingTransactions])
|
}, [chainId, provider, lastBlockNumber, getReceipt, onReceipt, onCheck, pendingTransactions, fastForwardBlockNumber])
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ const MISSING_PROVIDER = Symbol()
|
|||||||
const BlockNumberContext = createContext<
|
const BlockNumberContext = createContext<
|
||||||
| {
|
| {
|
||||||
value?: number
|
value?: number
|
||||||
|
fastForward(block: number): void
|
||||||
}
|
}
|
||||||
| typeof MISSING_PROVIDER
|
| typeof MISSING_PROVIDER
|
||||||
>(MISSING_PROVIDER)
|
>(MISSING_PROVIDER)
|
||||||
@ -23,6 +24,10 @@ export default function useBlockNumber(): number | undefined {
|
|||||||
return useBlockNumberContext().value
|
return useBlockNumberContext().value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useFastForwardBlockNumber(): (block: number) => void {
|
||||||
|
return useBlockNumberContext().fastForward
|
||||||
|
}
|
||||||
|
|
||||||
export function BlockNumberProvider({ children }: { children: ReactNode }) {
|
export function BlockNumberProvider({ children }: { children: ReactNode }) {
|
||||||
const { chainId: activeChainId, provider } = useWeb3React()
|
const { chainId: activeChainId, provider } = useWeb3React()
|
||||||
const [{ chainId, block }, setChainBlock] = useState<{ chainId?: number; block?: number }>({ chainId: activeChainId })
|
const [{ chainId, block }, setChainBlock] = useState<{ chainId?: number; block?: number }>({ chainId: activeChainId })
|
||||||
@ -68,7 +73,16 @@ export function BlockNumberProvider({ children }: { children: ReactNode }) {
|
|||||||
return void 0
|
return void 0
|
||||||
}, [activeChainId, provider, onBlock, setChainBlock, windowVisible])
|
}, [activeChainId, provider, onBlock, setChainBlock, windowVisible])
|
||||||
|
|
||||||
const blockValue = useMemo(() => (chainId === activeChainId ? block : undefined), [activeChainId, block, chainId])
|
const value = useMemo(
|
||||||
const value = useMemo(() => ({ value: blockValue }), [blockValue])
|
() => ({
|
||||||
|
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>
|
return <BlockNumberContext.Provider value={value}>{children}</BlockNumberContext.Provider>
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
|
|||||||
import { MouseoverTooltip } from 'components/Tooltip'
|
import { MouseoverTooltip } from 'components/Tooltip'
|
||||||
import { isSupportedChain } from 'constants/chains'
|
import { isSupportedChain } from 'constants/chains'
|
||||||
import { usePermit2Enabled } from 'featureFlags/flags/permit2'
|
import { usePermit2Enabled } from 'featureFlags/flags/permit2'
|
||||||
import usePermit, { PermitState } from 'hooks/usePermit2'
|
import usePermit2Allowance, { AllowanceState } from 'hooks/usePermit2Allowance'
|
||||||
import { useSwapCallback } from 'hooks/useSwapCallback'
|
import { useSwapCallback } from 'hooks/useSwapCallback'
|
||||||
import useTransactionDeadline from 'hooks/useTransactionDeadline'
|
import useTransactionDeadline from 'hooks/useTransactionDeadline'
|
||||||
import JSBI from 'jsbi'
|
import JSBI from 'jsbi'
|
||||||
@ -34,8 +34,8 @@ 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 { useTransactionAdder } from 'state/transactions/hooks'
|
|
||||||
import styled, { useTheme } from 'styled-components/macro'
|
import styled, { useTheme } from 'styled-components/macro'
|
||||||
|
import invariant from 'tiny-invariant'
|
||||||
import { currencyAmountToPreciseFloat, formatTransactionAmount } from 'utils/formatNumbers'
|
import { currencyAmountToPreciseFloat, formatTransactionAmount } from 'utils/formatNumbers'
|
||||||
|
|
||||||
import AddressInputPanel from '../../components/AddressInputPanel'
|
import AddressInputPanel from '../../components/AddressInputPanel'
|
||||||
@ -302,36 +302,31 @@ export default function Swap({ className }: { className?: string }) {
|
|||||||
const maximumAmountIn = trade?.maximumAmountIn(allowedSlippage)
|
const maximumAmountIn = trade?.maximumAmountIn(allowedSlippage)
|
||||||
return maximumAmountIn?.currency.isToken ? (maximumAmountIn as CurrencyAmount<Token>) : undefined
|
return maximumAmountIn?.currency.isToken ? (maximumAmountIn as CurrencyAmount<Token>) : undefined
|
||||||
}, [allowedSlippage, trade])
|
}, [allowedSlippage, trade])
|
||||||
const permit = usePermit(
|
const allowance = usePermit2Allowance(
|
||||||
permit2Enabled ? maximumAmountIn : undefined,
|
permit2Enabled ? maximumAmountIn : undefined,
|
||||||
permit2Enabled && chainId ? UNIVERSAL_ROUTER_ADDRESS(chainId) : undefined
|
permit2Enabled && chainId ? UNIVERSAL_ROUTER_ADDRESS(chainId) : undefined
|
||||||
)
|
)
|
||||||
const isApprovalLoading = permit.state === PermitState.APPROVAL_LOADING
|
const isApprovalLoading = allowance.state === AllowanceState.REQUIRED && allowance.isApprovalLoading
|
||||||
const [isPermitPending, setIsPermitPending] = useState(false)
|
const [isAllowancePending, setIsAllowancePending] = useState(false)
|
||||||
const [isPermitFailed, setIsPermitFailed] = useState(false)
|
const [isAllowanceFailed, setIsAllowanceFailed] = useState(false)
|
||||||
const addTransaction = useTransactionAdder()
|
const updateAllowance = useCallback(async () => {
|
||||||
const updatePermit = useCallback(async () => {
|
invariant(allowance.state === AllowanceState.REQUIRED)
|
||||||
setIsPermitPending(true)
|
setIsAllowancePending(true)
|
||||||
try {
|
try {
|
||||||
const approval = await permit.callback?.()
|
await allowance.approveAndPermit()
|
||||||
if (approval) {
|
|
||||||
sendAnalyticsEvent(InterfaceEventName.APPROVE_TOKEN_TXN_SUBMITTED, {
|
sendAnalyticsEvent(InterfaceEventName.APPROVE_TOKEN_TXN_SUBMITTED, {
|
||||||
chain_id: chainId,
|
chain_id: chainId,
|
||||||
token_symbol: maximumAmountIn?.currency.symbol,
|
token_symbol: maximumAmountIn?.currency.symbol,
|
||||||
token_address: maximumAmountIn?.currency.address,
|
token_address: maximumAmountIn?.currency.address,
|
||||||
})
|
})
|
||||||
|
setIsAllowanceFailed(false)
|
||||||
const { response, info } = approval
|
|
||||||
addTransaction(response, info)
|
|
||||||
}
|
|
||||||
setIsPermitFailed(false)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
setIsPermitFailed(true)
|
setIsAllowanceFailed(true)
|
||||||
} finally {
|
} 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
|
// check whether the user has approved the router on the input token
|
||||||
const [approvalState, approveCallback] = useApproveCallbackFromTrade(
|
const [approvalState, approveCallback] = useApproveCallbackFromTrade(
|
||||||
@ -394,7 +389,7 @@ export default function Swap({ className }: { className?: string }) {
|
|||||||
allowedSlippage,
|
allowedSlippage,
|
||||||
recipient,
|
recipient,
|
||||||
signatureData,
|
signatureData,
|
||||||
permit
|
allowance.state === AllowanceState.ALLOWED ? allowance.permitSignature : undefined
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleSwap = useCallback(() => {
|
const handleSwap = useCallback(() => {
|
||||||
@ -790,22 +785,20 @@ export default function Swap({ className }: { className?: string }) {
|
|||||||
</ButtonError>
|
</ButtonError>
|
||||||
</AutoColumn>
|
</AutoColumn>
|
||||||
</AutoRow>
|
</AutoRow>
|
||||||
) : isValid &&
|
) : isValid && allowance.state === AllowanceState.REQUIRED ? (
|
||||||
(permit.state === PermitState.APPROVAL_OR_PERMIT_NEEDED ||
|
|
||||||
permit.state === PermitState.APPROVAL_LOADING) ? (
|
|
||||||
<ButtonYellow
|
<ButtonYellow
|
||||||
onClick={updatePermit}
|
onClick={updateAllowance}
|
||||||
disabled={isPermitPending || isApprovalLoading}
|
disabled={isAllowancePending || isApprovalLoading}
|
||||||
style={{ gap: 14 }}
|
style={{ gap: 14 }}
|
||||||
>
|
>
|
||||||
{isPermitPending ? (
|
{isAllowancePending ? (
|
||||||
<>
|
<>
|
||||||
<Loader size="20px" stroke={theme.accentWarning} />
|
<Loader size="20px" stroke={theme.accentWarning} />
|
||||||
<ThemedText.SubHeader color="accentWarning">
|
<ThemedText.SubHeader color="accentWarning">
|
||||||
<Trans>Approve in your wallet</Trans>
|
<Trans>Approve in your wallet</Trans>
|
||||||
</ThemedText.SubHeader>
|
</ThemedText.SubHeader>
|
||||||
</>
|
</>
|
||||||
) : isPermitFailed ? (
|
) : isAllowanceFailed ? (
|
||||||
<>
|
<>
|
||||||
<AlertTriangle size={20} stroke={theme.accentWarning} />
|
<AlertTriangle size={20} stroke={theme.accentWarning} />
|
||||||
<ThemedText.SubHeader color="accentWarning">
|
<ThemedText.SubHeader color="accentWarning">
|
||||||
@ -860,7 +853,7 @@ export default function Swap({ className }: { className?: string }) {
|
|||||||
routeIsSyncing ||
|
routeIsSyncing ||
|
||||||
routeIsLoading ||
|
routeIsLoading ||
|
||||||
priceImpactTooHigh ||
|
priceImpactTooHigh ||
|
||||||
(permit2Enabled ? permit.state === PermitState.LOADING : Boolean(swapCallbackError))
|
(permit2Enabled ? allowance.state !== AllowanceState.ALLOWED : Boolean(swapCallbackError))
|
||||||
}
|
}
|
||||||
error={isValid && priceImpactSeverity > 2 && (permit2Enabled || !swapCallbackError)}
|
error={isValid && priceImpactSeverity > 2 && (permit2Enabled || !swapCallbackError)}
|
||||||
>
|
>
|
||||||
|
Loading…
Reference in New Issue
Block a user