-
{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 && (
-
- )}
- {complete.length > 0 && (
-
-
- History
-
-
-
-
- )}
- {hasNoTransactions && (
-
No transactions saved on this device
- )}
-
+
+
+ All Transfers made
+
+ {pending.length > 0 && (
+
+
Pending
+
+ {pending.map((transaction) => (
+
+ ))}
+
+
+ )}
+
+ {isLoading ?
+
+ :
+
+
+
+
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,
};
};