diff --git a/apps/namadillo/src/App/AppRoutes.tsx b/apps/namadillo/src/App/AppRoutes.tsx index 0237345644..3fd2ec046c 100644 --- a/apps/namadillo/src/App/AppRoutes.tsx +++ b/apps/namadillo/src/App/AppRoutes.tsx @@ -45,7 +45,6 @@ import { StakingRewards } from "./Staking/StakingRewards"; import { StakingWithdrawModal } from "./Staking/StakingWithdrawModal"; import { Unstake } from "./Staking/Unstake"; import { SwitchAccountPanel } from "./SwitchAccount/SwitchAccountPanel"; -import { TransactionDetails } from "./Transactions/TransactionDetails"; import { TransactionHistory } from "./Transactions/TransactionHistory"; import { TransferLayout } from "./Transfer/TransferLayout"; @@ -126,10 +125,6 @@ export const MainRoutes = (): JSX.Element => { {(features.namTransfersEnabled || features.ibcTransfersEnabled) && ( } /> - } - /> )} diff --git a/apps/namadillo/src/App/Common/TransactionReceipt.tsx b/apps/namadillo/src/App/Common/TransactionReceipt.tsx deleted file mode 100644 index ea1ee97307..0000000000 --- a/apps/namadillo/src/App/Common/TransactionReceipt.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import { Chain } from "@chain-registry/types"; -import { CopyToClipboardControl, Stack } from "@namada/components"; -import { shortenAddress } from "@namada/utils"; -import { - isNamadaAddress, - isShieldedAddress, - parseChainInfo, -} from "App/Transfer/common"; -import { SelectedChain } from "App/Transfer/SelectedChain"; -import { SelectedWallet } from "App/Transfer/SelectedWallet"; -import { TokenAmountCard } from "App/Transfer/TokenAmountCard"; -import { TransferArrow } from "App/Transfer/TransferArrow"; -import { findChainById } from "atoms/integrations"; -import BigNumber from "bignumber.js"; -import clsx from "clsx"; -import { wallets } from "integrations"; -import { useMemo } from "react"; -import { FaCheckCircle } from "react-icons/fa"; -import { GoHourglass, GoXCircle } from "react-icons/go"; -import { - allTransferTypes, - PartialTransferTransactionData, - TransferStep, -} from "types"; - -type TransactionReceiptProps = { - transaction: PartialTransferTransactionData; -}; - -const stepDescription: Record = { - sign: "Signature required", - "ibc-to-shielded": "IBC Transfer to Namada MASP", - "zk-proof": "Generating ZK Proof", - "ibc-to-transparent": "IBC Transfer to Namada", - "shielded-to-shielded": "Transfer to Namada Shielded", - "transparent-to-shielded": "Transfer to Namada Shielded", - "transparent-to-transparent": "Transfer to Namada Transparent", - "shielded-to-transparent": "Transfer to Namada Transparent", - "ibc-withdraw": "Transfer from Namada", - "waiting-confirmation": "Waiting for confirmation", - complete: "Transfer Complete", -}; - -const TransferTransactionReceipt = ({ - transaction, -}: TransactionReceiptProps): JSX.Element => { - const getChain = (chainId: string, address: string): Chain | undefined => { - const chain = findChainById(chainId); - if (isNamadaAddress(address) && chain) { - return parseChainInfo(chain, isShieldedAddress(address || "")); - } - return chain; - }; - - const sourceChain = useMemo(() => { - return getChain(transaction.chainId, transaction.sourceAddress || ""); - }, [transaction]); - - const destinationChain = useMemo(() => { - return getChain( - "destinationChainId" in transaction ? - transaction.destinationChainId || "" - : transaction.chainId, - transaction.destinationAddress || "" - ); - }, [transaction]); - - const sourceWallet = - isNamadaAddress(transaction.sourceAddress || "") ? - wallets.namada - : wallets.keplr; - - const destinationWallet = - isNamadaAddress(transaction.destinationAddress || "") ? - wallets.namada - : wallets.keplr; - - return ( - -
-
- {sourceChain && ( - - )} - {sourceWallet && ( - - )} -
-
- -
- - - -
- -
-
-
- {destinationChain && ( - - )} - {destinationWallet && ( - - )} -
-
-
-
- ); -}; - -export const TransactionReceipt = ({ - transaction, -}: TransactionReceiptProps): JSX.Element => { - const isTransferTransaction = (): boolean => { - return allTransferTypes.includes(transaction.type); - }; - - return ( -
-
-
- {transaction.status === "error" && ( - {} - )} - {transaction.status === "success" && ( - {} - )} - {transaction.status === "pending" && ( - {} - )} -
-

- {transaction.status === "error" ? - "Transaction Failed" - : stepDescription[transaction.currentStep || "sign"]} -

- {transaction.errorMessage && ( -
- {transaction.errorMessage} -
- )} - {transaction.hash && ( - - Transaction hash:{" "} - - {shortenAddress(transaction.hash, 8, 8)} - - - - )} -
-
- {isTransferTransaction() ? - - :
} -
-
- ); -}; diff --git a/apps/namadillo/src/App/Transactions/PendingTransactionCard.tsx b/apps/namadillo/src/App/Transactions/PendingTransactionCard.tsx new file mode 100644 index 0000000000..2ca20b1322 --- /dev/null +++ b/apps/namadillo/src/App/Transactions/PendingTransactionCard.tsx @@ -0,0 +1,138 @@ +import { shortenAddress } from "@namada/utils"; +import { TokenCurrency } from "App/Common/TokenCurrency"; +import { AssetImage } from "App/Transfer/AssetImage"; +import { isShieldedAddress, isTransparentAddress } from "App/Transfer/common"; +import clsx from "clsx"; +import { FaLock } from "react-icons/fa"; +import { + IoArrowForward, + IoCheckmarkCircleOutline, + IoCloseCircleOutline, +} from "react-icons/io5"; +import { twMerge } from "tailwind-merge"; +import { + ibcTransferStages, + namadaTransferStages, + TransferTransactionData, +} from "types"; +import keplrSvg from "../../integrations/assets/keplr.svg"; + +type TransactionCardProps = { + transaction: TransferTransactionData; +}; + +const getTitle = (transferTransaction: TransferTransactionData): string => { + const { type } = transferTransaction; + + if (Object.keys(namadaTransferStages).includes(type)) { + return "Transfer"; + } + + if (Object.keys(ibcTransferStages).includes(type)) { + return "Transfer IBC"; + } + + return ""; +}; + +export const PendingTransactionCard = ({ + transaction, +}: TransactionCardProps): JSX.Element => { + const renderKeplrIcon = (address: string): JSX.Element | null => { + if (isShieldedAddress(address)) return null; + if (isTransparentAddress(address)) return null; + return ; + }; + const sender = transaction.sourceAddress; + const receiver = transaction.destinationAddress; + return ( +
+
+ + + + +
+

+ {getTitle(transaction)}{" "} + {transaction.status === "success" && ( + + )} + {transaction.status === "error" && ( + + )} +

+

Pending

+
+
+ +
+
+ +
+ +
+
+

+ From +

+

+ {isShieldedAddress(sender ?? "") ? + + znam + + :
+ {renderKeplrIcon(sender ?? "")} + {shortenAddress(sender ?? "", 10, 10)} +
+ } +

+
+ +
+

+ To +

+

+ {isShieldedAddress(receiver ?? "") ? + + znam + + :
+ {renderKeplrIcon(receiver ?? "")} + {shortenAddress(receiver ?? "", 10, 10)} +
+ } +

+
+
+ ); +}; diff --git a/apps/namadillo/src/App/Transactions/TransactionCard.tsx b/apps/namadillo/src/App/Transactions/TransactionCard.tsx index 843bb9bf8d..ef4d8d38b0 100644 --- a/apps/namadillo/src/App/Transactions/TransactionCard.tsx +++ b/apps/namadillo/src/App/Transactions/TransactionCard.tsx @@ -1,95 +1,244 @@ +import { CopyToClipboardControl } from "@namada/components"; +import { TransactionHistory as TransactionHistoryType } from "@namada/indexer-client"; +import { shortenAddress } from "@namada/utils"; import { TokenCurrency } from "App/Common/TokenCurrency"; -import { routes } from "App/routes"; -import { parseChainInfo } from "App/Transfer/common"; -import { chainRegistryAtom } from "atoms/integrations"; +import { AssetImage } from "App/Transfer/AssetImage"; +import { isShieldedAddress, isTransparentAddress } from "App/Transfer/common"; +import { indexerApiAtom } from "atoms/api"; +import { fetchBlockTimestampByHeight } from "atoms/balance/services"; +import { chainAssetsMapAtom } from "atoms/chain"; +import BigNumber from "bignumber.js"; import clsx from "clsx"; import { useAtomValue } from "jotai"; -import { GoIssueClosed, GoIssueTrackedBy, GoXCircle } from "react-icons/go"; -import { ImCheckmark } from "react-icons/im"; -import { generatePath, useNavigate } from "react-router-dom"; -import { twMerge } from "tailwind-merge"; +import { useEffect, useState } from "react"; +import { FaLock } from "react-icons/fa6"; import { - ibcTransferStages, - IbcTransferTransactionData, - namadaTransferStages, - TransferTransactionData, -} from "types"; - -type TransactionCardProps = { - transaction: TransferTransactionData; + IoArrowBack, + IoArrowForward, + IoCheckmarkCircleOutline, + IoCloseCircleOutline, +} from "react-icons/io5"; +import { twMerge } from "tailwind-merge"; +import { toDisplayAmount } from "utils"; +import keplrSvg from "../../integrations/assets/keplr.svg"; + +type Tx = TransactionHistoryType; +type Props = { tx: Tx }; +type RawDataSection = { + amount?: string; + sources?: Array<{ amount: string; owner: string }>; + targets?: Array<{ amount: string; owner: string }>; }; +const IBC_PREFIX = "ibc"; -const getTitle = (transferTransaction: TransferTransactionData): string => { - const { type } = transferTransaction; +export function getToken(txn: Tx["tx"]): string | undefined { + const parsed = txn?.data ? JSON.parse(txn.data) : undefined; + if (!parsed) return undefined; + const sections = Array.isArray(parsed) ? parsed : [parsed]; - if (Object.keys(namadaTransferStages).includes(type)) { - return "Transfer"; + // return the first token found in sources or targets + for (const section of sections) { + if (section.sources?.length) { + return section.sources[0].token; + } + if (section.targets?.length) { + return section.targets[0].token; + } } - if (Object.keys(ibcTransferStages).includes(type)) { - return "Transfer IBC"; - } + return undefined; +} - return ""; +const titleFor = (kind: string | undefined, isReceived: boolean): string => { + if (!kind) return "Unknown"; + if (isReceived) return "Receive"; + if (kind.startsWith(IBC_PREFIX)) return "IBC Transfer"; + if (kind === "transparentTransfer") return "Transparent Transfer"; + if (kind === "shieldingTransfer") return "Shielding Transfer"; + if (kind === "unshieldingTransfer") return "Unshielding Transfer"; + if (kind === "shieldedTransfer") return "Shielded Transfer"; + return "Transfer"; }; -export const TransactionCard = ({ - transaction, -}: TransactionCardProps): JSX.Element => { - const navigate = useNavigate(); - const availableChains = useAtomValue(chainRegistryAtom); - const isIbc = Object.keys(ibcTransferStages).includes(transaction.type); +export function getTransactionInfo( + tx: Tx["tx"] +): { amount: BigNumber; sender?: string; receiver?: string } | undefined { + if (!tx?.data) return undefined; + + const parsed = typeof tx.data === "string" ? JSON.parse(tx.data) : tx.data; + const sections: RawDataSection[] = Array.isArray(parsed) ? parsed : [parsed]; + + let sender: string | undefined; + let receiver: string | undefined; + let amount: BigNumber | undefined; + + for (const sec of sections) { + if (!amount && sec.targets?.[0]?.amount) { + amount = new BigNumber(sec.targets[0].amount); + receiver = sec.targets[0].owner; + } + if (!amount && sec.sources?.[0]?.amount) { + amount = new BigNumber(sec.sources[0].amount); + } + if (!sender && sec.sources?.[0]?.owner) { + sender = sec.sources[0].owner; + } + if (amount && (sender || receiver)) break; // we have what we need + } + + return amount ? { amount, sender, receiver } : undefined; +} + +export const TransactionCard = ({ tx }: Props): JSX.Element => { + const transactionTopLevel = tx; + const transaction = transactionTopLevel.tx; + const isReceived = transactionTopLevel?.kind === "received"; + const token = getToken(transaction); + const chainAssetsMap = useAtomValue(chainAssetsMapAtom); + const asset = token ? chainAssetsMap[token] : undefined; + const txnInfo = getTransactionInfo(transaction); + const baseAmount = + asset && txnInfo?.amount ? + toDisplayAmount(asset, txnInfo.amount) + : undefined; + const receiver = txnInfo?.receiver; + const sender = txnInfo?.sender; + const transactionFailed = transaction?.exitCode === "rejected"; + const api = useAtomValue(indexerApiAtom); + const [timestamp, setTimestamp] = useState(undefined); - const chainId = - isIbc ? - (transaction as IbcTransferTransactionData).destinationChainId - : transaction.chainId; + useEffect(() => { + const getBlockTimestamp = async (): Promise => { + // TODO: need to update the type on indexer + // @ts-expect-error need to update the type on indexer + if (transactionTopLevel?.blockHeight && api) { + try { + const timestamp = await fetchBlockTimestampByHeight( + api, + // @ts-expect-error need to update the type on indexer + transactionTopLevel.blockHeight + ); + setTimestamp(timestamp); + } catch (error) { + console.error("Failed to fetch block height:", error); + } + } + }; + + getBlockTimestamp(); + // @ts-expect-error need to update the type on indexer + }, [api, transactionTopLevel?.blockHeight]); - const chainName = - chainId in availableChains ? - parseChainInfo(availableChains[chainId].chain)?.pretty_name - : chainId; + const renderKeplrIcon = (address: string): JSX.Element | null => { + if (isShieldedAddress(address)) return null; + if (isTransparentAddress(address)) return null; + return ; + }; return (
- transaction.hash && - navigate(generatePath(routes.transaction, { hash: transaction.hash })) - } > - - {transaction.status === "success" && } - {transaction.status === "pending" && } - {transaction.status === "error" && } - -
-

{getTitle(transaction)}

-

- {" "} - to {chainName} {transaction.destinationAddress} -

+
+ + {isReceived && } + {!isReceived && } + + +
+

+ {titleFor(transaction?.kind, isReceived)}{" "} + {!transactionFailed && ( + + )} + {transactionFailed && ( + + )} + +

+

+ {timestamp ? + new Date(timestamp * 1000) + .toLocaleString("en-US", { + day: "2-digit", + month: "2-digit", + year: "2-digit", + hour: "2-digit", + minute: "2-digit", + }) + .replace(",", "") + : "-"} +

+
+
+ +
+
+ +
+ +
+ +
+

+ From +

+

+ {isShieldedAddress(sender ?? "") ? + + znam + + :
+ {renderKeplrIcon(sender ?? "")} + {shortenAddress(sender ?? "", 10, 10)} +
+ } +

+
+ +
+

+ To +

+

+ {isShieldedAddress(receiver ?? "") ? + + znam + + :
+ {renderKeplrIcon(receiver ?? "")} + {shortenAddress(receiver ?? "", 10, 10)} +
+ } +

- - {transaction.status === "success" && } -
); }; diff --git a/apps/namadillo/src/App/Transactions/TransactionDetails.tsx b/apps/namadillo/src/App/Transactions/TransactionDetails.tsx deleted file mode 100644 index 85ea49c36c..0000000000 --- a/apps/namadillo/src/App/Transactions/TransactionDetails.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Panel } from "@namada/components"; -import { useSanitizedParams } from "@namada/hooks"; -import { TransactionReceipt } from "App/Common/TransactionReceipt"; -import { useTransactionActions } from "hooks/useTransactionActions"; -import { TransactionNotFoundPanel } from "./TransactionNotFoundPanel"; - -export const TransactionDetails = (): JSX.Element => { - const { hash } = useSanitizedParams(); - const { findByHash } = useTransactionActions(); - - const transaction = findByHash(hash || ""); - if (!transaction) { - return ; - } - - return ( - -

Transactions

- -
- ); -}; diff --git a/apps/namadillo/src/App/Transactions/TransactionHistory.tsx b/apps/namadillo/src/App/Transactions/TransactionHistory.tsx index 7f10272ead..d44c6673a9 100644 --- a/apps/namadillo/src/App/Transactions/TransactionHistory.tsx +++ b/apps/namadillo/src/App/Transactions/TransactionHistory.tsx @@ -1,55 +1,127 @@ -import { Panel } from "@namada/components"; +import { Panel, TableRow } from "@namada/components"; +import { TransactionHistory as TransactionHistoryType } from "@namada/indexer-client"; import { NavigationFooter } from "App/AccountOverview/NavigationFooter"; +import { PageLoader } from "App/Common/PageLoader"; +import { TableWithPaginator } from "App/Common/TableWithPaginator"; import { - completeTransactionsHistoryAtom, - myTransactionHistoryAtom, + chainTransactionHistoryFamily, pendingTransactionsHistoryAtom, } from "atoms/transactions/atoms"; -import { useTransactionActions } from "hooks/useTransactionActions"; import { useAtomValue } from "jotai"; -import { TransactionList } from "./TransactionHistoryList"; +import { useMemo, useState } from "react"; +import { twMerge } from "tailwind-merge"; +import { PendingTransactionCard } from "./PendingTransactionCard"; +import { TransactionCard } from "./TransactionCard"; + +const ITEMS_PER_PAGE = 30; +export const transferKindOptions = [ + "transparentTransfer", + "shieldingTransfer", + "unshieldingTransfer", + "shieldedTransfer", + "ibcTransparentTransfer", + "ibcShieldingTransfer", + "ibcUnshieldingTransfer", + "ibcShieldedTransfer", + "received", +]; export const TransactionHistory = (): JSX.Element => { - const transactions = useAtomValue(myTransactionHistoryAtom); + const [currentPage, setCurrentPage] = useState(0); const pending = useAtomValue(pendingTransactionsHistoryAtom); - const complete = useAtomValue(completeTransactionsHistoryAtom); - const hasNoTransactions = transactions.length === 0; - const { clearMyCompleteTransactions } = useTransactionActions(); + const { data: transactions, isLoading } = useAtomValue( + chainTransactionHistoryFamily({ perPage: ITEMS_PER_PAGE, fetchAll: true }) + ); + // Only show historical transactions that are in the transferKindOptions array + const historicalTransactions = + transactions?.results?.filter((transaction) => + transferKindOptions.includes(transaction.tx?.kind ?? "") + ) ?? []; + + // Calculate total pages based on the filtered transactions + const totalPages = Math.max( + 1, + Math.ceil(historicalTransactions.length / ITEMS_PER_PAGE) + ); + + // Create paginated data for the current page + const paginatedTransactions = useMemo(() => { + const startIndex = currentPage * ITEMS_PER_PAGE; + const endIndex = startIndex + ITEMS_PER_PAGE; + return historicalTransactions.slice(startIndex, endIndex); + }, [historicalTransactions, currentPage]); + + const renderRow = ( + transaction: TransactionHistoryType, + index: number + ): TableRow => { + return { + key: transaction.tx?.txId || index.toString(), + cells: [], + }; + }; + + const handlePageChange = (page: number): void => { + setCurrentPage(page); + }; return ( -
- -
-

Transfers made by this device

- {pending.length > 0 && ( -
-

In Progress

- -
- )} - {complete.length > 0 && ( -
-
-

History

- -
- -
- )} - {hasNoTransactions && ( -

No transactions saved on this device

- )} -
+
+ +

All Transfers made

+ + {pending.length > 0 && ( +
+

Pending

+
+ {pending.map((transaction) => ( + + ))} +
+
+ )} + + {isLoading ? + + :
+
+

History

+
+
+
+ tbody>tr:nth-child(odd)]:bg-transparent", + "[&>tbody>tr:nth-child(even)]:bg-transparent", + "w-full [&_td]:px-1 [&_th]:px-1 [&_td:first-child]:pl-4 [&_td]:h-[64px]", + "[&_td]:font-normal [&_td:last-child]:pr-4 [&_th:first-child]:pl-4 [&_th:last-child]:pr-4", + "[&_td:first-child]:rounded-s-md [&_td:last-child]:rounded-e-md" + ), + }} + /> +
+
+ {historicalTransactions.length === 0 && ( +

+ No transactions saved on this device +

+ )} +
+ }
- +
); }; diff --git a/apps/namadillo/src/App/Transactions/TransactionHistoryList.tsx b/apps/namadillo/src/App/Transactions/TransactionHistoryList.tsx deleted file mode 100644 index 63641c68f6..0000000000 --- a/apps/namadillo/src/App/Transactions/TransactionHistoryList.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { TransferTransactionData } from "types"; -import { TransactionCard } from "./TransactionCard"; - -export const TransactionList = ({ - transactions, -}: { - transactions: TransferTransactionData[]; -}): JSX.Element => ( -
    - {transactions.map((tx) => ( -
  • - -
  • - ))} -
-); diff --git a/apps/namadillo/src/App/Transactions/TransactionNotFoundPanel.tsx b/apps/namadillo/src/App/Transactions/TransactionNotFoundPanel.tsx deleted file mode 100644 index a75119796d..0000000000 --- a/apps/namadillo/src/App/Transactions/TransactionNotFoundPanel.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Panel } from "@namada/components"; -import { shortenAddress } from "@namada/utils"; - -type TransactionNotFoundPanelProps = { - hash: string; -}; - -export const TransactionNotFoundPanel = ({ - hash, -}: TransactionNotFoundPanelProps): JSX.Element => { - return ( - -

Transactions

-

- The details of transaction{" "} - {shortenAddress(hash || "", 10, 6)}{" "} - couldn't be found on this device. -

-
- ); -}; diff --git a/apps/namadillo/src/atoms/balance/services.ts b/apps/namadillo/src/atoms/balance/services.ts index ade688c7f2..fec3d06b63 100644 --- a/apps/namadillo/src/atoms/balance/services.ts +++ b/apps/namadillo/src/atoms/balance/services.ts @@ -111,6 +111,14 @@ export const fetchBlockHeightByTimestamp = async ( return Number(response.data.height); }; +export const fetchBlockTimestampByHeight = async ( + api: DefaultApi, + height: number +): Promise => { + const response = await api.apiV1BlockHeightValueGet(height); + return Number(response.data.timestamp); +}; + export const fetchShieldedRewards = async ( viewingKey: DatedViewingKey, chainId: string, diff --git a/apps/namadillo/src/atoms/transactions/atoms.ts b/apps/namadillo/src/atoms/transactions/atoms.ts index c5bcef3c56..97350a715a 100644 --- a/apps/namadillo/src/atoms/transactions/atoms.ts +++ b/apps/namadillo/src/atoms/transactions/atoms.ts @@ -1,13 +1,15 @@ -import { defaultAccountAtom } from "atoms/accounts"; +import { Pagination, TransactionHistory } from "@namada/indexer-client"; +import { allDefaultAccountsAtom, defaultAccountAtom } from "atoms/accounts"; import { indexerApiAtom } from "atoms/api"; import { atom } from "jotai"; -import { atomWithStorage } from "jotai/utils"; +import { atomWithQuery } from "jotai-tanstack-query"; +import { atomFamily, atomWithStorage } from "jotai/utils"; import { Address, TransferTransactionData } from "types"; import { filterCompleteTransactions, filterPendingTransactions, } from "./functions"; -import { fetchTransaction } from "./services"; +import { fetchHistoricalTransactions, fetchTransaction } from "./services"; export const transactionStorageKey = "namadillo:transactions"; @@ -38,3 +40,106 @@ export const fetchTransactionAtom = atom((get) => { const api = get(indexerApiAtom); return (hash: string) => fetchTransaction(api, hash); }); + +// New atom family for paginated transaction history +export const chainTransactionHistoryFamily = atomFamily( + (options?: { page?: number; perPage?: number; fetchAll?: boolean }) => + atomWithQuery<{ + results: TransactionHistory[]; + pagination: Pagination; + }>((get) => { + const api = get(indexerApiAtom); + const accounts = get(allDefaultAccountsAtom); + const addresses = accounts.data?.map((acc) => acc.address); + + return { + enabled: !!addresses, // Only run the query if we have addresses + queryKey: [ + "chain-transaction-history", + addresses, + options?.fetchAll ? "all" : options?.page, + options?.perPage, + ], + queryFn: async () => { + if (!addresses) { + return { + results: [], + pagination: { + totalPages: "0", + totalItems: "0", + currentPage: "0", + perPage: "0", + }, + }; + } + + // If fetchAll is true, we'll get all pages + if (options?.fetchAll) { + // First fetch to get pagination info + const firstPageResult = await fetchHistoricalTransactions( + api, + addresses, + 1, + options?.perPage || 10 + ); + + const totalPages = parseInt( + firstPageResult.pagination?.totalPages || "0" + ); + + // If there's only one page, return the first result + if (totalPages <= 1) { + return firstPageResult; + } + + // Otherwise, fetch all remaining pages + const allPagePromises = []; + // We already have page 1 + const allResults = [...firstPageResult.results]; + + // Fetch pages 2 to totalPages + for (let page = 2; page <= totalPages; page++) { + allPagePromises.push( + fetchHistoricalTransactions( + api, + addresses, + page, + options?.perPage || 10 + ) + ); + } + + const allPagesResults = await Promise.all(allPagePromises); + + // Combine all results + for (const pageResult of allPagesResults) { + allResults.push(...pageResult.results); + } + + // Return combined results with updated pagination info + return { + results: allResults, + pagination: { + ...firstPageResult.pagination, + totalPages: totalPages.toString(), + currentPage: "all", // Indicate we fetched all pages + }, + }; + } + + // Standard case: fetch a single page + return fetchHistoricalTransactions( + api, + addresses, + options?.page, + options?.perPage + ); + }, + }; + }), + (a, b) => + // Equality check for memoization - include fetchAll in comparison + a?.page === b?.page && + a?.perPage === b?.perPage && + a?.fetchAll === b?.fetchAll +); diff --git a/apps/namadillo/src/atoms/transactions/services.ts b/apps/namadillo/src/atoms/transactions/services.ts index 10d408fe98..bf05f82530 100644 --- a/apps/namadillo/src/atoms/transactions/services.ts +++ b/apps/namadillo/src/atoms/transactions/services.ts @@ -1,5 +1,10 @@ import { IndexedTx, StargateClient } from "@cosmjs/stargate"; -import { DefaultApi, WrapperTransaction } from "@namada/indexer-client"; +import { + DefaultApi, + Pagination, + TransactionHistory, + WrapperTransaction, +} from "@namada/indexer-client"; import { IbcTransferTransactionData } from "types"; import { sanitizeAddress } from "utils/address"; @@ -67,3 +72,20 @@ export const fetchTransaction = async ( // indexer only accepts the hash as lowercase return (await api.apiV1ChainWrapperTxIdGet(sanitizeAddress(hash))).data; }; + +export const fetchHistoricalTransactions = async ( + api: DefaultApi, + addresses: string[], + page?: number, + perPage?: number +): Promise<{ results: TransactionHistory[]; pagination: Pagination }> => { + const pageParam = page ? page : undefined; + const response = await api.apiV1ChainHistoryGet(addresses, { + params: { + page: pageParam, + perPage: perPage, + }, + }); + + return response.data; +}; diff --git a/apps/namadillo/src/hooks/useTransactionActions.ts b/apps/namadillo/src/hooks/useTransactionActions.ts index 86c08b823e..e75f2bafd3 100644 --- a/apps/namadillo/src/hooks/useTransactionActions.ts +++ b/apps/namadillo/src/hooks/useTransactionActions.ts @@ -8,9 +8,7 @@ import { Address, TransferTransactionData } from "types"; type UseTransactionActionsOutput = { transactions: TransferTransactionData[]; - findByHash: (hash: string) => TransferTransactionData | undefined; storeTransaction: (tx: TransferTransactionData) => void; - clearMyCompleteTransactions: () => void; changeTransaction: ( hash: string, updatedTx: Partial, @@ -57,27 +55,9 @@ export const useTransactionActions = (): UseTransactionActionsOutput => { }); }; - const findByHash = (hash: string): undefined | TransferTransactionData => { - return transactions.find((t) => t.hash === hash); - }; - - const clearMyCompleteTransactions = (): void => { - if (!account) return; - setTransactions((txs) => { - return { - ...txs, - [account.address]: txs[account.address].filter( - (tx) => tx.status === "pending" || tx.status === "idle" - ), - }; - }); - }; - return { transactions, - findByHash, storeTransaction, changeTransaction, - clearMyCompleteTransactions, }; };