diff --git a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableClaimableRow.tsx b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableClaimableRow.tsx
index ab14ecc45d..a231823a13 100644
--- a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableClaimableRow.tsx
+++ b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableClaimableRow.tsx
@@ -16,11 +16,7 @@ import {
isNetwork
} from '../../util/networks'
import { InformationCircleIcon } from '@heroicons/react/24/outline'
-import {
- isCustomDestinationAddressTx,
- findMatchingL1TxForWithdrawal,
- isPending
-} from '../../state/app/utils'
+import { isCustomDestinationAddressTx, isPending } from '../../state/app/utils'
import { TokenIcon, TransactionDateTime } from './TransactionHistoryTable'
import { formatAmount } from '../../util/NumberUtils'
import { sanitizeTokenSymbol } from '../../util/TokenUtils'
@@ -29,6 +25,7 @@ import { useRemainingTime } from '../../state/cctpState'
import { useChainLayers } from '../../hooks/useChainLayers'
import { getWagmiChain } from '../../util/wagmi/getWagmiChain'
import { NetworkImage } from '../common/NetworkImage'
+import { getWithdrawalClaimParentChainTxDetails } from './helpers'
type CommonProps = {
tx: MergedTransaction
@@ -37,9 +34,9 @@ type CommonProps = {
function ClaimableRowStatus({ tx }: CommonProps) {
const { parentLayer, layer } = useChainLayers()
- const matchingL1Tx = tx.isCctp
+ const matchingL1TxId = tx.isCctp
? tx.cctpData?.receiveMessageTransactionHash
- : findMatchingL1TxForWithdrawal(tx)
+ : getWithdrawalClaimParentChainTxDetails(tx)?.txId
switch (tx.status) {
case 'pending':
@@ -96,7 +93,7 @@ function ClaimableRowStatus({ tx }: CommonProps) {
)
case 'Executed': {
- if (typeof matchingL1Tx === 'undefined') {
+ if (typeof matchingL1TxId === 'undefined') {
return (
{layer} Transaction time}>
@@ -213,10 +208,10 @@ function ClaimableRowTime({ tx }: CommonProps) {
{layer} Transaction Time}>
- {claimedTx?.createdAt && (
+ {claimedTxTimestamp && (
{parentLayer} Transaction Time}>
-
+
)}
@@ -231,13 +226,11 @@ function ClaimedTxInfo({ tx, isSourceChainArbitrum }: CommonProps) {
const isExecuted = tx.status === 'Executed'
const isBeingClaimed = tx.status === 'Confirmed' && tx.resolvedAt
- const claimedTx = tx.isCctp
- ? {
- txId: tx.cctpData?.receiveMessageTransactionHash
- }
- : findMatchingL1TxForWithdrawal(tx)
+ const claimedTxId = tx.isCctp
+ ? tx.cctpData?.receiveMessageTransactionHash
+ : getWithdrawalClaimParentChainTxDetails(tx)?.txId
- if (!claimedTx?.txId) {
+ if (!claimedTxId) {
return (
To
@@ -257,10 +250,10 @@ function ClaimedTxInfo({ tx, isSourceChainArbitrum }: CommonProps) {
{getNetworkName(toNetworkId)}:{' '}
- {shortenTxHash(claimedTx.txId)}
+ {shortenTxHash(claimedTxId)}
)
diff --git a/packages/arb-token-bridge-ui/src/components/TransactionHistory/helpers.ts b/packages/arb-token-bridge-ui/src/components/TransactionHistory/helpers.ts
index ba97adb68f..f26eb1fbf1 100644
--- a/packages/arb-token-bridge-ui/src/components/TransactionHistory/helpers.ts
+++ b/packages/arb-token-bridge-ui/src/components/TransactionHistory/helpers.ts
@@ -15,7 +15,7 @@ import {
WithdrawalStatus
} from '../../state/app/state'
import { ChainId, getBlockTime, isNetwork, rpcURLs } from '../../util/networks'
-import { isCctpTransfer } from '../../hooks/useTransactionHistory'
+import { Deposit, isCctpTransfer } from '../../hooks/useTransactionHistory'
import { getWagmiChain } from '../../util/wagmi/getWagmiChain'
import { getL1ToL2MessageDataFromL1TxHash } from '../../util/deposits/helpers'
import { AssetType } from '../../hooks/arbTokenBridge.types'
@@ -23,6 +23,10 @@ import { getDepositStatus } from '../../state/app/utils'
import { getBlockBeforeConfirmation } from '../../state/cctpState'
import { getAttestationHashAndMessageFromReceipt } from '../../util/cctp/getAttestationHashAndMessageFromReceipt'
+const PARENT_CHAIN_TX_DETAILS_OF_CLAIM_TX =
+ 'arbitrum:bridge:claim:parent:tx:details'
+const DEPOSITS_LOCAL_STORAGE_KEY = 'arbitrum:bridge:deposits'
+
export enum StatusLabel {
PENDING = 'Pending',
CLAIMABLE = 'Claimable',
@@ -101,13 +105,21 @@ export function getProvider(chainId: ChainId) {
}
export function isSameTransaction(
- tx_1: MergedTransaction,
- tx_2: MergedTransaction
+ txDetails_1: {
+ txId: string
+ parentChainId: ChainId
+ childChainId: ChainId
+ },
+ txDetails_2: {
+ txId: string
+ parentChainId: ChainId
+ childChainId: ChainId
+ }
) {
return (
- tx_1.txId === tx_2.txId &&
- tx_1.parentChainId === tx_2.parentChainId &&
- tx_1.childChainId === tx_2.childChainId
+ txDetails_1.txId === txDetails_2.txId &&
+ txDetails_1.parentChainId === txDetails_2.parentChainId &&
+ txDetails_1.childChainId === txDetails_2.childChainId
)
}
@@ -133,6 +145,99 @@ function getWithdrawalStatusFromReceipt(
}
}
+export function getDepositsWithoutStatusesFromCache(): Deposit[] {
+ return JSON.parse(
+ localStorage.getItem(DEPOSITS_LOCAL_STORAGE_KEY) ?? '[]'
+ ) as Deposit[]
+}
+
+/**
+ * Cache deposits from event logs. We don't fetch these so we need to store them locally.
+ *
+ * @param {MergedTransaction} tx - Deposit from event logs to be cached.
+ */
+export function addDepositToCache(tx: Deposit) {
+ if (tx.direction !== 'deposit') {
+ return
+ }
+
+ const cachedDeposits = getDepositsWithoutStatusesFromCache()
+
+ const foundInCache = cachedDeposits.find(cachedTx =>
+ isSameTransaction(
+ { ...cachedTx, txId: cachedTx.txID },
+ { ...tx, txId: tx.txID }
+ )
+ )
+
+ if (foundInCache) {
+ return
+ }
+
+ const newCachedDeposits = [tx, ...cachedDeposits]
+
+ localStorage.setItem(
+ DEPOSITS_LOCAL_STORAGE_KEY,
+ JSON.stringify(newCachedDeposits)
+ )
+}
+
+/**
+ * Cache parent chain tx details when claiming. This is the chain the funds were claimed on. We store locally because we don't have access to this tx from the child chain tx data.
+ *
+ * @param {MergedTransaction} tx - Transaction that initiated the withdrawal (child chain transaction).
+ * @param {string} parentChainTxId - Transaction ID of the claim transaction (parent chain transaction ID).
+ */
+export function setParentChainTxDetailsOfWithdrawalClaimTx(
+ tx: MergedTransaction,
+ parentChainTxId: string
+) {
+ const key = `${tx.parentChainId}-${tx.childChainId}-${tx.txId}`
+
+ const cachedClaimParentChainTxId = JSON.parse(
+ localStorage.getItem(PARENT_CHAIN_TX_DETAILS_OF_CLAIM_TX) ?? '{}'
+ )
+
+ if (key in cachedClaimParentChainTxId) {
+ // already set
+ return
+ }
+
+ localStorage.setItem(
+ PARENT_CHAIN_TX_DETAILS_OF_CLAIM_TX,
+ JSON.stringify({
+ ...cachedClaimParentChainTxId,
+ [key]: {
+ txId: parentChainTxId,
+ timestamp: dayjs().valueOf()
+ }
+ })
+ )
+}
+
+export function getWithdrawalClaimParentChainTxDetails(
+ tx: MergedTransaction
+): { txId: string; timestamp: number } | undefined {
+ if (!tx.isWithdrawal || tx.isCctp) {
+ return undefined
+ }
+
+ const key = `${tx.parentChainId}-${tx.childChainId}-${tx.txId}`
+
+ const cachedClaimParentChainTxDetails = (
+ JSON.parse(
+ localStorage.getItem(PARENT_CHAIN_TX_DETAILS_OF_CLAIM_TX) ?? '{}'
+ ) as {
+ [key in string]: {
+ txId: string
+ timestamp: number
+ }
+ }
+ )[key]
+
+ return cachedClaimParentChainTxDetails
+}
+
export async function getUpdatedEthDeposit(
tx: MergedTransaction
): Promise {
diff --git a/packages/arb-token-bridge-ui/src/hooks/arbTokenBridge.types.ts b/packages/arb-token-bridge-ui/src/hooks/arbTokenBridge.types.ts
index f76249e7b9..80d24dcfa2 100644
--- a/packages/arb-token-bridge-ui/src/hooks/arbTokenBridge.types.ts
+++ b/packages/arb-token-bridge-ui/src/hooks/arbTokenBridge.types.ts
@@ -97,7 +97,7 @@ export type WithdrawalInitiated = EventArgs & {
txHash: string
timestamp?: BigNumber
direction: 'deposit' | 'withdrawal'
- source: 'subgraph' | 'event_logs'
+ source: 'subgraph' | 'event_logs' | 'local_storage_cache'
parentChainId: number
childChainId: number
}
diff --git a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts
index 3eb1f24026..b33c427a6c 100644
--- a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts
+++ b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts
@@ -46,6 +46,7 @@ import { useUpdateUSDCBalances } from './CCTP/useUpdateUSDCBalances'
import { useNativeCurrency } from './useNativeCurrency'
import { useTransactionHistory } from './useTransactionHistory'
import { DepositStatus, WithdrawalStatus } from '../state/app/state'
+import { addDepositToCache } from '../components/TransactionHistory/helpers'
export const wait = (ms = 0) => {
return new Promise(res => setTimeout(res, ms))
@@ -188,6 +189,8 @@ export const useArbTokenBridge = (
}
const ethBridger = await EthBridger.fromProvider(l2.provider)
+ const parentChainBlockTimestamp = (await l1.provider.getBlock('latest'))
+ .timestamp
let tx: L1EthDepositTransaction
@@ -212,7 +215,7 @@ export const useArbTokenBridge = (
destination: walletAddress,
direction: 'deposit-l1',
status: 'pending',
- createdAt: dayjs().valueOf(),
+ createdAt: parentChainBlockTimestamp * 1_000,
resolvedAt: null,
txId: tx.hash,
asset: nativeCurrency.symbol,
@@ -227,6 +230,25 @@ export const useArbTokenBridge = (
childChainId: Number(l2NetworkID)
})
+ addDepositToCache({
+ sender: walletAddress,
+ destination: walletAddress,
+ status: 'pending',
+ txID: tx.hash,
+ assetName: nativeCurrency.symbol,
+ assetType: AssetType.ETH,
+ l1NetworkID,
+ l2NetworkID,
+ value: utils.formatUnits(amount, nativeCurrency.decimals),
+ parentChainId: Number(l1NetworkID),
+ childChainId: Number(l2NetworkID),
+ direction: 'deposit',
+ type: 'deposit-l1',
+ source: 'local_storage_cache',
+ timestampCreated: String(parentChainBlockTimestamp),
+ nonce: tx.nonce
+ })
+
const receipt = await tx.wait()
if (txLifecycle?.onTxConfirm) {
@@ -398,6 +420,8 @@ export const useArbTokenBridge = (
return
}
const erc20Bridger = await Erc20Bridger.fromProvider(l2.provider)
+ const parentChainBlockTimestamp = (await l1.provider.getBlock('latest'))
+ .timestamp
try {
const { symbol, decimals } = await fetchErc20Data({
@@ -428,7 +452,7 @@ export const useArbTokenBridge = (
destination: destinationAddress ?? walletAddress,
direction: 'deposit-l1',
status: 'pending',
- createdAt: dayjs().valueOf(),
+ createdAt: parentChainBlockTimestamp * 1_000,
resolvedAt: null,
txId: tx.hash,
asset: symbol,
@@ -443,6 +467,25 @@ export const useArbTokenBridge = (
childChainId: Number(l2NetworkID)
})
+ addDepositToCache({
+ sender: walletAddress,
+ destination: destinationAddress ?? walletAddress,
+ status: 'pending',
+ txID: tx.hash,
+ assetName: symbol,
+ assetType: AssetType.ERC20,
+ l1NetworkID,
+ l2NetworkID,
+ value: utils.formatUnits(amount, decimals),
+ parentChainId: Number(l1NetworkID),
+ childChainId: Number(l2NetworkID),
+ direction: 'deposit',
+ type: 'deposit-l1',
+ source: 'local_storage_cache',
+ timestampCreated: String(parentChainBlockTimestamp),
+ nonce: tx.nonce
+ })
+
const receipt = await tx.wait()
if (txLifecycle?.onTxConfirm) {
diff --git a/packages/arb-token-bridge-ui/src/hooks/useClaimWithdrawal.ts b/packages/arb-token-bridge-ui/src/hooks/useClaimWithdrawal.ts
index 8396a8b889..c4ae3a54f1 100644
--- a/packages/arb-token-bridge-ui/src/hooks/useClaimWithdrawal.ts
+++ b/packages/arb-token-bridge-ui/src/hooks/useClaimWithdrawal.ts
@@ -7,7 +7,10 @@ import { MergedTransaction, WithdrawalStatus } from '../state/app/state'
import { isUserRejectedError } from '../util/isUserRejectedError'
import { errorToast } from '../components/common/atoms/Toast'
import { AssetType, L2ToL1EventResultPlus } from './arbTokenBridge.types'
-import { getProvider } from '../components/TransactionHistory/helpers'
+import {
+ getProvider,
+ setParentChainTxDetailsOfWithdrawalClaimTx
+} from '../components/TransactionHistory/helpers'
import { L2TransactionReceipt } from '@arbitrum/sdk'
import { ContractReceipt, utils } from 'ethers'
import { useTransactionHistory } from './useTransactionHistory'
@@ -109,12 +112,17 @@ export function useClaimWithdrawal(): UseClaimWithdrawalResult {
}
const isSuccess = (res as ContractReceipt).status === 1
+ const txHash = (res as ContractReceipt).transactionHash
updatePendingTransaction({
...tx,
status: isSuccess ? WithdrawalStatus.EXECUTED : WithdrawalStatus.FAILURE,
resolvedAt: isSuccess ? dayjs().valueOf() : null
})
+
+ if (isSuccess) {
+ setParentChainTxDetailsOfWithdrawalClaimTx(tx, txHash)
+ }
}
return { claim, isClaiming }
diff --git a/packages/arb-token-bridge-ui/src/hooks/useTransactionHistory.ts b/packages/arb-token-bridge-ui/src/hooks/useTransactionHistory.ts
index 748f2a880a..7f22011c90 100644
--- a/packages/arb-token-bridge-ui/src/hooks/useTransactionHistory.ts
+++ b/packages/arb-token-bridge-ui/src/hooks/useTransactionHistory.ts
@@ -33,6 +33,7 @@ import { FetchWithdrawalsFromSubgraphResult } from '../util/withdrawals/fetchWit
import { updateAdditionalDepositData } from '../util/deposits/helpers'
import { useCctpFetching } from '../state/cctpState'
import {
+ getDepositsWithoutStatusesFromCache,
getProvider,
getUpdatedCctpTransfer,
getUpdatedEthDeposit,
@@ -57,6 +58,8 @@ export type TransactionHistoryParams = {
updatePendingTransaction: (tx: MergedTransaction) => void
}
+export type ChainPair = { parentChain: ChainId; chain: ChainId }
+
export type Deposit = Transaction
export type Withdrawal =
@@ -89,7 +92,7 @@ function sortByTimestampDescending(a: Transfer, b: Transfer) {
: 1
}
-const multiChainFetchList: { parentChain: ChainId; chain: ChainId }[] = [
+const multiChainFetchList: ChainPair[] = [
{
parentChain: ChainId.Ethereum,
chain: ChainId.ArbitrumOne
@@ -194,6 +197,22 @@ function getTransactionsMapKey(tx: MergedTransaction) {
return `${tx.parentChainId}-${tx.childChainId}-${tx.txId}`
}
+function getTxIdFromTransaction(tx: Transfer) {
+ if (isCctpTransfer(tx)) {
+ return tx.txId
+ }
+ if (isDeposit(tx)) {
+ return tx.txID
+ }
+ if (isWithdrawalFromSubgraph(tx)) {
+ return tx.l2TxHash
+ }
+ if (isTokenWithdrawal(tx)) {
+ return tx.txHash
+ }
+ return tx.l2TxHash
+}
+
/**
* Fetches transaction history only for deposits and withdrawals, without their statuses.
*/
@@ -285,16 +304,41 @@ const useTransactionHistoryWithoutStatuses = (
() => fetcher('withdrawals')
)
- const deposits = (depositsData || []).flat()
+ const deposits = [
+ ...getDepositsWithoutStatusesFromCache().filter(tx =>
+ isTestnetMode ? true : !isNetwork(tx.parentChainId).isTestnet
+ ),
+ (depositsData || []).flat()
+ ]
+
const withdrawals = (withdrawalsData || []).flat()
// merge deposits and withdrawals and sort them by date
- const transactions = [...deposits, ...withdrawals, ...combinedCctpTransfers]
- .flat()
- .sort(sortByTimestampDescending)
+ const transactions = [
+ ...deposits,
+ ...withdrawals,
+ ...combinedCctpTransfers
+ ].flat()
+
+ // duplicates may occur when txs are taken from the local storage
+ // we don't use Set because it wouldn't dedupe objects with different reference (we fetch them from different sources)
+ const dedupedTransactions = useMemo(
+ () =>
+ Array.from(
+ new Map(
+ transactions.map(tx => [
+ `${tx.parentChainId}-${tx.childChainId}-${getTxIdFromTransaction(
+ tx
+ )?.toLowerCase()}}`,
+ tx
+ ])
+ ).values()
+ ).sort(sortByTimestampDescending),
+ [transactions]
+ )
return {
- data: transactions,
+ data: dedupedTransactions,
loading: depositsLoading || withdrawalsLoading || cctpLoading,
error: depositsError ?? withdrawalsError
}
diff --git a/packages/arb-token-bridge-ui/src/hooks/useTransactions.ts b/packages/arb-token-bridge-ui/src/hooks/useTransactions.ts
index 873fee9d92..3c3f274211 100644
--- a/packages/arb-token-bridge-ui/src/hooks/useTransactions.ts
+++ b/packages/arb-token-bridge-ui/src/hooks/useTransactions.ts
@@ -102,9 +102,10 @@ type TransactionBase = {
export interface Transaction extends TransactionBase {
txID: string
direction: 'deposit' | 'withdrawal'
- source: 'subgraph' | 'event_logs'
+ source: 'subgraph' | 'event_logs' | 'local_storage_cache'
parentChainId: number
childChainId: number
+ nonce?: number
}
export interface NewTransaction extends TransactionBase {
diff --git a/packages/arb-token-bridge-ui/src/state/app/utils.ts b/packages/arb-token-bridge-ui/src/state/app/utils.ts
index 62c4df2bda..e5f7aa66f5 100644
--- a/packages/arb-token-bridge-ui/src/state/app/utils.ts
+++ b/packages/arb-token-bridge-ui/src/state/app/utils.ts
@@ -240,30 +240,3 @@ export const getStandardizedTime = (standardizedTimestamp: number) => {
export const getStandardizedDate = (standardizedTimestamp: number) => {
return dayjs(standardizedTimestamp).format(TX_DATE_FORMAT) // dayjs timestamp -> date
}
-
-export const findMatchingL1TxForWithdrawal = (
- withdrawalTxn: MergedTransaction
-) => {
- // finds the corresponding L1 transaction for withdrawal
-
- const cachedTransactions: Transaction[] = JSON.parse(
- window.localStorage.getItem('arbTransactions') || '[]'
- )
- const outboxTransactions = cachedTransactions
- .filter(tx => tx.type === 'outbox')
- .map(transformDeposit)
-
- return outboxTransactions.find(_tx => {
- const l2ToL1MsgData = _tx.l2ToL1MsgData
-
- if (!(l2ToL1MsgData?.uniqueId && withdrawalTxn?.uniqueId)) {
- return false
- }
-
- // To get rid of Proxy
- const txUniqueId = BigNumber.from(withdrawalTxn.uniqueId)
- const _txUniqueId = BigNumber.from(l2ToL1MsgData.uniqueId)
-
- return txUniqueId.eq(_txUniqueId)
- })
-}
diff --git a/packages/arb-token-bridge-ui/src/util/withdrawals/fetchWithdrawalsFromSubgraph.ts b/packages/arb-token-bridge-ui/src/util/withdrawals/fetchWithdrawalsFromSubgraph.ts
index eff89f9d01..9b649ad3ad 100644
--- a/packages/arb-token-bridge-ui/src/util/withdrawals/fetchWithdrawalsFromSubgraph.ts
+++ b/packages/arb-token-bridge-ui/src/util/withdrawals/fetchWithdrawalsFromSubgraph.ts
@@ -15,7 +15,7 @@ export type FetchWithdrawalsFromSubgraphResult = {
l2TxHash: string
l2BlockNum: string
direction: 'deposit' | 'withdrawal'
- source: 'subgraph' | 'event_logs'
+ source: 'subgraph'
parentChainId: number
childChainId: number
}
diff --git a/packages/arb-token-bridge-ui/src/util/withdrawals/helpers.ts b/packages/arb-token-bridge-ui/src/util/withdrawals/helpers.ts
index eb35961bc6..5f08c1b1bb 100644
--- a/packages/arb-token-bridge-ui/src/util/withdrawals/helpers.ts
+++ b/packages/arb-token-bridge-ui/src/util/withdrawals/helpers.ts
@@ -24,7 +24,7 @@ export type EthWithdrawal = L2ToL1EventResult & {
l2TxHash?: string
transactionHash?: string
direction: 'deposit' | 'withdrawal'
- source: 'subgraph' | 'event_logs'
+ source: 'subgraph' | 'event_logs' | 'local_storage_cache'
parentChainId: number
childChainId: number
}