From 417283c6f5d0173b29335234bf9d1f8cd2a56a3a Mon Sep 17 00:00:00 2001 From: Fionna Chan <13184582+fionnachan@users.noreply.github.com> Date: Fri, 29 Nov 2024 21:22:38 +0800 Subject: [PATCH] feat: add wallet address field to tx history (#2080) --- .../EmptyTransactionHistory.tsx | 6 +- .../TransactionHistory/TransactionHistory.tsx | 136 +--------------- .../TransactionHistorySearchBar.tsx | 101 ++++++++++++ .../TransactionHistorySearchResults.tsx | 146 ++++++++++++++++++ .../TransactionHistoryTable.tsx | 8 +- .../TransactionsTableDetails.tsx | 19 +-- .../TransactionsTableDetailsSteps.tsx | 22 +-- ...ransactionsTableDetailsTeleporterSteps.tsx | 16 +- .../TransactionsTableRow.tsx | 3 - .../TransactionsTableRowAction.tsx | 11 +- 10 files changed, 280 insertions(+), 188 deletions(-) create mode 100644 packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistorySearchBar.tsx create mode 100644 packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistorySearchResults.tsx diff --git a/packages/arb-token-bridge-ui/src/components/TransactionHistory/EmptyTransactionHistory.tsx b/packages/arb-token-bridge-ui/src/components/TransactionHistory/EmptyTransactionHistory.tsx index 6c0d3e1b69..8a93052271 100644 --- a/packages/arb-token-bridge-ui/src/components/TransactionHistory/EmptyTransactionHistory.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransactionHistory/EmptyTransactionHistory.tsx @@ -52,5 +52,9 @@ export const EmptyTransactionHistory = ({ ) } - return Looks like no transactions here yet. + return ( + + No {tabType} transactions. + + ) } diff --git a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistory.tsx b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistory.tsx index f1dbc7bb2e..b1166da64a 100644 --- a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistory.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistory.tsx @@ -1,49 +1,8 @@ -import dayjs from 'dayjs' -import { useEffect, useMemo } from 'react' -import { Tab } from '@headlessui/react' import { create } from 'zustand' -import { useAccount } from 'wagmi' -import { TransactionHistoryTable } from './TransactionHistoryTable' -import { TransactionStatusInfo } from '../TransactionHistory/TransactionStatusInfo' -import { - isTxClaimable, - isTxCompleted, - isTxExpired, - isTxFailed, - isTxPending -} from './helpers' import { MergedTransaction } from '../../state/app/state' -import { TabButton } from '../common/Tab' -import { TransactionsTableDetails } from './TransactionsTableDetails' -import { useTransactionHistory } from '../../hooks/useTransactionHistory' - -function useTransactionHistoryUpdater() { - const { address } = useAccount() - - const transactionHistoryProps = useTransactionHistory(address, { - runFetcher: true - }) - - const { transactions, updatePendingTransaction } = transactionHistoryProps - - const pendingTransactions = useMemo(() => { - return transactions.filter(isTxPending) - }, [transactions]) - - useEffect(() => { - const interval = setInterval(() => { - pendingTransactions.forEach(updatePendingTransaction) - }, 10_000) - - return () => clearInterval(interval) - }, [pendingTransactions, updatePendingTransaction]) - - return transactionHistoryProps -} - -const tabClasses = - 'text-white px-3 mr-2 border-b-2 ui-selected:border-white ui-not-selected:border-transparent ui-not-selected:text-white/80 arb-hover' +import { TransactionHistorySearchBar } from './TransactionHistorySearchBar' +import { TransactionHistorySearchResults } from './TransactionHistorySearchResults' type TxDetailsStore = { tx: MergedTransaction | null @@ -72,98 +31,11 @@ export const useTxDetailsStore = create(set => ({ })) export const TransactionHistory = () => { - const { address } = useAccount() - const props = useTransactionHistoryUpdater() - const { transactions } = props - - const oldestTxTimeAgoString = useMemo(() => { - return dayjs(transactions[transactions.length - 1]?.createdAt).toNow(true) - }, [transactions]) - - const groupedTransactions = useMemo( - () => - transactions.reduce( - (acc, tx) => { - if (isTxCompleted(tx) || isTxExpired(tx)) { - acc.settled.push(tx) - } - if (isTxPending(tx)) { - acc.pending.push(tx) - } - if (isTxClaimable(tx)) { - acc.claimable.push(tx) - } - if (isTxFailed(tx)) { - acc.failed.push(tx) - } - return acc - }, - { - settled: [] as MergedTransaction[], - pending: [] as MergedTransaction[], - claimable: [] as MergedTransaction[], - failed: [] as MergedTransaction[] - } - ), - [transactions] - ) - - const pendingTransactions = [ - ...groupedTransactions.failed, - ...groupedTransactions.pending, - ...groupedTransactions.claimable - ] - - const settledTransactions = groupedTransactions.settled - return (
-
- -
- - - - - Pending transactions - - - Settled transactions - - + - - - - - - - - - - +
) } diff --git a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistorySearchBar.tsx b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistorySearchBar.tsx new file mode 100644 index 0000000000..f6486fcc35 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistorySearchBar.tsx @@ -0,0 +1,101 @@ +import { create } from 'zustand' +import { isAddress } from 'ethers/lib/utils.js' +import { Address, useAccount } from 'wagmi' +import { useCallback, useEffect } from 'react' +import { MagnifyingGlassIcon } from '@heroicons/react/24/outline' +import { twMerge } from 'tailwind-merge' + +import { Button } from '../common/Button' + +export enum TransactionHistorySearchError { + INVALID_ADDRESS = 'That doesn’t seem to be a valid address, please try again.' +} + +type TransactionHistoryAddressStore = { + address: string + sanitizedAddress: Address | undefined + searchError: TransactionHistorySearchError | undefined + setAddress: (address: string) => void + setSanitizedAddress: (address: string) => void + setSearchError: (error: TransactionHistorySearchError | undefined) => void +} + +export const useTransactionHistoryAddressStore = + create(set => ({ + address: '', + sanitizedAddress: undefined, + setAddress: (address: string) => set({ address }), + setSanitizedAddress: (address: string) => { + if (isAddress(address)) { + set({ sanitizedAddress: address }) + } + }, + searchError: undefined, + setSearchError: (error: TransactionHistorySearchError | undefined) => + set({ searchError: error }) + })) + +export function TransactionHistorySearchBar() { + const { address, setAddress, setSanitizedAddress, setSearchError } = + useTransactionHistoryAddressStore() + const { address: connectedAddress } = useAccount() + + useEffect(() => { + if (address === '' && connectedAddress) { + setSanitizedAddress(connectedAddress) + setSearchError(undefined) + } + }, [address, connectedAddress, setSanitizedAddress, setSearchError]) + + const searchTxForAddress = useCallback(() => { + if (address === '') { + return + } + + if (!isAddress(address)) { + setSearchError(TransactionHistorySearchError.INVALID_ADDRESS) + return + } + + setSanitizedAddress(address) + setSearchError(undefined) + }, [address, setSanitizedAddress, setSearchError]) + + return ( +
+
event.preventDefault()} + > + + setAddress(event.target.value)} + inputMode="search" + placeholder="Search by address" + aria-label="Transaction history wallet address input" + className="h-full w-full bg-transparent py-1 pl-1 pr-3 text-sm font-light placeholder:text-white/60" + // stop password managers from autofilling + data-1p-ignore + data-lpignore="true" + data-form-type="other" + /> + + +
+ ) +} diff --git a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistorySearchResults.tsx b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistorySearchResults.tsx new file mode 100644 index 0000000000..012cbe2146 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistorySearchResults.tsx @@ -0,0 +1,146 @@ +import dayjs from 'dayjs' +import { useEffect, useMemo } from 'react' +import { Tab } from '@headlessui/react' + +import { MergedTransaction } from '../../state/app/state' +import { + ContentWrapper, + TransactionHistoryTable +} from './TransactionHistoryTable' +import { TransactionStatusInfo } from '../TransactionHistory/TransactionStatusInfo' +import { + isTxClaimable, + isTxCompleted, + isTxExpired, + isTxFailed, + isTxPending +} from './helpers' +import { TabButton } from '../common/Tab' +import { TransactionsTableDetails } from './TransactionsTableDetails' +import { useTransactionHistory } from '../../hooks/useTransactionHistory' +import { useTransactionHistoryAddressStore } from './TransactionHistorySearchBar' + +function useTransactionHistoryUpdater() { + const { sanitizedAddress } = useTransactionHistoryAddressStore() + + const transactionHistoryProps = useTransactionHistory(sanitizedAddress, { + runFetcher: true + }) + + const { transactions, updatePendingTransaction } = transactionHistoryProps + + const pendingTransactions = useMemo(() => { + return transactions.filter(isTxPending) + }, [transactions]) + + useEffect(() => { + const interval = setInterval(() => { + pendingTransactions.forEach(updatePendingTransaction) + }, 10_000) + + return () => clearInterval(interval) + }, [pendingTransactions, updatePendingTransaction]) + + return transactionHistoryProps +} + +const tabClasses = + 'text-white px-3 mr-2 border-b-2 ui-selected:border-white ui-not-selected:border-transparent ui-not-selected:text-white/80 arb-hover' + +export function TransactionHistorySearchResults() { + const props = useTransactionHistoryUpdater() + const { transactions } = props + const { searchError } = useTransactionHistoryAddressStore() + + const oldestTxTimeAgoString = useMemo(() => { + return dayjs(transactions[transactions.length - 1]?.createdAt).toNow(true) + }, [transactions]) + + const groupedTransactions = useMemo( + () => + transactions.reduce( + (acc, tx) => { + if (isTxCompleted(tx) || isTxExpired(tx)) { + acc.settled.push(tx) + } + if (isTxPending(tx)) { + acc.pending.push(tx) + } + if (isTxClaimable(tx)) { + acc.claimable.push(tx) + } + if (isTxFailed(tx)) { + acc.failed.push(tx) + } + return acc + }, + { + settled: [] as MergedTransaction[], + pending: [] as MergedTransaction[], + claimable: [] as MergedTransaction[], + failed: [] as MergedTransaction[] + } + ), + [transactions] + ) + + const pendingTransactions = [ + ...groupedTransactions.failed, + ...groupedTransactions.pending, + ...groupedTransactions.claimable + ] + + const settledTransactions = groupedTransactions.settled + + if (searchError) { + return ( + +

{searchError}

+
+ ) + } + + return ( + <> +
+ +
+ + + + Pending transactions + + + Settled transactions + + + + + + + + + + + + + + + ) +} 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 830fa9f36d..2c05ec0da8 100644 --- a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistoryTable.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistoryTable.tsx @@ -26,7 +26,6 @@ import { isTxPending } from './helpers' import { PendingDepositWarning } from './PendingDepositWarning' import { TransactionsTableRow } from './TransactionsTableRow' import { EmptyTransactionHistory } from './EmptyTransactionHistory' -import { Address } from '../../util/AddressUtils' import { MergedTransaction } from '../../state/app/state' import { useNativeCurrency } from '../../hooks/useNativeCurrency' @@ -54,7 +53,7 @@ export const ContentWrapper = forwardRef<
{ const { transactions, - address, loading, completed, error, @@ -213,7 +210,7 @@ export const TransactionHistoryTable = ( >
@@ -278,7 +275,6 @@ export const TransactionHistoryTable = ( secondsPassed <= 30 && 'animate-blink bg-highlight' )} - address={address} />
) 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 a4122e6275..e3802a095b 100644 --- a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableDetails.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableDetails.tsx @@ -22,12 +22,12 @@ import { GET_HELP_LINK, ether } from '../../constants' import { useTransactionHistory } from '../../hooks/useTransactionHistory' import { shortenAddress } from '../../util/CommonUtils' import { isTxCompleted } from './helpers' -import { Address } from '../../util/AddressUtils' import { sanitizeTokenSymbol } from '../../util/TokenUtils' import { isBatchTransfer } from '../../util/TokenDepositUtils' import { BatchTransferNativeTokenTooltip } from './TransactionHistoryTable' import { useNativeCurrency } from '../../hooks/useNativeCurrency' import { isCustomDestinationAddressTx } from '../../state/app/utils' +import { useTransactionHistoryAddressStore } from './TransactionHistorySearchBar' const DetailsBox = ({ children, @@ -43,14 +43,11 @@ const DetailsBox = ({ ) } -export const TransactionsTableDetails = ({ - address -}: { - address: Address | undefined -}) => { +export const TransactionsTableDetails = () => { + const { sanitizedAddress } = useTransactionHistoryAddressStore() const { tx: txFromStore, isOpen, close, reset } = useTxDetailsStore() const { ethToUSD } = useETHPrice() - const { transactions } = useTransactionHistory(address) + const { transactions } = useTransactionHistory(sanitizedAddress) const tx = useMemo(() => { if (!txFromStore) { @@ -69,7 +66,7 @@ export const TransactionsTableDetails = ({ const childProvider = getProviderForChainId(tx?.childChainId ?? 0) const nativeCurrency = useNativeCurrency({ provider: childProvider }) - if (!tx || !address || !nativeCurrency) { + if (!tx || !sanitizedAddress || !nativeCurrency) { return null } @@ -82,9 +79,9 @@ export const TransactionsTableDetails = ({ !isNetwork(tx.parentChainId).isTestnet && tx.asset === ether.symbol const isDifferentSourceAddress = - address.toLowerCase() !== tx.sender?.toLowerCase() + sanitizedAddress.toLowerCase() !== tx.sender?.toLowerCase() const isDifferentDestinationAddress = isCustomDestinationAddressTx({ - sender: address, + sender: sanitizedAddress, destination: tx.destination }) @@ -269,7 +266,7 @@ export const TransactionsTableDetails = ({ )} - + {!isTxCompleted(tx) && ( diff --git a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableDetailsSteps.tsx b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableDetailsSteps.tsx index a349b939ff..9e662640ab 100644 --- a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableDetailsSteps.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableDetailsSteps.tsx @@ -31,6 +31,7 @@ import { minutesToHumanReadableTime, useTransferDuration } from '../../hooks/useTransferDuration' +import { useTransactionHistoryAddressStore } from './TransactionHistorySearchBar' function needsToClaimTransfer(tx: MergedTransaction) { return tx.isCctp || tx.isWithdrawal @@ -136,14 +137,7 @@ const LastStepEndItem = ({ (!isTeleport && isDepositReadyToRedeem(tx)) || (isTeleport && secondRetryableLegForTeleportRequiresRedeem(tx)) ) { - return ( - - ) + return } return null @@ -162,13 +156,12 @@ export const TransactionFailedOnNetwork = ({ ) export const TransactionsTableDetailsSteps = ({ - tx, - address + tx }: { tx: MergedTransaction - address: Address | undefined }) => { const { approximateDurationInMinutes } = useTransferDuration(tx) + const { sanitizedAddress } = useTransactionHistoryAddressStore() const { sourceChainId } = tx @@ -240,9 +233,7 @@ export const TransactionsTableDetailsSteps = ({ /> )} - {isTeleportTx(tx) && ( - - )} + {isTeleportTx(tx) && } {/* If claiming is required we show this step */} {needsToClaimTransfer(tx) && ( @@ -256,7 +247,6 @@ export const TransactionsTableDetailsSteps = ({ type={tx.isWithdrawal ? 'withdrawals' : 'deposits'} isError={false} tx={tx} - address={address} /> ) } @@ -268,7 +258,7 @@ export const TransactionsTableDetailsSteps = ({ done={isTxCompleted(tx)} failure={isTxExpired(tx) || isDestinationChainFailure} text={destinationChainTxText} - endItem={} + endItem={} />
) diff --git a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableDetailsTeleporterSteps.tsx b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableDetailsTeleporterSteps.tsx index 0540259aa3..a9a573b189 100644 --- a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableDetailsTeleporterSteps.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableDetailsTeleporterSteps.tsx @@ -1,5 +1,4 @@ import { useMemo } from 'react' -import { Address } from 'wagmi' import { twMerge } from 'tailwind-merge' import { ArrowTopRightOnSquareIcon, @@ -72,11 +71,9 @@ const TeleportMiddleStepFailureExplanationNote = ({ } export const TransactionsTableDetailsTeleporterSteps = ({ - tx, - address + tx }: { tx: TeleporterMergedTransaction - address: Address | undefined }) => { const l2TxID = tx.parentToChildMsgData?.childTxId const isFirstRetryableLegSucceeded = @@ -96,15 +93,8 @@ export const TransactionsTableDetailsTeleporterSteps = ({ typeof tx.l2ToL3MsgData?.l3TxID !== 'undefined' const firstRetryableRedeemButton = useMemo( - () => ( - - ), - [tx, address] + () => , + [tx] ) const firstTransactionExternalLink = useMemo( diff --git a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableRow.tsx b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableRow.tsx index ea4a54db0f..01f887a666 100644 --- a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableRow.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableRow.tsx @@ -121,11 +121,9 @@ const StatusLabel = ({ tx }: { tx: MergedTransaction }) => { export function TransactionsTableRow({ tx, - address, className = '' }: { tx: MergedTransaction - address: Address | undefined className?: string }) { const { open: openTxDetails } = useTxDetailsStore() @@ -259,7 +257,6 @@ export function TransactionsTableRow({ tx={tx} isError={isError} type={tx.isWithdrawal ? 'withdrawals' : 'deposits'} - address={address} />
diff --git a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableRowAction.tsx b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableRowAction.tsx index af4fe167ef..b698597a3a 100644 --- a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableRowAction.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableRowAction.tsx @@ -17,27 +17,26 @@ import { useNetwork } from 'wagmi' import { isDepositReadyToRedeem } from '../../state/app/utils' import { useRedeemRetryable } from '../../hooks/useRedeemRetryable' import { TransferCountdown } from '../common/TransferCountdown' -import { Address } from '../../util/AddressUtils' import { getChainIdForRedeemingRetryable } from '../../util/RetryableUtils' import { isTeleportTx } from '../../hooks/useTransactions' import { useRedeemTeleporter } from '../../hooks/useRedeemTeleporter' import { sanitizeTokenSymbol } from '../../util/TokenUtils' import { formatAmount } from '../../util/NumberUtils' +import { useTransactionHistoryAddressStore } from './TransactionHistorySearchBar' export function TransactionsTableRowAction({ tx, isError, - type, - address + type }: { tx: MergedTransaction | TeleporterMergedTransaction isError: boolean type: 'deposits' | 'withdrawals' - address: Address | undefined }) { const { chain } = useNetwork() const { switchNetworkAsync } = useSwitchNetworkWithConfig() const networkName = getNetworkName(chain?.id ?? 0) + const { sanitizedAddress } = useTransactionHistoryAddressStore() const tokenSymbol = sanitizeTokenSymbol(tx.asset, { erc20L1Address: tx.tokenAddress, @@ -48,10 +47,10 @@ export function TransactionsTableRowAction({ const { claim: claimCctp, isClaiming: isClaimingCctp } = useClaimCctp(tx) const { redeem, isRedeeming: isRetryableRedeeming } = useRedeemRetryable( tx, - address + sanitizedAddress ) const { redeem: teleporterRedeem, isRedeeming: isTeleporterRedeeming } = - useRedeemTeleporter(tx, address) + useRedeemTeleporter(tx, sanitizedAddress) const isRedeeming = isRetryableRedeeming || isTeleporterRedeeming