Skip to content

Commit

Permalink
feat: new tx history - cache tx details (#1371)
Browse files Browse the repository at this point in the history
  • Loading branch information
brtkx authored Dec 21, 2023
1 parent 8af4f19 commit d5a768b
Show file tree
Hide file tree
Showing 10 changed files with 237 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand All @@ -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':
Expand Down Expand Up @@ -96,7 +93,7 @@ function ClaimableRowStatus({ tx }: CommonProps) {
)

case 'Executed': {
if (typeof matchingL1Tx === 'undefined') {
if (typeof matchingL1TxId === 'undefined') {
return (
<div className="flex flex-col space-y-1">
<StatusBadge
Expand Down Expand Up @@ -187,13 +184,11 @@ function ClaimableRowTime({ tx }: CommonProps) {
)
}

const claimedTx = tx.isCctp
? {
createdAt: tx.cctpData?.receiveMessageTimestamp
}
: findMatchingL1TxForWithdrawal(tx)
const claimedTxTimestamp = tx.isCctp
? tx.cctpData?.receiveMessageTimestamp
: getWithdrawalClaimParentChainTxDetails(tx)?.timestamp

if (typeof claimedTx === 'undefined') {
if (typeof claimedTxTimestamp === 'undefined') {
return (
<div className="flex flex-col space-y-3">
<Tooltip content={<span>{layer} Transaction time</span>}>
Expand All @@ -213,10 +208,10 @@ function ClaimableRowTime({ tx }: CommonProps) {
<Tooltip content={<span>{layer} Transaction Time</span>}>
<TransactionDateTime standardizedDate={tx.createdAt} />
</Tooltip>
{claimedTx?.createdAt && (
{claimedTxTimestamp && (
<Tooltip content={<span>{parentLayer} Transaction Time</span>}>
<span className="whitespace-nowrap">
<TransactionDateTime standardizedDate={claimedTx?.createdAt} />
<TransactionDateTime standardizedDate={claimedTxTimestamp} />
</span>
</Tooltip>
)}
Expand All @@ -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 (
<span className="flex flex-nowrap items-center gap-1 whitespace-nowrap text-dark">
<span className="w-8 rounded-md pr-2 text-xs text-dark">To</span>
Expand All @@ -257,10 +250,10 @@ function ClaimedTxInfo({ tx, isSourceChainArbitrum }: CommonProps) {
<NetworkImage chainId={toNetworkId} />
{getNetworkName(toNetworkId)}:{' '}
<ExternalLink
href={`${getExplorerUrl(toNetworkId)}/tx/${claimedTx.txId}`}
href={`${getExplorerUrl(toNetworkId)}/tx/${claimedTxId}`}
className="arb-hover text-blue-link"
>
{shortenTxHash(claimedTx.txId)}
{shortenTxHash(claimedTxId)}
</ExternalLink>
</span>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,18 @@ 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'
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',
Expand Down Expand Up @@ -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
)
}

Expand All @@ -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<MergedTransaction> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export type WithdrawalInitiated = EventArgs<WithdrawalInitiatedEvent> & {
txHash: string
timestamp?: BigNumber
direction: 'deposit' | 'withdrawal'
source: 'subgraph' | 'event_logs'
source: 'subgraph' | 'event_logs' | 'local_storage_cache'
parentChainId: number
childChainId: number
}
Expand Down
47 changes: 45 additions & 2 deletions packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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

Expand All @@ -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,
Expand All @@ -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) {
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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,
Expand All @@ -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) {
Expand Down
10 changes: 9 additions & 1 deletion packages/arb-token-bridge-ui/src/hooks/useClaimWithdrawal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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 }
Expand Down
Loading

0 comments on commit d5a768b

Please sign in to comment.