From c7633d910bb37bff5a92d515b625410510e4b154 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Tue, 25 Jan 2022 15:55:27 -0800 Subject: [PATCH] refactor: track txs (#3185) * feat: track approval txs * refactor: update transactions * chore: add ms to deps * test: rm stale test * fix: comment usage of trade for optimized trade --- package.json | 2 +- src/lib/components/Swap/Status.fixture.tsx | 64 +------ .../components/Swap/Status/StatusDialog.tsx | 52 +++--- src/lib/components/Swap/SwapButton.tsx | 27 ++- src/lib/components/Widget.tsx | 2 + src/lib/hooks/transactions/index.tsx | 92 ++++++++++ .../hooks}/transactions/updater.test.ts | 0 src/lib/hooks/transactions/updater.tsx | 101 +++++++++++ src/lib/state/swap.ts | 13 +- src/lib/state/transactions.ts | 53 ++++++ src/state/transactions/updater.tsx | 160 +++++------------- 11 files changed, 335 insertions(+), 231 deletions(-) create mode 100644 src/lib/hooks/transactions/index.tsx rename src/{state => lib/hooks}/transactions/updater.test.ts (100%) create mode 100644 src/lib/hooks/transactions/updater.tsx create mode 100644 src/lib/state/transactions.ts diff --git a/package.json b/package.json index 4ed4d121e7..c0b5442b0b 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,6 @@ "graphql-request": "^3.4.0", "inter-ui": "^3.13.1", "jest-styled-components": "^7.0.5", - "ms.macro": "^2.0.0", "polyfill-object.fromentries": "^1.0.1", "prettier": "^2.2.1", "qs": "^6.9.4", @@ -187,6 +186,7 @@ "jotai": "^1.3.7", "jsbi": "^3.1.4", "make-plural": "^7.0.0", + "ms.macro": "^2.0.0", "multicodec": "^3.0.1", "multihashes": "^4.0.2", "node-vibrant": "^3.2.1-alpha.1", diff --git a/src/lib/components/Swap/Status.fixture.tsx b/src/lib/components/Swap/Status.fixture.tsx index ece479610c..e1b93a9d7d 100644 --- a/src/lib/components/Swap/Status.fixture.tsx +++ b/src/lib/components/Swap/Status.fixture.tsx @@ -1,68 +1,8 @@ -import { tokens } from '@uniswap/default-token-list' -import { CurrencyAmount } from '@uniswap/sdk-core' -import { SupportedChainId } from 'constants/chains' -import { nativeOnChain } from 'constants/tokens' -import { useUpdateAtom } from 'jotai/utils' -import JSBI from 'jsbi' -import { swapTransactionAtom } from 'lib/state/swap' -import { useEffect } from 'react' -import { useSelect } from 'react-cosmos/fixture' -import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo' -import invariant from 'tiny-invariant' - import { Modal } from '../Dialog' -import { StatusDialog } from './Status' - -const ETH = nativeOnChain(SupportedChainId.MAINNET) -const UNI = (function () { - const token = tokens.find(({ symbol }) => symbol === 'UNI') - invariant(token) - return new WrappedTokenInfo(token) -})() function Fixture() { - const setTransaction = useUpdateAtom(swapTransactionAtom) - - const [state] = useSelect('state', { - options: ['PENDING', 'ERROR', 'SUCCESS'], - }) - useEffect(() => { - setTransaction({ - input: CurrencyAmount.fromRawAmount(ETH, JSBI.BigInt(1)), - output: CurrencyAmount.fromRawAmount(UNI, JSBI.BigInt(42)), - receipt: '', - timestamp: Date.now(), - }) - }, [setTransaction]) - useEffect(() => { - switch (state) { - case 'PENDING': - setTransaction({ - input: CurrencyAmount.fromRawAmount(ETH, JSBI.BigInt(1)), - output: CurrencyAmount.fromRawAmount(UNI, JSBI.BigInt(42)), - receipt: '', - timestamp: Date.now(), - }) - break - case 'ERROR': - setTransaction((tx) => { - invariant(tx) - tx.status = new Error( - 'Swap failed: Unknown error: "Error: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent pulvinar, risus eu pretium condimentum, tellus dui fermentum turpis, id gravida metus justo ac lorem. Etiam vitae dapibus eros, nec elementum ipsum. Duis condimentum, felis vel tempor ultricies, eros diam tempus odio, at tempor urna odio id massa. Aliquam laoreet turpis justo, auctor accumsan est pellentesque at. Integer et dolor feugiat, sodales tortor non, cursus augue. Phasellus id suscipit justo, in ultricies tortor. Aenean libero nibh, egestas sit amet vehicula sit amet, tempor ac ligula. Cras at tempor lectus. Mauris sollicitudin est velit, nec consectetur lorem dapibus ut. Praesent magna ex, faucibus ac fermentum malesuada, molestie at ex. Phasellus bibendum lorem nec dolor dignissim eleifend. Nam dignissim varius velit, at volutpat justo pretium id."' - ) - tx.elapsedMs = Date.now() - tx.timestamp - }) - break - case 'SUCCESS': - setTransaction((tx) => { - invariant(tx) - tx.status = true - tx.elapsedMs = Date.now() - tx.timestamp - }) - break - } - }, [setTransaction, state]) - return void 0} /> + return null + // TODO(zzmp): Mock void 0} /> } export default ( diff --git a/src/lib/components/Swap/Status/StatusDialog.tsx b/src/lib/components/Swap/Status/StatusDialog.tsx index 687613c637..90bd225cc5 100644 --- a/src/lib/components/Swap/Status/StatusDialog.tsx +++ b/src/lib/components/Swap/Status/StatusDialog.tsx @@ -1,16 +1,14 @@ import { Trans } from '@lingui/macro' -import { useAtomValue } from 'jotai/utils' import ErrorDialog, { StatusHeader } from 'lib/components/Error/ErrorDialog' import useInterval from 'lib/hooks/useInterval' import { CheckCircle, Clock, Spinner } from 'lib/icons' -import { SwapTransaction, swapTransactionAtom } from 'lib/state/swap' +import { SwapTransactionInfo, Transaction } from 'lib/state/transactions' import styled, { ThemedText } from 'lib/theme' import { useCallback, useMemo, useState } from 'react' import ActionButton from '../../ActionButton' import Column from '../../Column' import Row from '../../Row' -import Summary from '../Summary' const errorMessage = ( @@ -24,17 +22,17 @@ const TransactionRow = styled(Row)` flex-direction: row-reverse; ` -function ElapsedTime({ tx }: { tx: SwapTransaction | null }) { +function ElapsedTime({ tx }: { tx: Transaction }) { const [elapsedMs, setElapsedMs] = useState(0) useInterval( () => { - if (tx?.elapsedMs) { - setElapsedMs(tx.elapsedMs) - } else if (tx?.timestamp) { - setElapsedMs(Date.now() - tx.timestamp) + if (tx.info.response.timestamp) { + setElapsedMs(tx.info.response.timestamp - tx.addedTime) + } else { + setElapsedMs(Date.now() - tx.addedTime) } }, - elapsedMs === tx?.elapsedMs ? null : 1000 + elapsedMs === tx.info.response.timestamp ? null : 1000 ) const toElapsedTime = useCallback((ms: number) => { let sec = Math.floor(ms / 1000) @@ -63,22 +61,25 @@ const EtherscanA = styled.a` text-decoration: none; ` -interface TransactionStatusProps extends StatusProps { - tx: SwapTransaction | null +interface TransactionStatusProps { + tx: Transaction + onClose: () => void } function TransactionStatus({ tx, onClose }: TransactionStatusProps) { const Icon = useMemo(() => { - return tx?.status ? CheckCircle : Spinner - }, [tx?.status]) + return tx.receipt?.status ? CheckCircle : Spinner + }, [tx.receipt?.status]) const heading = useMemo(() => { - return tx?.status ? Transaction submitted : Transaction pending - }, [tx?.status]) + return tx.receipt?.status ? Transaction submitted : Transaction pending + }, [tx.receipt?.status]) return ( - + {heading} - {tx ? :
} + {/* TODO(zzmp): Display actual transaction. + + */} @@ -95,15 +96,14 @@ function TransactionStatus({ tx, onClose }: TransactionStatusProps) { ) } -interface StatusProps { - onClose: () => void -} - -export default function TransactionStatusDialog({ onClose }: StatusProps) { - const tx = useAtomValue(swapTransactionAtom) - - return tx?.status instanceof Error ? ( - Dismiss} onAction={onClose} /> +export default function TransactionStatusDialog({ tx, onClose }: TransactionStatusProps) { + return tx.receipt?.status === 0 ? ( + Dismiss} + onAction={onClose} + /> ) : ( ) diff --git a/src/lib/components/Swap/SwapButton.tsx b/src/lib/components/Swap/SwapButton.tsx index c2a6639bb1..2c047860b6 100644 --- a/src/lib/components/Swap/SwapButton.tsx +++ b/src/lib/components/Swap/SwapButton.tsx @@ -1,12 +1,14 @@ import { Trans } from '@lingui/macro' import { useSwapInfo } from 'lib/hooks/swap' import useSwapApproval, { ApprovalState, useSwapApprovalOptimizedTrade } from 'lib/hooks/swap/useSwapApproval' +import { useAddTransaction } from 'lib/hooks/transactions' +import { useIsPendingApproval } from 'lib/hooks/transactions' import { Field } from 'lib/state/swap' +import { TransactionType } from 'lib/state/transactions' import { useCallback, useEffect, useMemo, useState } from 'react' import ActionButton from '../ActionButton' import Dialog from '../Dialog' -import { StatusDialog } from './Status' import { SummaryDialog } from './Summary' interface SwapButtonProps { @@ -26,17 +28,26 @@ export default function SwapButton({ disabled }: SwapButtonProps) { setActiveTrade((activeTrade) => activeTrade && trade.trade) }, [trade]) - // TODO(zzmp): Track pending approval - const useIsPendingApproval = () => false - // TODO(zzmp): Return an optimized trade directly from useSwapInfo. - const optimizedTrade = useSwapApprovalOptimizedTrade(trade.trade, allowedSlippage, useIsPendingApproval) + const optimizedTrade = + // Use trade.trade if there is no swap optimized trade. This occurs if approvals are still pending. + useSwapApprovalOptimizedTrade(trade.trade, allowedSlippage, useIsPendingApproval) || trade.trade const [approval, getApproval] = useSwapApproval(optimizedTrade, allowedSlippage, useIsPendingApproval) + const addTransaction = useAddTransaction() + const addApprovalTransaction = useCallback(() => { + getApproval().then((transaction) => { + if (transaction) { + addTransaction({ type: TransactionType.APPROVAL, ...transaction }) + } + }) + }, [addTransaction, getApproval]) + const actionProps = useMemo(() => { if (disabled) return { disabled: true } if (inputCurrencyAmount && inputCurrencyBalance?.greaterThan(inputCurrencyAmount)) { + // TODO(zzmp): Update UI for pending approvals. if (approval === ApprovalState.PENDING) { return { disabled: true } } else if (approval === ApprovalState.NOT_APPROVED) { @@ -62,7 +73,7 @@ export default function SwapButton({ disabled }: SwapButtonProps) { setActiveTrade(trade.trade)} - onUpdate={getApproval} + onUpdate={addApprovalTransaction} {...actionProps} > Review swap @@ -72,11 +83,11 @@ export default function SwapButton({ disabled }: SwapButtonProps) { )} - {false && ( + {/* TODO(zzmp): Pass the completed tx, possibly at a different level of the DOM. void 0} /> - )} + */} ) } diff --git a/src/lib/components/Widget.tsx b/src/lib/components/Widget.tsx index 0aad902099..8bc0bcce9e 100644 --- a/src/lib/components/Widget.tsx +++ b/src/lib/components/Widget.tsx @@ -1,5 +1,6 @@ import { DEFAULT_LOCALE, SupportedLocale } from 'constants/locales' import { Provider as AtomProvider } from 'jotai' +import { TransactionsUpdater } from 'lib/hooks/transactions' import { BlockUpdater } from 'lib/hooks/useBlockNumber' import { UNMOUNTING } from 'lib/hooks/useUnmount' import { Provider as I18nProvider } from 'lib/i18n' @@ -73,6 +74,7 @@ function Updaters() { <> + ) } diff --git a/src/lib/hooks/transactions/index.tsx b/src/lib/hooks/transactions/index.tsx new file mode 100644 index 0000000000..560b50c9e8 --- /dev/null +++ b/src/lib/hooks/transactions/index.tsx @@ -0,0 +1,92 @@ +import { Token } from '@uniswap/sdk-core' +import { useAtomValue, useUpdateAtom } from 'jotai/utils' +import useActiveWeb3React from 'lib/hooks/useActiveWeb3React' +import { Transaction, TransactionInfo, transactionsAtom, TransactionType } from 'lib/state/transactions' +import ms from 'ms.macro' +import { useCallback } from 'react' +import invariant from 'tiny-invariant' + +import useBlockNumber from '../useBlockNumber' +import Updater from './updater' + +function isTransactionRecent(transaction: Transaction) { + return Date.now() - transaction.addedTime < ms`1d` +} + +export function usePendingTransactions() { + const { chainId } = useActiveWeb3React() + const txs = useAtomValue(transactionsAtom) + return (chainId ? txs[chainId] : null) ?? {} +} + +export function useAddTransaction() { + const { chainId } = useActiveWeb3React() + const blockNumber = useBlockNumber() + const updateTxs = useUpdateAtom(transactionsAtom) + + return useCallback( + (info: TransactionInfo) => { + invariant(chainId) + const txChainId = chainId + const { hash } = info.response + + updateTxs((chainTxs) => { + const txs = chainTxs[txChainId] || {} + txs[hash] = { addedTime: new Date().getTime(), lastCheckedBlockNumber: blockNumber, info } + chainTxs[chainId] = txs + }) + }, + [blockNumber, chainId, updateTxs] + ) +} + +export function useIsPendingApproval(token?: Token, spender?: string) { + const { chainId } = useActiveWeb3React() + const txs = useAtomValue(transactionsAtom) + if (!chainId || !token || !spender) return false + + const chainTxs = txs[chainId] + if (!chainTxs) return false + + return Object.values(chainTxs).some( + (tx) => + tx && + tx.receipt === undefined && + tx.info.type === TransactionType.APPROVAL && + tx.info.tokenAddress === token.address && + tx.info.spenderAddress === spender && + isTransactionRecent(tx) + ) +} + +export function TransactionsUpdater() { + const pendingTransactions = usePendingTransactions() + + const updateTxs = useUpdateAtom(transactionsAtom) + const onCheck = useCallback( + ({ chainId, hash, blockNumber }) => { + updateTxs((txs) => { + const tx = txs[chainId]?.[hash] + if (tx) { + tx.lastCheckedBlockNumber = tx.lastCheckedBlockNumber + ? Math.max(tx.lastCheckedBlockNumber, blockNumber) + : blockNumber + } + }) + }, + [updateTxs] + ) + const onReceipt = useCallback( + ({ chainId, hash, receipt }) => { + updateTxs((txs) => { + const tx = txs[chainId]?.[hash] + if (tx) { + tx.receipt = receipt + } + }) + }, + [updateTxs] + ) + + return +} diff --git a/src/state/transactions/updater.test.ts b/src/lib/hooks/transactions/updater.test.ts similarity index 100% rename from src/state/transactions/updater.test.ts rename to src/lib/hooks/transactions/updater.test.ts diff --git a/src/lib/hooks/transactions/updater.tsx b/src/lib/hooks/transactions/updater.tsx new file mode 100644 index 0000000000..063fe9f617 --- /dev/null +++ b/src/lib/hooks/transactions/updater.tsx @@ -0,0 +1,101 @@ +import { TransactionReceipt } from '@ethersproject/abstract-provider' +import { SupportedChainId } from 'constants/chains' +import useActiveWeb3React from 'hooks/useActiveWeb3React' +import useBlockNumber, { useFastForwardBlockNumber } from 'lib/hooks/useBlockNumber' +import ms from 'ms.macro' +import { useCallback, useEffect } from 'react' +import { retry, RetryableError, RetryOptions } from 'utils/retry' + +interface Transaction { + addedTime: number + receipt?: unknown + lastCheckedBlockNumber?: number +} + +export function shouldCheck(lastBlockNumber: number, tx: Transaction): boolean { + if (tx.receipt) return false + if (!tx.lastCheckedBlockNumber) return true + const blocksSinceCheck = lastBlockNumber - tx.lastCheckedBlockNumber + if (blocksSinceCheck < 1) return false + const minutesPending = (new Date().getTime() - tx.addedTime) / ms`1m` + if (minutesPending > 60) { + // every 10 blocks if pending longer than an hour + return blocksSinceCheck > 9 + } else if (minutesPending > 5) { + // every 3 blocks if pending longer than 5 minutes + return blocksSinceCheck > 2 + } else { + // otherwise every block + return true + } +} + +const RETRY_OPTIONS_BY_CHAIN_ID: { [chainId: number]: RetryOptions } = { + [SupportedChainId.ARBITRUM_ONE]: { n: 10, minWait: 250, maxWait: 1000 }, + [SupportedChainId.ARBITRUM_RINKEBY]: { n: 10, minWait: 250, maxWait: 1000 }, + [SupportedChainId.OPTIMISTIC_KOVAN]: { n: 10, minWait: 250, maxWait: 1000 }, + [SupportedChainId.OPTIMISM]: { n: 10, minWait: 250, maxWait: 1000 }, +} +const DEFAULT_RETRY_OPTIONS: RetryOptions = { n: 1, minWait: 0, maxWait: 0 } + +interface UpdaterProps { + pendingTransactions: { [hash: string]: Transaction } + onCheck: (tx: { chainId: number; hash: string; blockNumber: number }) => void + onReceipt: (tx: { chainId: number; hash: string; receipt: TransactionReceipt }) => void +} + +export default function Updater({ pendingTransactions, onCheck, onReceipt }: UpdaterProps): null { + const { chainId, library } = useActiveWeb3React() + + const lastBlockNumber = useBlockNumber() + const fastForwardBlockNumber = useFastForwardBlockNumber() + + const getReceipt = useCallback( + (hash: string) => { + if (!library || !chainId) throw new Error('No library or chainId') + const retryOptions = RETRY_OPTIONS_BY_CHAIN_ID[chainId] ?? DEFAULT_RETRY_OPTIONS + return retry( + () => + library.getTransactionReceipt(hash).then((receipt) => { + if (receipt === null) { + console.debug(`Retrying tranasaction receipt for ${hash}`) + throw new RetryableError() + } + return receipt + }), + retryOptions + ) + }, + [chainId, library] + ) + + useEffect(() => { + if (!chainId || !library || !lastBlockNumber) return + + const cancels = Object.keys(pendingTransactions) + .filter((hash) => shouldCheck(lastBlockNumber, pendingTransactions[hash])) + .map((hash) => { + const { promise, cancel } = getReceipt(hash) + promise + .then((receipt) => { + if (receipt) { + onReceipt({ chainId, hash, receipt }) + } else { + onCheck({ chainId, hash, blockNumber: lastBlockNumber }) + } + }) + .catch((error) => { + if (!error.isCancelledError) { + console.warn(`Failed to get transaction receipt for ${hash}`, error) + } + }) + return cancel + }) + + return () => { + cancels.forEach((cancel) => cancel()) + } + }, [chainId, library, lastBlockNumber, getReceipt, fastForwardBlockNumber, onReceipt, onCheck, pendingTransactions]) + + return null +} diff --git a/src/lib/state/swap.ts b/src/lib/state/swap.ts index 50458abf5a..804467b4c2 100644 --- a/src/lib/state/swap.ts +++ b/src/lib/state/swap.ts @@ -1,4 +1,4 @@ -import { Currency, CurrencyAmount } from '@uniswap/sdk-core' +import { Currency } from '@uniswap/sdk-core' import { SupportedChainId } from 'constants/chains' import { nativeOnChain } from 'constants/tokens' import { atomWithImmer } from 'jotai/immer' @@ -26,14 +26,3 @@ export const swapAtom = atomWithImmer({ export const independentFieldAtom = pickAtom(swapAtom, 'independentField') export const integratorFeeAtom = pickAtom(swapAtom, 'integratorFee') export const amountAtom = pickAtom(swapAtom, 'amount') - -export interface SwapTransaction { - input: CurrencyAmount - output: CurrencyAmount - receipt: string - timestamp: number - elapsedMs?: number - status?: true | Error -} - -export const swapTransactionAtom = atomWithImmer(null) diff --git a/src/lib/state/transactions.ts b/src/lib/state/transactions.ts new file mode 100644 index 0000000000..514cd30db7 --- /dev/null +++ b/src/lib/state/transactions.ts @@ -0,0 +1,53 @@ +import { TransactionReceipt, TransactionResponse } from '@ethersproject/abstract-provider' +import { TradeType } from '@uniswap/sdk-core' +import { atomWithImmer } from 'jotai/immer' + +export enum TransactionType { + APPROVAL, + SWAP, +} + +interface BaseTransactionInfo { + type: TransactionType + response: TransactionResponse +} + +export interface ApprovalTransactionInfo extends BaseTransactionInfo { + type: TransactionType.APPROVAL + tokenAddress: string + spenderAddress: string +} + +export interface SwapTransactionInfo extends BaseTransactionInfo { + type: TransactionType.SWAP + tradeType: TradeType + inputCurrencyAddress: string + outputCurrencyAddress: string +} + +export interface InputSwapTransactionInfo extends SwapTransactionInfo { + tradeType: TradeType.EXACT_INPUT + inputCurrencyAmount: string + expectedOutputCurrencyAmount: string + minimumOutputCurrencyAmount: string +} + +export interface OutputSwapTransactionInfo extends SwapTransactionInfo { + tradeType: TradeType.EXACT_OUTPUT + outputCurrencyAmount: string + expectedInputCurrencyAmount: string + maximumInputCurrencyAmount: string +} + +export type TransactionInfo = ApprovalTransactionInfo | SwapTransactionInfo + +export interface Transaction { + addedTime: number + lastCheckedBlockNumber?: number + receipt?: TransactionReceipt + info: T +} + +export const transactionsAtom = atomWithImmer<{ + [chainId: string]: { [hash: string]: Transaction } +}>({}) diff --git a/src/state/transactions/updater.tsx b/src/state/transactions/updater.tsx index b397112896..c9a9869765 100644 --- a/src/state/transactions/updater.tsx +++ b/src/state/transactions/updater.tsx @@ -1,139 +1,55 @@ import { DEFAULT_TXN_DISMISS_MS, L2_TXN_DISMISS_MS } from 'constants/misc' import useActiveWeb3React from 'hooks/useActiveWeb3React' -import useBlockNumber, { useFastForwardBlockNumber } from 'lib/hooks/useBlockNumber' -import { useCallback, useEffect, useMemo } from 'react' +import LibUpdater from 'lib/hooks/transactions/updater' +import { useCallback, useMemo } from 'react' import { useAppDispatch, useAppSelector } from 'state/hooks' -import { L2_CHAIN_IDS, SupportedChainId } from '../../constants/chains' -import { retry, RetryableError, RetryOptions } from '../../utils/retry' +import { L2_CHAIN_IDS } from '../../constants/chains' import { useAddPopup } from '../application/hooks' import { checkedTransaction, finalizeTransaction } from './actions' -interface TxInterface { - addedTime: number - receipt?: Record - lastCheckedBlockNumber?: number -} - -export function shouldCheck(lastBlockNumber: number, tx: TxInterface): boolean { - if (tx.receipt) return false - if (!tx.lastCheckedBlockNumber) return true - const blocksSinceCheck = lastBlockNumber - tx.lastCheckedBlockNumber - if (blocksSinceCheck < 1) return false - const minutesPending = (new Date().getTime() - tx.addedTime) / 1000 / 60 - if (minutesPending > 60) { - // every 10 blocks if pending for longer than an hour - return blocksSinceCheck > 9 - } else if (minutesPending > 5) { - // every 3 blocks if pending more than 5 minutes - return blocksSinceCheck > 2 - } else { - // otherwise every block - return true - } -} - -const RETRY_OPTIONS_BY_CHAIN_ID: { [chainId: number]: RetryOptions } = { - [SupportedChainId.ARBITRUM_ONE]: { n: 10, minWait: 250, maxWait: 1000 }, - [SupportedChainId.ARBITRUM_RINKEBY]: { n: 10, minWait: 250, maxWait: 1000 }, - [SupportedChainId.OPTIMISTIC_KOVAN]: { n: 10, minWait: 250, maxWait: 1000 }, - [SupportedChainId.OPTIMISM]: { n: 10, minWait: 250, maxWait: 1000 }, -} -const DEFAULT_RETRY_OPTIONS: RetryOptions = { n: 1, minWait: 0, maxWait: 0 } - -export default function Updater(): null { - const { chainId, library } = useActiveWeb3React() - - const lastBlockNumber = useBlockNumber() - const fastForwardBlockNumber = useFastForwardBlockNumber() - - const dispatch = useAppDispatch() - const state = useAppSelector((state) => state.transactions) - - const transactions = useMemo(() => (chainId ? state[chainId] ?? {} : {}), [chainId, state]) - - // show popup on confirm +export default function Updater() { + const { chainId } = useActiveWeb3React() const addPopup = useAddPopup() - // speed up popup dismisall time if on L2 const isL2 = Boolean(chainId && L2_CHAIN_IDS.includes(chainId)) - const getReceipt = useCallback( - (hash: string) => { - if (!library || !chainId) throw new Error('No library or chainId') - const retryOptions = RETRY_OPTIONS_BY_CHAIN_ID[chainId] ?? DEFAULT_RETRY_OPTIONS - return retry( - () => - library.getTransactionReceipt(hash).then((receipt) => { - if (receipt === null) { - console.debug('Retrying for hash', hash) - throw new RetryableError() - } - return receipt - }), - retryOptions + const dispatch = useAppDispatch() + const onCheck = useCallback( + ({ chainId, hash, blockNumber }) => dispatch(checkedTransaction({ chainId, hash, blockNumber })), + [dispatch] + ) + const onReceipt = useCallback( + ({ chainId, hash, receipt }) => { + dispatch( + finalizeTransaction({ + chainId, + hash, + receipt: { + blockHash: receipt.blockHash, + blockNumber: receipt.blockNumber, + contractAddress: receipt.contractAddress, + from: receipt.from, + status: receipt.status, + to: receipt.to, + transactionHash: receipt.transactionHash, + transactionIndex: receipt.transactionIndex, + }, + }) + ) + addPopup( + { + txn: { hash }, + }, + hash, + isL2 ? L2_TXN_DISMISS_MS : DEFAULT_TXN_DISMISS_MS ) }, - [chainId, library] + [addPopup, dispatch, isL2] ) - useEffect(() => { - if (!chainId || !library || !lastBlockNumber) return + const state = useAppSelector((state) => state.transactions) + const pendingTransactions = useMemo(() => (chainId ? state[chainId] ?? {} : {}), [chainId, state]) - const cancels = Object.keys(transactions) - .filter((hash) => shouldCheck(lastBlockNumber, transactions[hash])) - .map((hash) => { - const { promise, cancel } = getReceipt(hash) - promise - .then((receipt) => { - if (receipt) { - dispatch( - finalizeTransaction({ - chainId, - hash, - receipt: { - blockHash: receipt.blockHash, - blockNumber: receipt.blockNumber, - contractAddress: receipt.contractAddress, - from: receipt.from, - status: receipt.status, - to: receipt.to, - transactionHash: receipt.transactionHash, - transactionIndex: receipt.transactionIndex, - }, - }) - ) - - addPopup( - { - txn: { - hash, - }, - }, - hash, - isL2 ? L2_TXN_DISMISS_MS : DEFAULT_TXN_DISMISS_MS - ) - - // the receipt was fetched before the block, fast forward to that block to trigger balance updates - if (receipt.blockNumber > lastBlockNumber) { - fastForwardBlockNumber(receipt.blockNumber) - } - } else { - dispatch(checkedTransaction({ chainId, hash, blockNumber: lastBlockNumber })) - } - }) - .catch((error) => { - if (!error.isCancelledError) { - console.error(`Failed to check transaction hash: ${hash}`, error) - } - }) - return cancel - }) - - return () => { - cancels.forEach((cancel) => cancel()) - } - }, [chainId, library, transactions, lastBlockNumber, dispatch, addPopup, getReceipt, isL2, fastForwardBlockNumber]) - - return null + return }