Commit c7633d91 authored by Zach Pomerantz's avatar Zach Pomerantz Committed by GitHub

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
parent 1f89a46a
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 { 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() { function Fixture() {
const setTransaction = useUpdateAtom(swapTransactionAtom) return null
// TODO(zzmp): Mock <StatusDialog tx={} onClose={() => void 0} />
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} />
} }
export default ( export default (
......
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { useAtomValue } from 'jotai/utils'
import ErrorDialog, { StatusHeader } from 'lib/components/Error/ErrorDialog' import ErrorDialog, { StatusHeader } from 'lib/components/Error/ErrorDialog'
import useInterval from 'lib/hooks/useInterval' import useInterval from 'lib/hooks/useInterval'
import { CheckCircle, Clock, Spinner } from 'lib/icons' 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 styled, { ThemedText } from 'lib/theme'
import { useCallback, useMemo, useState } from 'react' import { useCallback, useMemo, useState } from 'react'
import ActionButton from '../../ActionButton' import ActionButton from '../../ActionButton'
import Column from '../../Column' import Column from '../../Column'
import Row from '../../Row' import Row from '../../Row'
import Summary from '../Summary'
const errorMessage = ( const errorMessage = (
<Trans> <Trans>
...@@ -24,17 +22,17 @@ const TransactionRow = styled(Row)` ...@@ -24,17 +22,17 @@ const TransactionRow = styled(Row)`
flex-direction: row-reverse; flex-direction: row-reverse;
` `
function ElapsedTime({ tx }: { tx: SwapTransaction | null }) { function ElapsedTime({ tx }: { tx: Transaction<SwapTransactionInfo> }) {
const [elapsedMs, setElapsedMs] = useState(0) const [elapsedMs, setElapsedMs] = useState(0)
useInterval( useInterval(
() => { () => {
if (tx?.elapsedMs) { if (tx.info.response.timestamp) {
setElapsedMs(tx.elapsedMs) setElapsedMs(tx.info.response.timestamp - tx.addedTime)
} else if (tx?.timestamp) { } else {
setElapsedMs(Date.now() - tx.timestamp) setElapsedMs(Date.now() - tx.addedTime)
} }
}, },
elapsedMs === tx?.elapsedMs ? null : 1000 elapsedMs === tx.info.response.timestamp ? null : 1000
) )
const toElapsedTime = useCallback((ms: number) => { const toElapsedTime = useCallback((ms: number) => {
let sec = Math.floor(ms / 1000) let sec = Math.floor(ms / 1000)
...@@ -63,22 +61,25 @@ const EtherscanA = styled.a` ...@@ -63,22 +61,25 @@ const EtherscanA = styled.a`
text-decoration: none; text-decoration: none;
` `
interface TransactionStatusProps extends StatusProps { interface TransactionStatusProps {
tx: SwapTransaction | null tx: Transaction<SwapTransactionInfo>
onClose: () => void
} }
function TransactionStatus({ tx, onClose }: TransactionStatusProps) { function TransactionStatus({ tx, onClose }: TransactionStatusProps) {
const Icon = useMemo(() => { const Icon = useMemo(() => {
return tx?.status ? CheckCircle : Spinner return tx.receipt?.status ? CheckCircle : Spinner
}, [tx?.status]) }, [tx.receipt?.status])
const heading = useMemo(() => { const heading = useMemo(() => {
return tx?.status ? <Trans>Transaction submitted</Trans> : <Trans>Transaction pending</Trans> return tx.receipt?.status ? <Trans>Transaction submitted</Trans> : <Trans>Transaction pending</Trans>
}, [tx?.status]) }, [tx.receipt?.status])
return ( return (
<Column flex padded gap={0.75} align="stretch" style={{ height: '100%' }}> <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> <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> </StatusHeader>
<TransactionRow flex> <TransactionRow flex>
<ThemedText.ButtonSmall> <ThemedText.ButtonSmall>
...@@ -95,15 +96,14 @@ function TransactionStatus({ tx, onClose }: TransactionStatusProps) { ...@@ -95,15 +96,14 @@ function TransactionStatus({ tx, onClose }: TransactionStatusProps) {
) )
} }
interface StatusProps { export default function TransactionStatusDialog({ tx, onClose }: TransactionStatusProps) {
onClose: () => void return tx.receipt?.status === 0 ? (
} <ErrorDialog
header={errorMessage}
export default function TransactionStatusDialog({ onClose }: StatusProps) { error={new Error('TODO(zzmp)')}
const tx = useAtomValue(swapTransactionAtom) action={<Trans>Dismiss</Trans>}
onAction={onClose}
return tx?.status instanceof Error ? ( />
<ErrorDialog header={errorMessage} error={tx.status} action={<Trans>Dismiss</Trans>} onAction={onClose} />
) : ( ) : (
<TransactionStatus tx={tx} onClose={onClose} /> <TransactionStatus tx={tx} onClose={onClose} />
) )
......
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { useSwapInfo } from 'lib/hooks/swap' import { useSwapInfo } from 'lib/hooks/swap'
import useSwapApproval, { ApprovalState, useSwapApprovalOptimizedTrade } from 'lib/hooks/swap/useSwapApproval' 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 { Field } from 'lib/state/swap'
import { TransactionType } from 'lib/state/transactions'
import { useCallback, useEffect, useMemo, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import ActionButton from '../ActionButton' import ActionButton from '../ActionButton'
import Dialog from '../Dialog' import Dialog from '../Dialog'
import { StatusDialog } from './Status'
import { SummaryDialog } from './Summary' import { SummaryDialog } from './Summary'
interface SwapButtonProps { interface SwapButtonProps {
...@@ -26,17 +28,26 @@ export default function SwapButton({ disabled }: SwapButtonProps) { ...@@ -26,17 +28,26 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
setActiveTrade((activeTrade) => activeTrade && trade.trade) setActiveTrade((activeTrade) => activeTrade && trade.trade)
}, [trade]) }, [trade])
// TODO(zzmp): Track pending approval
const useIsPendingApproval = () => false
// TODO(zzmp): Return an optimized trade directly from useSwapInfo. // 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 [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(() => { const actionProps = useMemo(() => {
if (disabled) return { disabled: true } if (disabled) return { disabled: true }
if (inputCurrencyAmount && inputCurrencyBalance?.greaterThan(inputCurrencyAmount)) { if (inputCurrencyAmount && inputCurrencyBalance?.greaterThan(inputCurrencyAmount)) {
// TODO(zzmp): Update UI for pending approvals.
if (approval === ApprovalState.PENDING) { if (approval === ApprovalState.PENDING) {
return { disabled: true } return { disabled: true }
} else if (approval === ApprovalState.NOT_APPROVED) { } else if (approval === ApprovalState.NOT_APPROVED) {
...@@ -62,7 +73,7 @@ export default function SwapButton({ disabled }: SwapButtonProps) { ...@@ -62,7 +73,7 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
<ActionButton <ActionButton
color="interactive" color="interactive"
onClick={() => setActiveTrade(trade.trade)} onClick={() => setActiveTrade(trade.trade)}
onUpdate={getApproval} onUpdate={addApprovalTransaction}
{...actionProps} {...actionProps}
> >
<Trans>Review swap</Trans> <Trans>Review swap</Trans>
...@@ -72,11 +83,11 @@ export default function SwapButton({ disabled }: SwapButtonProps) { ...@@ -72,11 +83,11 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
<SummaryDialog trade={activeTrade} allowedSlippage={allowedSlippage} onConfirm={onConfirm} /> <SummaryDialog trade={activeTrade} allowedSlippage={allowedSlippage} onConfirm={onConfirm} />
</Dialog> </Dialog>
)} )}
{false && ( {/* TODO(zzmp): Pass the completed tx, possibly at a different level of the DOM.
<Dialog color="dialog"> <Dialog color="dialog">
<StatusDialog onClose={() => void 0} /> <StatusDialog onClose={() => void 0} />
</Dialog> </Dialog>
)} */}
</> </>
) )
} }
import { DEFAULT_LOCALE, SupportedLocale } from 'constants/locales' import { DEFAULT_LOCALE, SupportedLocale } from 'constants/locales'
import { Provider as AtomProvider } from 'jotai' import { Provider as AtomProvider } from 'jotai'
import { TransactionsUpdater } from 'lib/hooks/transactions'
import { BlockUpdater } from 'lib/hooks/useBlockNumber' import { BlockUpdater } from 'lib/hooks/useBlockNumber'
import { UNMOUNTING } from 'lib/hooks/useUnmount' import { UNMOUNTING } from 'lib/hooks/useUnmount'
import { Provider as I18nProvider } from 'lib/i18n' import { Provider as I18nProvider } from 'lib/i18n'
...@@ -73,6 +74,7 @@ function Updaters() { ...@@ -73,6 +74,7 @@ function Updaters() {
<> <>
<BlockUpdater /> <BlockUpdater />
<MulticallUpdater /> <MulticallUpdater />
<TransactionsUpdater />
</> </>
) )
} }
......
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} />
}
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
}
import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { Currency } from '@uniswap/sdk-core'
import { SupportedChainId } from 'constants/chains' import { SupportedChainId } from 'constants/chains'
import { nativeOnChain } from 'constants/tokens' import { nativeOnChain } from 'constants/tokens'
import { atomWithImmer } from 'jotai/immer' import { atomWithImmer } from 'jotai/immer'
...@@ -26,14 +26,3 @@ export const swapAtom = atomWithImmer<Swap>({ ...@@ -26,14 +26,3 @@ export const swapAtom = atomWithImmer<Swap>({
export const independentFieldAtom = pickAtom(swapAtom, 'independentField') export const independentFieldAtom = pickAtom(swapAtom, 'independentField')
export const integratorFeeAtom = pickAtom(swapAtom, 'integratorFee') export const integratorFeeAtom = pickAtom(swapAtom, 'integratorFee')
export const amountAtom = pickAtom(swapAtom, 'amount') 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)
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 }
}>({})
import { DEFAULT_TXN_DISMISS_MS, L2_TXN_DISMISS_MS } from 'constants/misc' import { DEFAULT_TXN_DISMISS_MS, L2_TXN_DISMISS_MS } from 'constants/misc'
import useActiveWeb3React from 'hooks/useActiveWeb3React' import useActiveWeb3React from 'hooks/useActiveWeb3React'
import useBlockNumber, { useFastForwardBlockNumber } from 'lib/hooks/useBlockNumber' import LibUpdater from 'lib/hooks/transactions/updater'
import { useCallback, useEffect, useMemo } from 'react' import { useCallback, useMemo } from 'react'
import { useAppDispatch, useAppSelector } from 'state/hooks' import { useAppDispatch, useAppSelector } from 'state/hooks'
import { L2_CHAIN_IDS, SupportedChainId } from '../../constants/chains' import { L2_CHAIN_IDS } from '../../constants/chains'
import { retry, RetryableError, RetryOptions } from '../../utils/retry'
import { useAddPopup } from '../application/hooks' import { useAddPopup } from '../application/hooks'
import { checkedTransaction, finalizeTransaction } from './actions' import { checkedTransaction, finalizeTransaction } from './actions'
interface TxInterface { export default function Updater() {
addedTime: number const { chainId } = useActiveWeb3React()
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
const addPopup = useAddPopup() const addPopup = useAddPopup()
// speed up popup dismisall time if on L2 // speed up popup dismisall time if on L2
const isL2 = Boolean(chainId && L2_CHAIN_IDS.includes(chainId)) const isL2 = Boolean(chainId && L2_CHAIN_IDS.includes(chainId))
const getReceipt = useCallback( const dispatch = useAppDispatch()
(hash: string) => { const onCheck = useCallback(
if (!library || !chainId) throw new Error('No library or chainId') ({ chainId, hash, blockNumber }) => dispatch(checkedTransaction({ chainId, hash, blockNumber })),
const retryOptions = RETRY_OPTIONS_BY_CHAIN_ID[chainId] ?? DEFAULT_RETRY_OPTIONS [dispatch]
return retry( )
() => const onReceipt = useCallback(
library.getTransactionReceipt(hash).then((receipt) => { ({ chainId, hash, receipt }) => {
if (receipt === null) { dispatch(
console.debug('Retrying for hash', hash) finalizeTransaction({
throw new RetryableError() chainId,
} hash,
return receipt receipt: {
}), blockHash: receipt.blockHash,
retryOptions 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(() => { const state = useAppSelector((state) => state.transactions)
if (!chainId || !library || !lastBlockNumber) return 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} />
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment