From ef3407f2999e109739747566c728b607b8626e77 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Thu, 12 Jan 2023 15:16:07 -0800 Subject: [PATCH] fix: update permit2 to match widget implementation (#5821) * refactor: usePermit2->usePermit2Allowance * fix: update permit2 logic to match widgets * fix: lint issues --- src/abis/permit2.json | 41 ++++++++ src/hooks/usePermit2.ts | 140 ------------------------- src/hooks/usePermit2Allowance.ts | 123 ++++++++++++++++++++++ src/hooks/usePermitAllowance.ts | 63 +++++------ src/hooks/useSwapCallback.tsx | 6 +- src/hooks/useTokenAllowance.ts | 39 ++++--- src/lib/hooks/transactions/updater.tsx | 6 +- src/lib/hooks/useBlockNumber.tsx | 18 +++- src/pages/Swap/index.tsx | 59 +++++------ 9 files changed, 263 insertions(+), 232 deletions(-) create mode 100644 src/abis/permit2.json delete mode 100644 src/hooks/usePermit2.ts create mode 100644 src/hooks/usePermit2Allowance.ts diff --git a/src/abis/permit2.json b/src/abis/permit2.json new file mode 100644 index 0000000000..aefd71a8b5 --- /dev/null +++ b/src/abis/permit2.json @@ -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" + } +] diff --git a/src/hooks/usePermit2.ts b/src/hooks/usePermit2.ts deleted file mode 100644 index 442776562b..0000000000 --- a/src/hooks/usePermit2.ts +++ /dev/null @@ -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, 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() - 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, - ]) -} diff --git a/src/hooks/usePermit2Allowance.ts b/src/hooks/usePermit2Allowance.ts new file mode 100644 index 0000000000..367fd62273 --- /dev/null +++ b/src/hooks/usePermit2Allowance.ts @@ -0,0 +1,123 @@ +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 +} + +type Allowance = + | { state: AllowanceState.LOADING } + | { + state: AllowanceState.ALLOWED + permitSignature?: PermitSignature + } + | AllowanceRequired + +export default function usePermit2Allowance(amount?: CurrencyAmount, 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. + const [now, setNow] = useState(Date.now()) + // Calculate now such that the signature will still be valid for the submitting block. + useInterval(() => setNow((Date.now() + AVERAGE_L1_BLOCK_TIME) / 1000), AVERAGE_L1_BLOCK_TIME, true) + + const [signature, setSignature] = useState() + 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, + ]) +} diff --git a/src/hooks/usePermitAllowance.ts b/src/hooks/usePermitAllowance.ts index 0cd8028a50..18c338ee09 100644 --- a/src/hooks/usePermitAllowance.ts +++ b/src/hooks/usePermitAllowance.ts @@ -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() +export function usePermitAllowance(token?: Token, owner?: string, spender?: string) { + const contract = useContract(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> | 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]) } diff --git a/src/hooks/useSwapCallback.tsx b/src/hooks/useSwapCallback.tsx index 27b5c56452..b5bf0c11b2 100644 --- a/src/hooks/useSwapCallback.tsx +++ b/src/hooks/useSwapCallback.tsx @@ -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); 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 diff --git a/src/hooks/useTokenAllowance.ts b/src/hooks/useTokenAllowance.ts index 4a68c2d625..a9847d2a41 100644 --- a/src/hooks/useTokenAllowance.ts +++ b/src/hooks/useTokenAllowance.ts @@ -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['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 | undefined, spender: string) { +export function useUpdateTokenAllowance( + amount: CurrencyAmount | 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 | 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]) } diff --git a/src/lib/hooks/transactions/updater.tsx b/src/lib/hooks/transactions/updater.tsx index 7e1adfe47a..8a85292680 100644 --- a/src/lib/hooks/transactions/updater.tsx +++ b/src/lib/hooks/transactions/updater.tsx @@ -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 } diff --git a/src/lib/hooks/useBlockNumber.tsx b/src/lib/hooks/useBlockNumber.tsx index fda980a559..aedf4336ba 100644 --- a/src/lib/hooks/useBlockNumber.tsx +++ b/src/lib/hooks/useBlockNumber.tsx @@ -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 {children} } diff --git a/src/pages/Swap/index.tsx b/src/pages/Swap/index.tsx index 5d9ec46e44..31541dac60 100644 --- a/src/pages/Swap/index.tsx +++ b/src/pages/Swap/index.tsx @@ -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) : 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 }) { - ) : isValid && - (permit.state === PermitState.APPROVAL_OR_PERMIT_NEEDED || - permit.state === PermitState.APPROVAL_LOADING) ? ( + ) : isValid && allowance.state === AllowanceState.REQUIRED ? ( - {isPermitPending ? ( + {isAllowancePending ? ( <> Approve in your wallet - ) : isPermitFailed ? ( + ) : isAllowanceFailed ? ( <> @@ -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)} >