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
This commit is contained in:
Zach Pomerantz 2022-01-25 15:55:27 -08:00 committed by GitHub
parent 1f89a46a3f
commit c7633d910b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 335 additions and 231 deletions

@ -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",

@ -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 <StatusDialog onClose={() => void 0} />
return null
// TODO(zzmp): Mock <StatusDialog tx={} onClose={() => void 0} />
}
export default (

@ -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 = (
<Trans>
@ -24,17 +22,17 @@ const TransactionRow = styled(Row)`
flex-direction: row-reverse;
`
function ElapsedTime({ tx }: { tx: SwapTransaction | null }) {
function ElapsedTime({ tx }: { tx: Transaction<SwapTransactionInfo> }) {
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<SwapTransactionInfo>
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 ? <Trans>Transaction submitted</Trans> : <Trans>Transaction pending</Trans>
}, [tx?.status])
return tx.receipt?.status ? <Trans>Transaction submitted</Trans> : <Trans>Transaction pending</Trans>
}, [tx.receipt?.status])
return (
<Column flex padded gap={0.75} align="stretch" style={{ height: '100%' }}>
<StatusHeader icon={Icon} iconColor={tx?.status && 'success'}>
<StatusHeader icon={Icon} iconColor={tx.receipt?.status ? 'success' : undefined}>
<ThemedText.Subhead1>{heading}</ThemedText.Subhead1>
{tx ? <Summary input={tx.input} output={tx.output} /> : <div style={{ height: '1.25em' }} />}
{/* TODO(zzmp): Display actual transaction.
<Summary input={tx.info.inputCurrency} output={tx.info.outputCurrency} />
*/}
</StatusHeader>
<TransactionRow flex>
<ThemedText.ButtonSmall>
@ -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 ? (
<ErrorDialog header={errorMessage} error={tx.status} action={<Trans>Dismiss</Trans>} onAction={onClose} />
export default function TransactionStatusDialog({ tx, onClose }: TransactionStatusProps) {
return tx.receipt?.status === 0 ? (
<ErrorDialog
header={errorMessage}
error={new Error('TODO(zzmp)')}
action={<Trans>Dismiss</Trans>}
onAction={onClose}
/>
) : (
<TransactionStatus tx={tx} onClose={onClose} />
)

@ -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) {
<ActionButton
color="interactive"
onClick={() => setActiveTrade(trade.trade)}
onUpdate={getApproval}
onUpdate={addApprovalTransaction}
{...actionProps}
>
<Trans>Review swap</Trans>
@ -72,11 +83,11 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
<SummaryDialog trade={activeTrade} allowedSlippage={allowedSlippage} onConfirm={onConfirm} />
</Dialog>
)}
{false && (
{/* TODO(zzmp): Pass the completed tx, possibly at a different level of the DOM.
<Dialog color="dialog">
<StatusDialog onClose={() => void 0} />
</Dialog>
)}
*/}
</>
)
}

@ -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() {
<>
<BlockUpdater />
<MulticallUpdater />
<TransactionsUpdater />
</>
)
}

@ -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 <Updater pendingTransactions={pendingTransactions} onCheck={onCheck} onReceipt={onReceipt} />
}

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

@ -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<Swap>({
export const independentFieldAtom = pickAtom(swapAtom, 'independentField')
export const integratorFeeAtom = pickAtom(swapAtom, 'integratorFee')
export const amountAtom = pickAtom(swapAtom, 'amount')
export interface SwapTransaction {
input: CurrencyAmount<Currency>
output: CurrencyAmount<Currency>
receipt: string
timestamp: number
elapsedMs?: number
status?: true | Error
}
export const swapTransactionAtom = atomWithImmer<SwapTransaction | null>(null)

@ -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<T extends TransactionInfo = TransactionInfo> {
addedTime: number
lastCheckedBlockNumber?: number
receipt?: TransactionReceipt
info: T
}
export const transactionsAtom = atomWithImmer<{
[chainId: string]: { [hash: string]: Transaction }
}>({})

@ -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<string, any>
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 <LibUpdater pendingTransactions={pendingTransactions} onCheck={onCheck} onReceipt={onReceipt} />
}