From 54145ba6b8915e01e511d052cc26b53cc8e50e44 Mon Sep 17 00:00:00 2001 From: Bartek Date: Mon, 10 Mar 2025 16:38:08 +0100 Subject: [PATCH] fetch core history before orbit --- .../TransactionHistorySearchResults.tsx | 3 +- .../TransactionHistoryTable.tsx | 31 +- .../TransactionsTableDetails.tsx | 4 +- .../useTransactionReminderInfo.ts | 4 +- .../TransferPanel/TransferPanel.tsx | 4 +- .../src/hooks/useClaimWithdrawal.ts | 6 +- .../src/hooks/useRedeemRetryable.ts | 4 +- .../src/hooks/useRedeemTeleporter.ts | 2 +- .../src/hooks/useTransactionHistory.ts | 276 +++++++++++++----- .../src/state/cctpState.ts | 4 +- 10 files changed, 258 insertions(+), 80 deletions(-) diff --git a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistorySearchResults.tsx b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistorySearchResults.tsx index e63423241d..aaf73e3cf0 100644 --- a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistorySearchResults.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistorySearchResults.tsx @@ -24,7 +24,8 @@ import { OftTransactionHistoryDisclaimer } from './OftTransactionHistoryDisclaim function useTransactionHistoryUpdater() { const { sanitizedAddress } = useTransactionHistoryAddressStore() - const transactionHistoryProps = useTransactionHistory(sanitizedAddress, { + const transactionHistoryProps = useTransactionHistory({ + address: sanitizedAddress, runFetcher: true }) diff --git a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistoryTable.tsx b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistoryTable.tsx index 2c05ec0da8..2fc9c6b817 100644 --- a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistoryTable.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistoryTable.tsx @@ -18,6 +18,7 @@ import { getProviderForChainId } from '@/token-bridge-sdk/utils' import { isTokenDeposit } from '../../state/app/utils' import { ChainPair, + TransactionHistoryLoadingStates, UseTransactionHistoryResult } from '../../hooks/useTransactionHistory' import { Tooltip } from '../common/Tooltip' @@ -28,6 +29,7 @@ import { TransactionsTableRow } from './TransactionsTableRow' import { EmptyTransactionHistory } from './EmptyTransactionHistory' import { MergedTransaction } from '../../state/app/state' import { useNativeCurrency } from '../../hooks/useNativeCurrency' +import { Loader } from '../common/atoms/Loader' export const BatchTransferNativeTokenTooltip = ({ children, @@ -134,6 +136,24 @@ type TransactionHistoryTableProps = UseTransactionHistoryResult & { oldestTxTimeAgoString: string } +function loadingStateToTooltip(loadingState: TransactionHistoryLoadingStates) { + let text + + if (loadingState.core) { + text = 'Core Chains' + } + + if (loadingState.orbit) { + text = 'Orbit Chains' + } + + if (!text) { + return null + } + + return We are still loading history for {text}. +} + export const TransactionHistoryTable = ( props: TransactionHistoryTableProps ) => { @@ -153,7 +173,7 @@ export const TransactionHistoryTable = ( const isTxHistoryEmpty = transactions.length === 0 const isPendingTab = selectedTabIndex === 0 - const paused = !loading && !completed + const paused = !loading.any && !completed const contentWrapperRef = useRef(null) const tableRef = useRef(null) @@ -194,7 +214,7 @@ export const TransactionHistoryTable = ( if (isTxHistoryEmpty) { return ( - {loading ? ( + {loading.core ? (
@@ -222,6 +242,11 @@ export const TransactionHistoryTable = ( ) : (
+ {loading.any && ( + + + + )} Showing {transactions.length}{' '} diff --git a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableDetails.tsx b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableDetails.tsx index c447a93733..7f6de4a1ac 100644 --- a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableDetails.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableDetails.tsx @@ -48,7 +48,9 @@ export const TransactionsTableDetails = () => { const { sanitizedAddress } = useTransactionHistoryAddressStore() const { tx: txFromStore, isOpen, close, reset } = useTxDetailsStore() const { ethToUSD } = useETHPrice() - const { transactions } = useTransactionHistory(sanitizedAddress) + const { transactions } = useTransactionHistory({ + address: sanitizedAddress + }) const tx = useMemo(() => { if (!txFromStore) { diff --git a/packages/arb-token-bridge-ui/src/components/TransactionHistory/useTransactionReminderInfo.ts b/packages/arb-token-bridge-ui/src/components/TransactionHistory/useTransactionReminderInfo.ts index 8385965c03..94351af51f 100644 --- a/packages/arb-token-bridge-ui/src/components/TransactionHistory/useTransactionReminderInfo.ts +++ b/packages/arb-token-bridge-ui/src/components/TransactionHistory/useTransactionReminderInfo.ts @@ -7,7 +7,9 @@ import { useTransactionHistoryAddressStore } from './TransactionHistorySearchBar export function useTransactionReminderInfo() { const { sanitizedAddress } = useTransactionHistoryAddressStore() - const { transactions } = useTransactionHistory(sanitizedAddress) + const { transactions } = useTransactionHistory({ + address: sanitizedAddress + }) const { numClaimableTransactions, diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx index 4c504c79a0..f716ff01da 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx @@ -151,7 +151,9 @@ export function TransferPanel() { const { setTransferring } = useAppContextActions() const { switchToTransactionHistoryTab } = useMainContentTabs() - const { addPendingTransaction } = useTransactionHistory(walletAddress) + const { addPendingTransaction } = useTransactionHistory({ + address: walletAddress + }) const isCctpTransfer = useIsCctpTransfer() diff --git a/packages/arb-token-bridge-ui/src/hooks/useClaimWithdrawal.ts b/packages/arb-token-bridge-ui/src/hooks/useClaimWithdrawal.ts index be9b21769d..defb43995c 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useClaimWithdrawal.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useClaimWithdrawal.ts @@ -31,9 +31,9 @@ export function useClaimWithdrawal( const { address } = useAccount() const { sanitizedAddress } = useTransactionHistoryAddressStore() const { data: signer } = useSigner({ chainId: tx.parentChainId }) - const { updatePendingTransaction } = useTransactionHistory( - sanitizedAddress ?? address - ) + const { updatePendingTransaction } = useTransactionHistory({ + address: sanitizedAddress ?? address + }) const [isClaiming, setIsClaiming] = useState(false) const claim = useCallback(async () => { diff --git a/packages/arb-token-bridge-ui/src/hooks/useRedeemRetryable.ts b/packages/arb-token-bridge-ui/src/hooks/useRedeemRetryable.ts index 3a50f2ba23..eaa8ae7900 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useRedeemRetryable.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useRedeemRetryable.ts @@ -23,7 +23,9 @@ export function useRedeemRetryable( address: Address | undefined ): UseRedeemRetryableResult { const { data: signer } = useSigner({ chainId: tx.destinationChainId }) - const { updatePendingTransaction } = useTransactionHistory(address) + const { updatePendingTransaction } = useTransactionHistory({ + address + }) const destinationNetworkName = getNetworkName(tx.destinationChainId) diff --git a/packages/arb-token-bridge-ui/src/hooks/useRedeemTeleporter.ts b/packages/arb-token-bridge-ui/src/hooks/useRedeemTeleporter.ts index 20edd6715d..41bb8aeac3 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useRedeemTeleporter.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useRedeemTeleporter.ts @@ -160,7 +160,7 @@ export function useRedeemTeleporter( const { data: signer } = useSigner({ chainId: chainIdForRedeemingRetryable }) - const { updatePendingTransaction } = useTransactionHistory(address) + const { updatePendingTransaction } = useTransactionHistory({ address }) const redeemerNetworkName = getNetworkName(chainIdForRedeemingRetryable) diff --git a/packages/arb-token-bridge-ui/src/hooks/useTransactionHistory.ts b/packages/arb-token-bridge-ui/src/hooks/useTransactionHistory.ts index a4a1255595..141f444bb5 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useTransactionHistory.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useTransactionHistory.ts @@ -62,7 +62,7 @@ import { import { captureSentryErrorWithExtraData } from '../util/SentryUtils' import { useArbQueryParams } from './useArbQueryParams' -export type UseTransactionHistoryResult = { +type UsePartialTransactionHistoryResult = { transactions: MergedTransaction[] loading: boolean completed: boolean @@ -70,7 +70,6 @@ export type UseTransactionHistoryResult = { failedChainPairs: ChainPair[] pause: () => void resume: () => void - addPendingTransaction: (tx: MergedTransaction) => void updatePendingTransaction: (tx: MergedTransaction) => Promise } @@ -113,13 +112,30 @@ function sortByTimestampDescending(a: Transfer, b: Transfer) { return getTransactionTimestamp(a) > getTransactionTimestamp(b) ? -1 : 1 } -function getMultiChainFetchList(): ChainPair[] { +export function getMultiChainFetchList({ + core = true, + orbit = true +}: { + core?: boolean + orbit?: boolean +} = {}): ChainPair[] { return getChains().flatMap(chain => { // We only grab child chains because we don't want duplicates and we need the parent chain // Although the type is correct here we default to an empty array for custom networks backwards compatibility const childChainIds = getChildChainIds(chain) - const isParentChain = childChainIds.length > 0 + const filteredChildChainIds = childChainIds.filter(chainId => { + const isOrbitChain = isNetwork(chainId).isOrbitChain + if (core && !isOrbitChain) { + return true + } + if (orbit && isOrbitChain) { + return true + } + return false + }) + + const isParentChain = filteredChildChainIds.length > 0 if (!isParentChain) { // Skip non-parent chains @@ -127,7 +143,7 @@ function getMultiChainFetchList(): ChainPair[] { } // For each destination chain, map to an array of ChainPair objects - return childChainIds.map(childChainId => ({ + return filteredChildChainIds.map(childChainId => ({ parentChainId: chain.chainId, childChainId: childChainId })) @@ -235,7 +251,10 @@ function dedupeTransactions(txs: Transfer[]) { /** * Fetches transaction history only for deposits and withdrawals, without their statuses. */ -const useTransactionHistoryWithoutStatuses = (address: Address | undefined) => { +const useTransactionHistoryWithoutStatuses = ( + address: Address | undefined, + { forChains, ready }: { forChains: ChainPair[]; ready: boolean } +) => { const { chain } = useNetwork() const [isTestnetMode] = useIsTestnetMode() const { isSmartContractWallet, isLoading: isLoadingAccountType } = @@ -246,7 +265,16 @@ const useTransactionHistoryWithoutStatuses = (address: Address | undefined) => { // We need this because of Smart Contract Wallets const cctpTypeToFetch = useCallback( (chainPair: ChainPair): 'deposits' | 'withdrawals' | 'all' | undefined => { - if (isLoadingAccountType || !chain || !isTxHistoryEnabled) { + if ( + typeof forChains.find( + c => + c.parentChainId === chainPair.parentChainId && + c.childChainId === chainPair.childChainId + ) === 'undefined' + ) { + return undefined + } + if (isLoadingAccountType || !chain || !isTxHistoryEnabled || !ready) { return undefined } if (isSmartContractWallet) { @@ -341,7 +369,7 @@ const useTransactionHistoryWithoutStatuses = (address: Address | undefined) => { const fetcherFn = type === 'deposits' ? fetchDeposits : fetchWithdrawals return Promise.all( - getMultiChainFetchList() + forChains .filter(chainPair => { if (isSmartContractWallet) { // only fetch txs from the connected network @@ -434,14 +462,22 @@ const useTransactionHistoryWithoutStatuses = (address: Address | undefined) => { ) const shouldFetch = - address && chain && !isLoadingAccountType && isTxHistoryEnabled + ready && address && chain && !isLoadingAccountType && isTxHistoryEnabled const { data: depositsData, error: depositsError, isLoading: depositsLoading } = useSWRImmutable( - shouldFetch ? ['tx_list', 'deposits', address, isTestnetMode] : null, + shouldFetch + ? [ + 'tx_list', + 'deposits', + address, + isTestnetMode, + JSON.stringify(forChains) + ] + : null, () => fetcher('deposits') ) @@ -450,7 +486,15 @@ const useTransactionHistoryWithoutStatuses = (address: Address | undefined) => { error: withdrawalsError, isLoading: withdrawalsLoading } = useSWRImmutable( - shouldFetch ? ['tx_list', 'withdrawals', address, isTestnetMode] : null, + shouldFetch + ? [ + 'tx_list', + 'withdrawals', + address, + isTestnetMode, + JSON.stringify(forChains) + ] + : null, () => fetcher('withdrawals') ) @@ -473,15 +517,27 @@ const useTransactionHistoryWithoutStatuses = (address: Address | undefined) => { } } +type UsePartialTransactionHistoryProps = { + address: Address | undefined + // TODO: look for a solution to this. It's used for now so that useEffect that handles pagination runs only a single instance. + runFetcher?: boolean + fetchFor?: { + chains: ChainPair[] + } + ready?: boolean +} + /** * Maps additional info to previously fetches transaction history, starting with the earliest data. * This is done in small batches to safely meet RPC limits. */ -export const useTransactionHistory = ( - address: Address | undefined, - // TODO: look for a solution to this. It's used for now so that useEffect that handles pagination runs only a single instance. - { runFetcher = false } = {} -): UseTransactionHistoryResult => { +const usePartialTransactionHistory = ( + props: UsePartialTransactionHistoryProps +): UsePartialTransactionHistoryResult => { + const ready = typeof props.ready === 'undefined' ? true : props.ready + const forChains = props.fetchFor?.chains || getMultiChainFetchList() + const { address, runFetcher } = props + const [isTestnetMode] = useIsTestnetMode() const { chain } = useNetwork() const { isSmartContractWallet, isLoading: isLoadingAccountType } = @@ -501,7 +557,7 @@ export const useTransactionHistory = ( loading: isLoadingTxsWithoutStatus, error, failedChainPairs - } = useTransactionHistoryWithoutStatuses(address) + } = useTransactionHistoryWithoutStatuses(address, { forChains, ready }) const getCacheKey = useCallback( (pageNumber: number, prevPageTxs: MergedTransaction[]) => { @@ -512,11 +568,14 @@ export const useTransactionHistory = ( } } - return address && !isLoadingTxsWithoutStatus && !isLoadingAccountType + return ready && + address && + !isLoadingTxsWithoutStatus && + !isLoadingAccountType ? (['complete_tx_list', address, pageNumber, data] as const) : null }, - [address, isLoadingTxsWithoutStatus, data, isLoadingAccountType] + [ready, address, isLoadingTxsWithoutStatus, data, isLoadingAccountType] ) const depositsFromCache = useMemo(() => { @@ -526,7 +585,7 @@ export const useTransactionHistory = ( return getDepositsWithoutStatusesFromCache(address) .filter(tx => isNetwork(tx.parentChainId).isTestnet === isTestnetMode) .filter(tx => { - const chainPairExists = getMultiChainFetchList().some(chainPair => { + const chainPairExists = forChains.some(chainPair => { return ( chainPair.parentChainId === tx.parentChainId && chainPair.childChainId === tx.childChainId @@ -610,60 +669,18 @@ export const useTransactionHistory = ( typeof txPages !== 'undefined' && data.length === txPages.flat().length - // transfers initiated by the user during the current session - // we store it separately as there are a lot of side effects when mutating SWRInfinite - const { data: newTransactionsData, mutate: mutateNewTransactionsData } = - useSWRImmutable( - address ? ['new_tx_list', address] : null - ) - const transactions: MergedTransaction[] = useMemo(() => { - const txs = [...(newTransactionsData || []), ...(txPages || [])].flat() + const txs = (txPages || []).flat() // make sure txs are for the current account, we can have a mismatch when switching accounts for a bit return txs.filter(tx => [tx.sender?.toLowerCase(), tx.destination?.toLowerCase()].includes( address?.toLowerCase() ) ) - }, [newTransactionsData, txPages, address]) - - const addPendingTransaction = useCallback( - (tx: MergedTransaction) => { - if (!isTxPending(tx)) { - return - } - - mutateNewTransactionsData(currentNewTransactions => { - if (!currentNewTransactions) { - return [tx] - } - - return [tx, ...currentNewTransactions] - }) - }, - [mutateNewTransactionsData] - ) + }, [txPages, address]) const updateCachedTransaction = useCallback( (newTx: MergedTransaction) => { - // check if tx is a new transaction initiated by the user, and update it - const foundInNewTransactions = - typeof newTransactionsData?.find(oldTx => - isSameTransaction(oldTx, newTx) - ) !== 'undefined' - - if (foundInNewTransactions) { - // replace the existing tx with the new tx - mutateNewTransactionsData(txs => - txs?.map(oldTx => { - return { ...(isSameTransaction(oldTx, newTx) ? newTx : oldTx) } - }) - ) - return - } - - // tx not found in the new user initiated transaction list - // look in the paginated historical data mutateTxPages(prevTxPages => { if (!prevTxPages) { return @@ -706,7 +723,7 @@ export const useTransactionHistory = ( return newTxPages }, false) }, - [mutateNewTransactionsData, mutateTxPages, newTransactionsData] + [mutateTxPages] ) const updatePendingTransaction = useCallback( @@ -839,14 +856,13 @@ export const useTransactionHistory = ( if (isLoadingTxsWithoutStatus || error) { return { - transactions: newTransactionsData || [], + transactions: [], loading: isLoadingTxsWithoutStatus, error, failedChainPairs: [], completed: true, pause, resume, - addPendingTransaction, updatePendingTransaction } } @@ -859,6 +875,132 @@ export const useTransactionHistory = ( failedChainPairs, pause, resume, + updatePendingTransaction + } +} + +const useCurrentSessionTransactions = (address: Address | undefined) => { + const { data: currentSessionTransactions, mutate } = useSWRImmutable< + MergedTransaction[] + >(address ? [address, 'current_session_transactions'] : null) + + const addPendingTransaction = useCallback( + (tx: MergedTransaction) => { + if (!isTxPending(tx)) { + return + } + + mutate(transactions => { + if (!transactions) { + return [tx] + } + + return [tx, ...transactions] + }) + }, + [mutate] + ) + + return { + currentSessionTransactions: currentSessionTransactions || [], + addPendingTransaction + } +} + +type UseTransactionHistoryProps = Omit< + UsePartialTransactionHistoryProps, + 'forChains' | 'ready' +> + +export type TransactionHistoryLoadingStates = { + any: boolean + core: boolean + orbit: boolean +} + +export type UseTransactionHistoryResult = Omit< + UsePartialTransactionHistoryResult, + 'loading' +> & { + addPendingTransaction: (tx: MergedTransaction) => void + loading: TransactionHistoryLoadingStates +} + +export const useTransactionHistory = ( + props: UseTransactionHistoryProps +): UseTransactionHistoryResult => { + const { currentSessionTransactions, addPendingTransaction } = + useCurrentSessionTransactions(props.address) + + const { + transactions: coreTransactions, + completed: coreHistoryCompleted, + loading: coreHistoryLoading, + ...restCoreResults + } = usePartialTransactionHistory({ + ...props, + fetchFor: { + chains: getMultiChainFetchList({ core: true, orbit: false }) + }, + ready: true + }) + + const { + transactions: orbitTransactions, + completed: orbitHistoryCompleted, + loading: orbitHistoryLoading, + ...restOrbitResults + } = usePartialTransactionHistory({ + ...props, + fetchFor: { + chains: getMultiChainFetchList({ core: false, orbit: true }) + }, + ready: coreHistoryCompleted + }) + + const completed = coreHistoryCompleted && orbitHistoryCompleted + + const error = restCoreResults.error || restOrbitResults.error + + const failedChainPairs = [ + ...new Set([ + ...(restCoreResults.failedChainPairs || []), + ...(restOrbitResults.failedChainPairs || []) + ]) + ] + + const updatePendingTransaction = useCallback( + async (tx: MergedTransaction) => { + await restCoreResults.updatePendingTransaction(tx) + await restOrbitResults.updatePendingTransaction(tx) + }, + [restCoreResults, restOrbitResults] + ) + + const rest = useMemo(() => { + // Return all remaining relevant props, returns props for the currently loaded data + // Because we need its pause/resume methods etc + if (coreHistoryCompleted) { + return restOrbitResults + } + return restCoreResults + }, [coreHistoryCompleted, restOrbitResults, restCoreResults]) + + return { + ...rest, + transactions: [ + ...currentSessionTransactions, + ...coreTransactions, + ...orbitTransactions + ].sort(sortByTimestampDescending), + loading: { + any: coreHistoryLoading || orbitHistoryLoading, + core: coreHistoryLoading, + orbit: orbitHistoryLoading + }, + completed, + error, + failedChainPairs, addPendingTransaction, updatePendingTransaction } diff --git a/packages/arb-token-bridge-ui/src/state/cctpState.ts b/packages/arb-token-bridge-ui/src/state/cctpState.ts index 523aa80b73..61bb884cef 100644 --- a/packages/arb-token-bridge-ui/src/state/cctpState.ts +++ b/packages/arb-token-bridge-ui/src/state/cctpState.ts @@ -501,7 +501,9 @@ export function useCctpFetching({ export function useClaimCctp(tx: MergedTransaction) { const { address } = useAccount() - const { updatePendingTransaction } = useTransactionHistory(address) + const { updatePendingTransaction } = useTransactionHistory({ + address + }) const [isClaiming, setIsClaiming] = useState(false) const { waitForAttestation, receiveMessage } = getCctpUtils({ sourceChainId: tx.cctpData?.sourceChainId