diff --git a/src/abis/permit2.json b/src/abis/permit2.json new file mode 100644 index 000000000..aefd71a8b --- /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/components/Swap/SwapActionButton/Permit2Button.tsx b/src/components/Swap/SwapActionButton/AllowanceButton.tsx similarity index 85% rename from src/components/Swap/SwapActionButton/Permit2Button.tsx rename to src/components/Swap/SwapActionButton/AllowanceButton.tsx index e8057e682..b7d8c6d24 100644 --- a/src/components/Swap/SwapActionButton/Permit2Button.tsx +++ b/src/components/Swap/SwapActionButton/AllowanceButton.tsx @@ -3,24 +3,23 @@ import { PERMIT2_ADDRESS } from '@uniswap/permit2-sdk' import ActionButton from 'components/ActionButton' import EtherscanLink from 'components/EtherscanLink' import { usePendingApproval } from 'hooks/transactions' -import { Permit, PermitState } from 'hooks/usePermit2' +import { AllowanceRequired } from 'hooks/usePermit2Allowance' import { Spinner } from 'icons' import { useCallback, useEffect, useMemo, useState } from 'react' import { ApprovalTransactionInfo } from 'state/transactions' import { Colors } from 'theme' import { ExplorerDataType } from 'utils/getExplorerLink' -interface PermitButtonProps extends Permit { +interface AllowanceButtonProps extends AllowanceRequired { color: keyof Colors onSubmit: (submit: () => Promise) => Promise } /** - * An approving PermitButton. - * Should only be rendered if a valid trade exists that is not yet permitted. + * An approving AllowanceButton. + * Should only be rendered if a valid trade exists that is not yet allowed. */ -export default function PermitButton({ token, state, callback, color, onSubmit }: PermitButtonProps) { - const isApprovalLoading = state === PermitState.APPROVAL_LOADING +export default function AllowanceButton({ token, isApprovalLoading, callback, color, onSubmit }: AllowanceButtonProps) { const [isPending, setIsPending] = useState(false) const [isFailed, setIsFailed] = useState(false) const pendingApproval = usePendingApproval(token, PERMIT2_ADDRESS) @@ -68,11 +67,11 @@ export default function PermitButton({ token, state, callback, color, onSubmit } } else { return { tooltipContent: t`Permission is required for Uniswap to swap each token. This will expire after one month for your security.`, - message: t`Approve use of ${token ?? 'token'}`, + message: t`Approve use of ${token?.symbol ?? 'token'}`, onClick, } } - }, [isApprovalLoading, isFailed, isPending, onClick, pendingApproval, token]) + }, [isApprovalLoading, isFailed, isPending, onClick, pendingApproval, token?.symbol]) return ( diff --git a/src/components/Swap/SwapActionButton/SwapButton.tsx b/src/components/Swap/SwapActionButton/SwapButton.tsx index d734c05e8..df18ff14d 100644 --- a/src/components/Swap/SwapActionButton/SwapButton.tsx +++ b/src/components/Swap/SwapActionButton/SwapButton.tsx @@ -5,8 +5,8 @@ import { SwapApprovalState } from 'hooks/swap/useSwapApproval' import { useSwapCallback } from 'hooks/swap/useSwapCallback' import { useConditionalHandler } from 'hooks/useConditionalHandler' import { useSetOldestValidBlock } from 'hooks/useIsValidBlock' -import { PermitState } from 'hooks/usePermit2' -import { usePermit2 } from 'hooks/useSyncFlags' +import { AllowanceState } from 'hooks/usePermit2Allowance' +import { usePermit2 as usePermit2Enabled } from 'hooks/useSyncFlags' import useTransactionDeadline from 'hooks/useTransactionDeadline' import { useUniversalRouterSwapCallback } from 'hooks/useUniversalRouter' import { useAtomValue } from 'jotai/utils' @@ -19,8 +19,8 @@ import invariant from 'tiny-invariant' import ActionButton from '../../ActionButton' import Dialog from '../../Dialog' import { SummaryDialog } from '../Summary' +import AllowanceButton from './AllowanceButton' import ApproveButton from './ApproveButton' -import PermitButton from './Permit2Button' /** * A swapping ActionButton. @@ -41,14 +41,14 @@ export default function SwapButton({ [Field.OUTPUT]: { usdc: outputUSDC }, trade: { trade, gasUseEstimateUSD }, approval, - permit, + allowance, slippage, impact, } = useSwapInfo() const deadline = useTransactionDeadline() const feeOptions = useAtomValue(feeOptionsAtom) - const permit2Enabled = usePermit2() + const permit2Enabled = usePermit2Enabled() const { callback: swapRouterCallback } = useSwapCallback({ trade: permit2Enabled ? undefined : trade, allowedSlippage: slippage.allowed, @@ -60,7 +60,7 @@ export default function SwapButton({ const universalRouterSwapCallback = useUniversalRouterSwapCallback(permit2Enabled ? trade : undefined, { slippageTolerance: slippage.allowed, deadline, - permit: permit.signature, + permit: allowance.state === AllowanceState.ALLOWED ? allowance.permitSignature : undefined, feeOptions, }) const swapCallback = permit2Enabled ? universalRouterSwapCallback : swapRouterCallback @@ -107,11 +107,8 @@ export default function SwapButton({ }, [onReviewSwapClick]) if (permit2Enabled) { - if ( - !disabled && - (permit.state === PermitState.APPROVAL_OR_PERMIT_NEEDED || permit.state === PermitState.APPROVAL_LOADING) - ) { - return + if (!disabled && allowance.state === AllowanceState.REQUIRED) { + return } } else { if (!disabled && approval.state !== SwapApprovalState.APPROVED) { @@ -124,7 +121,7 @@ export default function SwapButton({ Review swap diff --git a/src/hooks/swap/useSwapInfo.tsx b/src/hooks/swap/useSwapInfo.tsx index e58bf4719..933d0bc8b 100644 --- a/src/hooks/swap/useSwapInfo.tsx +++ b/src/hooks/swap/useSwapInfo.tsx @@ -4,10 +4,10 @@ import { useWeb3React } from '@web3-react/core' import { RouterPreference, useRouterTrade } from 'hooks/routing/useRouterTrade' import { useCurrencyBalances } from 'hooks/useCurrencyBalance' import useOnSupportedNetwork from 'hooks/useOnSupportedNetwork' -import usePermit, { Permit, PermitState } from 'hooks/usePermit2' +import usePermit2Allowance, { Allowance, AllowanceState } from 'hooks/usePermit2Allowance' import { PriceImpact, usePriceImpact } from 'hooks/usePriceImpact' import useSlippage, { DEFAULT_SLIPPAGE, Slippage } from 'hooks/useSlippage' -import { usePermit2 } from 'hooks/useSyncFlags' +import { usePermit2 as usePermit2Enabled } from 'hooks/useSyncFlags' import useUSDCPrice, { useUSDCValue } from 'hooks/useUSDCPrice' import { useAtomValue } from 'jotai/utils' import { createContext, PropsWithChildren, useContext, useEffect, useMemo, useRef } from 'react' @@ -44,7 +44,7 @@ interface SwapInfo { gasUseEstimateUSD?: CurrencyAmount } approval: SwapApproval - permit: Permit + allowance: Allowance slippage: Slippage impact?: PriceImpact } @@ -101,13 +101,13 @@ function useComputeSwapInfo(routerUrl?: string): SwapInfo { const slippage = useSlippage(trade) const impact = usePriceImpact(trade.trade) - const permit2Enabled = usePermit2() + const permit2Enabled = usePermit2Enabled() const maximumAmountIn = useMemo(() => { const maximumAmountIn = trade.trade?.maximumAmountIn(slippage.allowed) return maximumAmountIn?.currency.isToken ? (maximumAmountIn as CurrencyAmount) : undefined }, [slippage.allowed, trade.trade]) const approval = useSwapApproval(permit2Enabled ? undefined : maximumAmountIn) - const permit = usePermit( + const allowance = usePermit2Allowance( permit2Enabled ? maximumAmountIn : undefined, permit2Enabled && chainId ? UNIVERSAL_ROUTER_ADDRESS(chainId) : undefined ) @@ -129,11 +129,12 @@ function useComputeSwapInfo(routerUrl?: string): SwapInfo { error, trade, approval, - permit, + allowance, slippage, impact, } }, [ + allowance, amountIn, amountOut, approval, @@ -143,7 +144,6 @@ function useComputeSwapInfo(routerUrl?: string): SwapInfo { currencyOut, error, impact, - permit, slippage, trade, usdcIn, @@ -157,7 +157,7 @@ const DEFAULT_SWAP_INFO: SwapInfo = { error: ChainError.UNCONNECTED_CHAIN, trade: { state: TradeState.INVALID, trade: undefined }, approval: { state: SwapApprovalState.APPROVED }, - permit: { state: PermitState.INVALID }, + allowance: { state: AllowanceState.LOADING }, slippage: DEFAULT_SLIPPAGE, } diff --git a/src/hooks/transactions/updater.tsx b/src/hooks/transactions/updater.tsx index b6865649e..d3023af1e 100644 --- a/src/hooks/transactions/updater.tsx +++ b/src/hooks/transactions/updater.tsx @@ -79,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 }) diff --git a/src/hooks/useBlockNumber.tsx b/src/hooks/useBlockNumber.tsx index c71dd5eb4..d633e6af4 100644 --- a/src/hooks/useBlockNumber.tsx +++ b/src/hooks/useBlockNumber.tsx @@ -76,7 +76,11 @@ export function BlockNumberProvider({ children }: { children: ReactNode }) { const value = useMemo( () => ({ value: chainId === activeChainId ? block : undefined, - fastForward: (block: number) => setChainBlock({ chainId: activeChainId, block }), + fastForward: (update: number) => { + if (block && update > block) { + setChainBlock({ chainId: activeChainId, block: update }) + } + }, }), [activeChainId, block, chainId] ) diff --git a/src/hooks/usePermit2.ts b/src/hooks/usePermit2.ts deleted file mode 100644 index 140697489..000000000 --- a/src/hooks/usePermit2.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { PERMIT2_ADDRESS } from '@uniswap/permit2-sdk' -import { CurrencyAmount, Token } from '@uniswap/sdk-core' -import { useWeb3React } from '@web3-react/core' -import { STANDARD_L1_BLOCK_TIME } from 'constants/chainInfo' -import { useCallback, useEffect, useMemo, useState } from 'react' - -import { ApprovalTransactionInfo } from '..' -import { usePendingApproval } from './transactions' -import useInterval from './useInterval' -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 { - token?: Token - state: PermitState - signature?: PermitSignature - callback?: () => Promise -} - -export default function usePermit(amount?: CurrencyAmount, spender?: string): Permit { - const { account } = useWeb3React() - const token = amount?.currency - const { tokenAllowance, isSyncing: isApprovalSyncing } = useTokenAllowance(token, account, PERMIT2_ADDRESS) - const updateTokenAllowance = useUpdateTokenAllowance(amount, PERMIT2_ADDRESS) - const isAllowed = useMemo( - () => amount && (tokenAllowance?.greaterThan(amount) || tokenAllowance?.equalTo(amount)), - [amount, tokenAllowance] - ) - - const permitAllowance = usePermitAllowance(token, 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(token, spender, permitAllowance?.nonce, setSignature) - const isSigned = useMemo( - () => amount && signature?.details.token === token?.address && signature?.spender === spender, - [amount, signature?.details.token, signature?.spender, spender, token?.address] - ) - - // 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() - STANDARD_L1_BLOCK_TIME) / 1000 - if (signature && signature.sigDeadline < now) { - setSignature(undefined) - } - if (permitAllowance && permitAllowance.expiration < now) { - setPermitAllowanceAmount(undefined) - } - }, - STANDARD_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 = Boolean(usePendingApproval(token, 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: ApprovalTransactionInfo | undefined - if (!isAllowed && !hasPendingApproval) { - info = await updateTokenAllowance() - } - if (!isPermitted && !isSigned) { - await updatePermitAllowance() - } - return info - }, [hasPendingApproval, isAllowed, isPermitted, isSigned, updatePermitAllowance, updateTokenAllowance]) - - return useMemo(() => { - if (!token) { - return { state: PermitState.INVALID } - } else if (!tokenAllowance || !permitAllowance) { - return { token, state: PermitState.LOADING } - } else if (!(isPermitted || isSigned)) { - return { token, state: PermitState.APPROVAL_OR_PERMIT_NEEDED, callback } - } else if (!isAllowed) { - return { - token, - state: isApprovalLoading ? PermitState.APPROVAL_LOADING : PermitState.APPROVAL_OR_PERMIT_NEEDED, - callback, - } - } else { - return { token, state: PermitState.APPROVED_AND_PERMITTED, signature: isPermitted ? undefined : signature } - } - }, [callback, isAllowed, isApprovalLoading, isPermitted, isSigned, permitAllowance, signature, token, tokenAllowance]) -} diff --git a/src/hooks/usePermit2Allowance.ts b/src/hooks/usePermit2Allowance.ts new file mode 100644 index 000000000..ee1c2acb2 --- /dev/null +++ b/src/hooks/usePermit2Allowance.ts @@ -0,0 +1,122 @@ +import { PERMIT2_ADDRESS } from '@uniswap/permit2-sdk' +import { CurrencyAmount, Token } from '@uniswap/sdk-core' +import { useWeb3React } from '@web3-react/core' +import { STANDARD_L1_BLOCK_TIME } from 'constants/chainInfo' +import { usePendingApproval } from 'hooks/transactions' +import useInterval from 'hooks/useInterval' +import { PermitSignature, usePermitAllowance, useUpdatePermitAllowance } from 'hooks/usePermitAllowance' +import { useTokenAllowance, useUpdateTokenAllowance } from 'hooks/useTokenAllowance' +import { useCallback, useEffect, useMemo, useState } from 'react' +import { ApprovalTransactionInfo } from 'state/transactions' + +enum ApprovalState { + PENDING, + SYNCING, + SYNCED, +} + +export enum AllowanceState { + LOADING, + REQUIRED, + ALLOWED, +} + +export interface AllowanceRequired { + state: AllowanceState.REQUIRED + token: Token + isApprovalLoading: boolean + callback: () => Promise +} + +export 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 = Boolean(usePendingApproval(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() - STANDARD_L1_BLOCK_TIME) / 1000), STANDARD_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 callback = useCallback(async () => { + let info: ApprovalTransactionInfo | undefined + if (!(isApproved || isApprovalLoading)) { + info = await updateTokenAllowance() + } + if (!(isPermitted || isSigned)) { + await updatePermitAllowance() + } + return info + }, [isApprovalLoading, isApproved, isPermitted, isSigned, updatePermitAllowance, updateTokenAllowance]) + + return useMemo(() => { + if (token) { + if (!tokenAllowance || !permitAllowance) { + return { state: AllowanceState.LOADING } + } else if (!(isPermitted || isSigned)) { + return { token, state: AllowanceState.REQUIRED, isApprovalLoading: false, callback } + } else if (!isApproved) { + return { token, state: AllowanceState.REQUIRED, isApprovalLoading, callback } + } + } + return { token, state: AllowanceState.ALLOWED, permitSignature: !isPermitted && isSigned ? signature : undefined } + }, [ + callback, + isApprovalLoading, + isApproved, + isPermitted, + isSigned, + permitAllowance, + signature, + token, + tokenAllowance, + ]) +} diff --git a/src/hooks/usePermitAllowance.ts b/src/hooks/usePermitAllowance.ts index b86f4615d..84fe50ea3 100644 --- a/src/hooks/usePermitAllowance.ts +++ b/src/hooks/usePermitAllowance.ts @@ -1,18 +1,13 @@ -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 PERMIT2_ABI from 'abis/permit2.json' +import { Permit2 } from 'abis/types' +import { useSingleCallResult } from 'hooks/multicall' +import { useContract } from 'hooks/useContract' import ms from 'ms.macro' import { useCallback, useEffect, useMemo, useState } from 'react' -import useBlockNumber from './useBlockNumber' - const PERMIT_EXPIRATION = ms`30d` const PERMIT_SIG_EXPIRATION = ms`30m` @@ -20,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() - - // If there is no allowanceData, recheck every block so a submitted allowance is immediately observed. - const blockNumber = useBlockNumber() - const shouldUpdate = allowanceData ? false : blockNumber +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]) - useEffect(() => { - if (!account || !token || !spender) return + // 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 - 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]) + 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]) - return allowanceData + return useMemo( + () => ({ permitAllowance: allowance, expiration: result?.expiration, nonce: result?.nonce }), + [allowance, result?.expiration, result?.nonce] + ) } interface Permit extends PermitSingle { diff --git a/src/hooks/useTokenAllowance.ts b/src/hooks/useTokenAllowance.ts index b0ba10de6..f6d8944bf 100644 --- a/src/hooks/useTokenAllowance.ts +++ b/src/hooks/useTokenAllowance.ts @@ -1,12 +1,12 @@ import { BigNumberish } from '@ethersproject/bignumber' import { CurrencyAmount, MaxUint256, Token } from '@uniswap/sdk-core' +import { Erc20 } from 'abis/types' import { useSingleCallResult } from 'hooks/multicall' -import { useCallback, useMemo } from 'react' +import { useTokenContract } from 'hooks/useContract' +import { useCallback, useEffect, useMemo, useState } from 'react' import { ApprovalTransactionInfo, TransactionType } from 'state/transactions' import { calculateGasMargin } from 'utils/calculateGasMargin' -import { useTokenContract } from './useContract' - export function useTokenAllowance( token?: Token, owner?: string, @@ -16,14 +16,24 @@ 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> | 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) {