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:
Zach Pomerantz 2023-01-13 11:28:54 -08:00 committed by GitHub
parent 14b02eda0f
commit 9719af66e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 266 additions and 232 deletions

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,
])
}

@ -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)}
> >