Repository: caravan-bitcoin/caravan
Pull request #168: feat(wallet): Add transaction history tab with pending/confirmed views
Author: login: Legend101Zz
Created: 2025-01-05T18:29:16Z | Merged: 2025-03-25T03:58:38Z

Add a new **Transactions** tab to the wallet interface that provides users with a detailed view of their transaction history. The tab includes:

## Features
- **Split view** for pending and confirmed transactions.
- **Transaction tables** with key information:
  - Transaction ID with copy functionality.
  - Time/date of transaction.
  - Total fee.
  - Transaction size in vBytes.
  - Confirmation status.
  - Block height (for confirmed transactions).
- Ability to view transaction details in a block explorer.
- **Auto-refresh** capability for transaction status updates.
- Clear loading states and error handling for a better user experience.

## Technical Changes
- Added new transaction state management in **Redux**.
- Implemented transaction filtering by confirmation status.
- Added data fetching logic for transaction details.
- Included test coverage for:
  - Redux actions.
  - Reducers.
  - Component behavior.

## Why This Change?
This enhancement improves the wallet's usability by providing users with:
- Better visibility into their transaction history and statuses.
- Easy access to key details for pending and confirmed transactions.
- A more streamlined and informative transaction management experience.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
	- Introduced a new "Transactions" view in the wallet, allowing you to easily switch between confirmed and pending transactions.
	- The transactions screen features a sortable table with details such as fees, size, and time, plus refresh and error indicators.
	- Enhanced transaction data retrieval ensures more consistent and reliable transaction information display.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/.changeset/new-squids-talk.md b/.changeset/new-squids-talk.md
new file mode 100644
index 0000000000..76bea87de0
--- /dev/null
+++ b/.changeset/new-squids-talk.md
@@ -0,0 +1,5 @@
+---
+"@caravan/clients": patch
+---
+
+Patch: align client.getTransaction method implementation with bitcoindRawTxData response
diff --git a/.changeset/pretty-chefs-explain.md b/.changeset/pretty-chefs-explain.md
new file mode 100644
index 0000000000..1ad494c528
--- /dev/null
+++ b/.changeset/pretty-chefs-explain.md
@@ -0,0 +1,5 @@
+---
+"caravan-coordinator": minor
+---
+
+Add a new Transactions tab to the wallet interface that provides users with a detailed view of their transaction history.
diff --git a/apps/coordinator/src/actions/transactionActions.js b/apps/coordinator/src/actions/transactionActions.js
index 9468282b7e..82a5d2a32c 100644
--- a/apps/coordinator/src/actions/transactionActions.js
+++ b/apps/coordinator/src/actions/transactionActions.js
@@ -7,6 +7,7 @@ import {
   autoLoadPSBT,
 } from "@caravan/bitcoin";
 import { getSpendableSlices, getConfirmedBalance } from "../selectors/wallet";
+
 import { DUST_IN_BTC } from "../utils/constants";
 
 export const CHOOSE_PERFORM_SPEND = "CHOOSE_PERFORM_SPEND";
diff --git a/apps/coordinator/src/actions/walletActions.js b/apps/coordinator/src/actions/walletActions.js
index f884bb3f42..5d77935f02 100644
--- a/apps/coordinator/src/actions/walletActions.js
+++ b/apps/coordinator/src/actions/walletActions.js
@@ -33,6 +33,7 @@ export const WALLET_MODES = {
   VIEW: 0,
   DEPOSIT: 1,
   SPEND: 2,
+  TRANSACTIONS: 3, // new mode to view both confirmed and pending txs
 };
 
 export function updateDepositSliceAction(value) {
diff --git a/apps/coordinator/src/components/Wallet/TransactionsTab/TransactionsTable.tsx b/apps/coordinator/src/components/Wallet/TransactionsTab/TransactionsTable.tsx
new file mode 100644
index 0000000000..3e200a8a80
--- /dev/null
+++ b/apps/coordinator/src/components/Wallet/TransactionsTab/TransactionsTable.tsx
@@ -0,0 +1,244 @@
+import React, { useState } from "react";
+import {
+  Table,
+  TableBody,
+  TableCell,
+  TableContainer,
+  TableHead,
+  TableRow,
+  TableSortLabel,
+  Paper,
+  Chip,
+  IconButton,
+  Tooltip,
+  Alert,
+  Snackbar,
+  Box,
+} from "@mui/material";
+import HelpOutlineIcon from "@mui/icons-material/HelpOutline";
+import { formatDistanceToNow } from "date-fns";
+import { OpenInNew } from "@mui/icons-material";
+import { TransactionT, TransactionTableProps } from "./types";
+
+/**
+ * ⚠️ MAJOR TODO:
+ * ---------------------------------------------------------------
+ * Due to a discrepancy in fees not being supplied by the client's
+ * package for the private note, the "fees" column has been dropped.
+ * This will be included in a future update of the coordinator.
+ *
+ * That’s why the fee component code has been commented out.
+ * ---------------------------------------------------------------
+ */
+
+// Helper function to format the relative time
+const formatRelativeTime = (timestamp?: number): string => {
+  if (!timestamp) return "Pending";
+  return formatDistanceToNow(new Date(timestamp * 1000), { addSuffix: true });
+};
+
+// Column definitions with sorting configuration
+const columns = [
+  { id: "txid", label: "Transaction ID", sortable: false },
+  { id: "blockTime", label: "Time", sortable: true },
+  { id: "size", label: "Size (vBytes)", sortable: true },
+  // { id: "fee", label: "Fee (sats)", sortable: true },
+  { id: "status", label: "Status", sortable: false },
+  { id: "actions", label: "", sortable: false },
+];
+
+// FeeDisplay component to display fees in both sats and BTC
+// const FeeDisplay: React.FC<{ feeInSats?: number }> = ({ feeInSats }) => {
+//   if (feeInSats === null || feeInSats === undefined) {
+//     return <Typography color="textSecondary">N/A</Typography>;
+//   }
+
+//   const feeInBTC = satoshisToBitcoins(feeInSats.toString());
+
+//   return (
+//     <Tooltip
+//       title={
+//         <Box>
+//           <Typography variant="caption">
+//             {`${feeInSats.toLocaleString()} sats`}
+//           </Typography>
+//           <br />
+//           <Typography variant="caption">{`${feeInBTC} BTC`}</Typography>
+//         </Box>
+//       }
+//     >
+//       <Box>
+//         <Typography variant="body2" color="textPrimary">
+//           {feeInSats.toLocaleString()} sats
+//         </Typography>
+//         <Typography variant="caption" color="textSecondary">
+//           {feeInBTC} BTC
+//         </Typography>
+//       </Box>
+//     </Tooltip>
+//   );
+// };
+
+// Table header with sort labels
+const TransactionTableHeader: React.FC<{
+  columns: Array<{ id: string; label: string; sortable: boolean }>;
+  sortBy: string;
+  sortDirection: "asc" | "desc";
+  onSort: (property: keyof TransactionT) => void;
+}> = ({ columns, sortBy, sortDirection, onSort }) => (
+  <TableHead>
+    <TableRow>
+      {columns.map((column) => (
+        <TableCell key={column.id}>
+          {column.sortable ? (
+            <TableSortLabel
+              active={sortBy === column.id}
+              direction={sortDirection}
+              onClick={() => onSort(column.id as keyof TransactionT)}
+            >
+              {column.label}
+            </TableSortLabel>
+          ) : (
+            column.label
+          )}
+        </TableCell>
+      ))}
+    </TableRow>
+  </TableHead>
+);
+
+// A single transaction row
+const TransactionTableRow: React.FC<{
+  tx: TransactionT;
+  network?: string;
+  onClickTransaction?: (txid: string) => void;
+  onCopySuccess: () => void;
+}> = ({ tx, network, onClickTransaction, onCopySuccess }) => (
+  <TableRow>
+    <TableCell>
+      <Box display="flex" alignItems="center">
+        <Tooltip title={tx.txid}>
+          <Chip
+            label={`${tx.txid.substring(0, 8)}...`}
+            variant="outlined"
+            size="small"
+            onClick={(e) => {
+              e.stopPropagation(); // Prevent row click from firing
+              navigator.clipboard
+                .writeText(tx.txid)
+                .then(() => {
+                  onCopySuccess();
+                })
+                .catch((err) => {
+                  console.error("Could not copy text: ", err);
+                });
+            }}
+            style={{ cursor: "pointer" }}
+          />
+        </Tooltip>
+        {tx.isSpent && (
+          <Tooltip title="This transaction created UTXOs that have been spent">
+            <Box display="flex" alignItems="center" ml={1}>
+              <Chip
+                label="Spent"
+                size="small"
+                color="default"
+                sx={{ fontSize: "0.7rem" }}
+              />
+              <HelpOutlineIcon
+                fontSize="small"
+                sx={{ fontSize: "0.9rem", ml: 0.5, color: "text.secondary" }}
+              />
+            </Box>
+          </Tooltip>
+        )}
+      </Box>
+    </TableCell>
+    <TableCell>{formatRelativeTime(tx.status.blockTime)}</TableCell>
+    <TableCell>{tx.size}</TableCell>
+    {/* <TableCell>
+      <FeeDisplay feeInSats={tx.fee} />
+    </TableCell> */}
+    <TableCell>
+      <Chip
+        label={tx.status.confirmed ? "Confirmed" : "Pending"}
+        color={tx.status.confirmed ? "success" : "warning"}
+        size="small"
+      />
+    </TableCell>
+    <TableCell>
+      {network && (
+        <Tooltip title="View in your preferred block explorer">
+          <IconButton
+            size="small"
+            onClick={(e) => {
+              e.stopPropagation();
+              // Let parent handle block explorer navigation
+              onClickTransaction?.(tx.txid);
+            }}
+          >
+            <OpenInNew fontSize="small" />
+          </IconButton>
+        </Tooltip>
+      )}
+    </TableCell>
+  </TableRow>
+);
+
+export const TransactionTable: React.FC<TransactionTableProps> = ({
+  transactions,
+  onSort,
+  sortBy,
+  sortDirection,
+  network,
+  onClickTransaction,
+}) => {
+  const [snackbarOpen, setSnackbarOpen] = useState(false);
+
+  return (
+    <>
+      <TableContainer component={Paper}>
+        <Table size="small">
+          <TransactionTableHeader
+            columns={columns}
+            sortBy={sortBy}
+            sortDirection={sortDirection}
+            onSort={onSort}
+          />
+          <TableBody>
+            {transactions.length === 0 ? (
+              <TableRow>
+                <TableCell colSpan={columns.length} align="center">
+                  No transactions found
+                </TableCell>
+              </TableRow>
+            ) : (
+              transactions.map((tx) => (
+                <TransactionTableRow
+                  key={tx.txid}
+                  tx={tx}
+                  network={network}
+                  onClickTransaction={onClickTransaction}
+                  onCopySuccess={() => setSnackbarOpen(true)}
+                />
+              ))
+            )}
+          </TableBody>
+        </Table>
+      </TableContainer>
+      <Snackbar
+        open={snackbarOpen}
+        autoHideDuration={3000}
+        onClose={() => setSnackbarOpen(false)}
+      >
+        <Alert
+          onClose={() => setSnackbarOpen(false)}
+          severity="success"
+          sx={{ width: "100%" }}
+        >
+          Transaction ID copied to clipboard
+        </Alert>
+      </Snackbar>
+    </>
+  );
+};
diff --git a/apps/coordinator/src/components/Wallet/TransactionsTab/hooks.ts b/apps/coordinator/src/components/Wallet/TransactionsTab/hooks.ts
new file mode 100644
index 0000000000..31148899d1
--- /dev/null
+++ b/apps/coordinator/src/components/Wallet/TransactionsTab/hooks.ts
@@ -0,0 +1,280 @@
+import { useState, useEffect, useCallback } from "react";
+import { useSelector } from "react-redux";
+import { blockExplorerTransactionURL } from "@caravan/bitcoin";
+import { Transaction, SortDirection, SortBy } from "./types";
+import { useGetClient } from "../../../hooks/client";
+
+/**
+ * Custom hook to manage transaction fetching and state
+ */
+export const useFetchTransactions = () => {
+  const [transactions, setTransactions] = useState<Transaction[]>([]);
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const [mounted, setMounted] = useState(true);
+
+  // Use selectors to get state from Redux store
+  const deposits = useSelector((state: any) => state.wallet.deposits);
+  const change = useSelector((state: any) => state.wallet.change);
+
+  const blockchainClient = useGetClient();
+
+  // Helper function to fetch individual transaction details
+  const fetchTransactionDetails = async (txid: string) => {
+    try {
+      if (!blockchainClient) {
+        throw new Error("No blockchain client available");
+      }
+      return await blockchainClient.getTransaction(txid);
+    } catch (err) {
+      console.error(`Error fetching tx ${txid}:`, err);
+      return null;
+    }
+  };
+
+  // Fetch transactions
+  const fetchTransactions = useCallback(async () => {
+    if (!mounted) return;
+
+    setIsLoading(true);
+    setError(null);
+
+    try {
+      if (!blockchainClient) {
+        throw new Error("No blockchain client available");
+      }
+
+      // Set to store transaction IDs
+      const txids = new Set<string>();
+
+      // As some addresses can belong to UTXO's that no longer belong to the wallet so we separate addresses with active UTXOs and spent UTXOs
+      const addressesWithActiveUTXOs = new Set<string>();
+      const addressesWithSpentUTXOs = new Set<string>();
+
+      // Track which transactions are from spent UTXOs
+      const spentTxids = new Set<string>();
+
+      //Now we process all nodes to categorize addresses
+      [
+        ...Object.values(deposits.nodes),
+        ...Object.values(change.nodes),
+      ].forEach((node: any) => {
+        if (node && node.multisig && node.multisig.address) {
+          if (node.utxos && node.utxos.length > 0) {
+            // Address has active UTXOs
+            addressesWithActiveUTXOs.add(node.multisig.address);
+
+            // Add transaction IDs from active UTXOs
+            node.utxos.forEach((utxo: any) => {
+              if (utxo.txid) txids.add(utxo.txid);
+            });
+          } else if (node.addressUsed) {
+            // Address has been used but has no UTXOs (spent)
+            addressesWithSpentUTXOs.add(node.multisig.address);
+          }
+        }
+      });
+
+      // Only fetch transaction history for addresses with spent UTXOs
+      for (const address of addressesWithSpentUTXOs) {
+        try {
+          const txHistory =
+            await blockchainClient.getAddressTransactions(address);
+          txHistory.forEach((tx) => {
+            txids.add(tx.txid);
+            // Mark this transaction as coming from a spent UTXO needed to tag spent TX's in tableRow
+            spentTxids.add(tx.txid);
+          });
+        } catch (err) {
+          console.error(`Error fetching history for address ${address}:`, err);
+        }
+      }
+
+      // Fetch transaction details in parallel
+      const txPromises = Array.from(txids).map((txid) =>
+        fetchTransactionDetails(txid),
+      );
+
+      const txDetails = await Promise.all(txPromises);
+
+      if (mounted) {
+        setTransactions(
+          txDetails.filter((tx): tx is Transaction => tx !== null),
+        );
+        setError(null);
+      }
+    } catch (err) {
+      if (mounted) {
+        setError((err as Error).message);
+      }
+    } finally {
+      if (mounted) {
+        setIsLoading(false);
+      }
+    }
+  }, [blockchainClient, deposits.nodes, change.nodes, mounted]);
+
+  // Initialize and clean up
+  useEffect(() => {
+    fetchTransactions();
+
+    return () => {
+      setMounted(false);
+    };
+  }, [fetchTransactions]);
+
+  return {
+    transactions,
+    isLoading,
+    error,
+    fetchTransactions,
+  };
+};
+
+/**
+ * Custom hook to manage transaction sorting
+ */
+export const useSortedTransactions = (transactions: Transaction[]) => {
+  const [sortBy, setSortBy] = useState<SortBy>("blockTime");
+  const [sortDirection, setSortDirection] = useState<SortDirection>("desc");
+
+  // Handle sorting
+  const handleSort = (property: SortBy) => {
+    const isAsc = sortBy === property && sortDirection === "asc";
+    setSortDirection(isAsc ? "desc" : "asc");
+    setSortBy(property);
+  };
+
+  // Sort transactions
+  const getSortedTransactions = useCallback(
+    (txs: Transaction[]) => {
+      return [...txs].sort((a, b) => {
+        // Handle different sorting properties
+        let comparison = 0;
+
+        if (sortBy === "blockTime") {
+          comparison = (a.status.blockTime || 0) - (b.status.blockTime || 0);
+        } else if (sortBy === "size") {
+          comparison = a.size - b.size;
+        } else if (sortBy === "fee") {
+          comparison = (a.fee || 0) - (b.fee || 0);
+        } else {
+          // For any other property that might be sortable
+          const aValue = a[sortBy as keyof Transaction];
+          const bValue = b[sortBy as keyof Transaction];
+
+          if (typeof aValue === "number" && typeof bValue === "number") {
+            comparison = aValue - bValue;
+          } else if (typeof aValue === "string" && typeof bValue === "string") {
+            comparison = aValue.localeCompare(bValue);
+          }
+        }
+
+        return sortDirection === "desc" ? -comparison : comparison;
+      });
+    },
+    [sortBy, sortDirection],
+  );
+
+  // Split and sort transactions
+  const pendingTxs = getSortedTransactions(
+    transactions.filter((tx) => !tx.status.confirmed),
+  );
+  const confirmedTxs = getSortedTransactions(
+    transactions.filter((tx) => tx.status.confirmed),
+  );
+
+  return {
+    sortBy,
+    sortDirection,
+    handleSort,
+    pendingTxs,
+    confirmedTxs,
+  };
+};
+
+/**
+ * Custom hook to manage pagination
+ */
+export const usePagination = (totalItems: number) => {
+  const [page, setPage] = useState(1);
+  const [rowsPerPage, setRowsPerPage] = useState(5);
+
+  // Reset page when total items changes
+  useEffect(() => {
+    setPage(1);
+  }, [totalItems]);
+
+  // Get current page items
+  const getCurrentPageItems = useCallback(
+    <T>(items: T[]) => {
+      const startIndex = (page - 1) * rowsPerPage;
+      return items.slice(startIndex, startIndex + rowsPerPage);
+    },
+    [page, rowsPerPage],
+  );
+
+  // Calculate total pages
+  const totalPages = Math.ceil(totalItems / rowsPerPage);
+
+  // Handle page change
+  const handlePageChange = (
+    _event: React.ChangeEvent<unknown>,
+    newPage: number,
+  ) => {
+    setPage(newPage);
+  };
+
+  // Handle rows per page change
+  const handleRowsPerPageChange = (event: { target: { value: string } }) => {
+    setRowsPerPage(Number(event.target.value));
+    setPage(1); // Reset to first page when changing rows per page
+  };
+
+  return {
+    page,
+    rowsPerPage,
+    totalPages,
+    getCurrentPageItems,
+    handlePageChange,
+    handleRowsPerPageChange,
+  };
+};
+
+/**
+ * Custom hook to handle clicking on transaction links
+ */
+export const useHandleExplorerLinkClick = () => {
+  const network = useSelector((state: any) => state.settings.network);
+  const client = useSelector((state: any) => state.client);
+
+  return useCallback(
+    (txid: string) => {
+      // Check if user is using a private node
+      const isPrivateNode = client.type === "private";
+
+      if (isPrivateNode) {
+        // If using private node, warn about privacy implications
+        const confirmed = window.confirm(
+          "Opening in a block explorer may expose your wallet activity to third parties. Continue?",
+        );
+        if (!confirmed) return;
+      }
+
+      // Determine which block explorer to use based on the blockchain client type
+      let explorerUrl = blockExplorerTransactionURL(txid, network);
+
+      if (client.blockchainClient?.type) {
+        const clientType = client.blockchainClient.type;
+        if (clientType === "mempool") {
+          explorerUrl = `https://${network === "mainnet" ? "" : "testnet."}mempool.space/tx/${txid}`;
+        } else if (clientType === "blockstream") {
+          explorerUrl = `https://blockstream.info/${network === "mainnet" ? "" : "testnet/"}tx/${txid}`;
+        }
+      }
+
+      window.open(explorerUrl, "_blank");
+    },
+    [network, client],
+  );
+};
diff --git a/apps/coordinator/src/components/Wallet/TransactionsTab/index.tsx b/apps/coordinator/src/components/Wallet/TransactionsTab/index.tsx
new file mode 100644
index 0000000000..8998c99669
--- /dev/null
+++ b/apps/coordinator/src/components/Wallet/TransactionsTab/index.tsx
@@ -0,0 +1,211 @@
+import React, { useState } from "react";
+import { useSelector } from "react-redux";
+import {
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Pagination,
+  FormControl,
+  InputLabel,
+  Select,
+  MenuItem,
+} from "@mui/material";
+import { Refresh } from "@mui/icons-material";
+import { TransactionTable } from "./TransactionsTable";
+import {
+  useFetchTransactions,
+  useSortedTransactions,
+  usePagination,
+  useHandleExplorerLinkClick,
+} from "./hooks";
+
+/**
+ * TRANSACTIONS HISTORY LIMITATION
+ *
+ * Only pending transactions are shown in this implementation.
+ *
+ * The commented code reflects the changes done to comment out the Completed Transactions for now .
+ *
+ * Tracking confirmed/spent transactions is challenging because as UTXOs are spent,
+ * they disappear from the wallet state. In private clients, we need a different
+ * approach to track historical transactions since we can't collect transaction IDs
+ * directly from UTXO data that no longer exists in the wallet.
+ *
+ * This would require maintaining a separate transaction history database or querying
+ * all address histories, which has privacy implications.
+ */
+
+const TransactionsTab: React.FC<{ refreshWallet?: () => Promise<any> }> = ({
+  refreshWallet,
+}) => {
+  // This key state controls remounting
+  const [mountKey, setMountKey] = useState(0);
+
+  // Handle refresh by forcing remount
+  const handleRefresh = () => {
+    if (refreshWallet) {
+      refreshWallet()
+        .then(() => {
+          // Force remount by incrementing key
+          setMountKey((k) => k + 1);
+        })
+        .catch((err) => {
+          console.error("Error refreshing wallet:", err);
+          setMountKey((k) => k + 1);
+        });
+    } else {
+      // No wallet refresh function, just force remount
+      setMountKey((k) => k + 1);
+    }
+  };
+
+  // Render actual content with key to force remount
+  return <TransactionsTabContent key={mountKey} onRefresh={handleRefresh} />;
+};
+
+const TransactionsTabContent: React.FC<{ onRefresh: () => void }> = ({
+  onRefresh,
+}) => {
+  // const [tabValue, setTabValue] = useState(0);
+  const network = useSelector((state: any) => state.settings.network);
+
+  // Use our custom hooks
+  const { transactions, isLoading, error, fetchTransactions } =
+    useFetchTransactions();
+  const { sortBy, sortDirection, handleSort, pendingTxs } =
+    useSortedTransactions(transactions);
+  const handleExplorerLinkClick = useHandleExplorerLinkClick();
+
+  // Get the correct transaction list based on selected tab
+  // const currentTabTxs = tabValue === 0 ? pendingTxs : confirmedTxs;
+  const currentTabTxs = pendingTxs;
+  // Set up pagination for the current tab's transactions
+  const {
+    page,
+    rowsPerPage,
+    totalPages,
+    getCurrentPageItems,
+    handlePageChange,
+    handleRowsPerPageChange,
+  } = usePagination(currentTabTxs.length);
+
+  // Get transactions for current page
+  const currentPageTxs = getCurrentPageItems(currentTabTxs);
+
+  const handleRefreshClick = () => {
+    fetchTransactions();
+    onRefresh();
+  };
+
+  return (
+    <div>
+      <Box
+        display="flex"
+        justifyContent="space-between"
+        alignItems="center"
+        mb={2}
+      >
+        <Typography variant="h6">Pending Transactions</Typography>
+        <Tooltip title="Refresh transactions">
+          <IconButton onClick={handleRefreshClick} disabled={isLoading}>
+            <Refresh />
+          </IconButton>
+        </Tooltip>
+      </Box>
+
+      {error && (
+        <Typography color="error" gutterBottom>
+          Error: {error}
+        </Typography>
+      )}
+
+      {/* <Tabs value={tabValue} onChange={(_, value) => setTabValue(value)}>
+        <Tab
+          label={
+            <Box display="flex" alignItems="center" gap={1}>
+              <span>Pending</span>
+              <Chip
+                label={pendingTxs.length}
+                size="small"
+                color={pendingTxs.length > 0 ? "primary" : "default"}
+              />
+            </Box>
+          }
+        />
+        <Tab
+          label={
+            <Box display="flex" alignItems="center" gap={1}>
+              <span>Confirmed</span>
+              <Chip label={confirmedTxs.length} size="small" />
+            </Box>
+          }
+        />
+      </Tabs> */}
+
+      <Box mt={2}>
+        {isLoading ? (
+          <Box display="flex" justifyContent="center" p={3}>
+            <CircularProgress />
+          </Box>
+        ) : (
+          <>
+            <TransactionTable
+              transactions={currentPageTxs}
+              onSort={handleSort}
+              sortBy={sortBy}
+              sortDirection={sortDirection}
+              network={network}
+              onClickTransaction={handleExplorerLinkClick}
+            />
+            {/* Pagination controls */}
+            {currentTabTxs.length > 0 && (
+              <Box
+                display="flex"
+                justifyContent="space-between"
+                alignItems="center"
+                mt={2}
+                px={1}
+              >
+                <FormControl
+                  variant="outlined"
+                  size="small"
+                  sx={{ minWidth: 120 }}
+                >
+                  <InputLabel id="rows-per-page-label">Rows</InputLabel>
+                  <Select
+                    labelId="rows-per-page-label"
+                    value={rowsPerPage.toString()}
+                    onChange={handleRowsPerPageChange}
+                    label="Rows"
+                  >
+                    <MenuItem value="5">5</MenuItem>
+                    <MenuItem value="10">10</MenuItem>
+                    <MenuItem value="25">25</MenuItem>
+                    <MenuItem value="50">50</MenuItem>
+                  </Select>
+                </FormControl>
+
+                <Box display="flex" alignItems="center">
+                  <Typography variant="body2" color="textSecondary" mr={2}>
+                    {`${(page - 1) * rowsPerPage + 1}-${Math.min(page * rowsPerPage, currentTabTxs.length)} of ${currentTabTxs.length}`}
+                  </Typography>
+                  <Pagination
+                    count={totalPages}
+                    page={page}
+                    onChange={handlePageChange}
+                    color="primary"
+                    size="small"
+                  />
+                </Box>
+              </Box>
+            )}
+          </>
+        )}
+      </Box>
+    </div>
+  );
+};
+
+export default TransactionsTab;
diff --git a/apps/coordinator/src/components/Wallet/TransactionsTab/types.ts b/apps/coordinator/src/components/Wallet/TransactionsTab/types.ts
new file mode 100644
index 0000000000..8cee85aafb
--- /dev/null
+++ b/apps/coordinator/src/components/Wallet/TransactionsTab/types.ts
@@ -0,0 +1,48 @@
+// TODO: This should come from the `clients` package
+// Types for the transaction data
+export interface Transaction {
+  txid: string;
+  version: number;
+  locktime: number;
+  vin: any[]; // Input vector
+  vout: any[]; // Output vector
+  weight: number;
+  status: {
+    confirmed: boolean;
+    blockTime?: number;
+    blockHeight?: number;
+  };
+  size: number;
+  fee: number;
+}
+
+export interface TransactionTableProps {
+  transactions: Transaction[];
+  onSort: (property: keyof TransactionT) => void;
+  sortBy: string;
+  sortDirection: "asc" | "desc";
+  network?: string;
+  onClickTransaction?: (txid: string) => void;
+}
+
+// How our Transaction Table's should look like
+export interface TransactionT {
+  txid: string;
+  status: {
+    confirmed: boolean;
+    blockTime?: number;
+    blockHeight?: number;
+  };
+  size: number;
+  fee: number;
+  isSpent?: boolean;
+}
+
+// For MUI's Select component
+export type SelectChangeEvent<Value = string> =
+  | (Event & { target: { value: Value; name: string } })
+  | React.ChangeEvent<HTMLInputElement>;
+
+// Sorting parameters
+export type SortDirection = "asc" | "desc";
+export type SortBy = keyof TransactionT | "blockTime";
diff --git a/apps/coordinator/src/components/Wallet/WalletControl.jsx b/apps/coordinator/src/components/Wallet/WalletControl.jsx
index 0eb62573ed..8558206b83 100644
--- a/apps/coordinator/src/components/Wallet/WalletControl.jsx
+++ b/apps/coordinator/src/components/Wallet/WalletControl.jsx
@@ -16,6 +16,7 @@ import { setRequiredSigners as setRequiredSignersAction } from "../../actions/tr
 import { MAX_FETCH_UTXOS_ERRORS, MAX_TRAILING_EMPTY_NODES } from "./constants";
 import WalletDeposit from "./WalletDeposit";
 import WalletSpend from "./WalletSpend";
+import TransactionsTab from "./TransactionsTab/index";
 import { SlicesTableContainer } from "../Slices";
 
 class WalletControl extends React.Component {
@@ -41,6 +42,11 @@ class WalletControl extends React.Component {
             <Tab label="Addresses" value={WALLET_MODES.VIEW} key={0} />,
             <Tab label="Receive" value={WALLET_MODES.DEPOSIT} key={1} />,
             <Tab label="Send" value={WALLET_MODES.SPEND} key={2} />,
+            <Tab
+              label="Transactions"
+              value={WALLET_MODES.TRANSACTIONS}
+              key={3}
+            />,
           ]}
         </Tabs>
         <Box mt={2}>{this.renderModeComponent()}</Box>
@@ -55,6 +61,8 @@ class WalletControl extends React.Component {
       if (walletMode === WALLET_MODES.SPEND)
         return <WalletSpend addNode={addNode} updateNode={updateNode} />;
       if (walletMode === WALLET_MODES.VIEW) return <SlicesTableContainer />;
+      if (walletMode === WALLET_MODES.TRANSACTIONS)
+        return <TransactionsTab refreshWallet={this.props.refreshNodes} />;
     }
     const progress = this.progress();
     return [
@@ -114,6 +122,7 @@ WalletControl.propTypes = {
   signatureImporters: PropTypes.shape({}).isRequired,
   updateNode: PropTypes.func.isRequired,
   walletMode: PropTypes.number.isRequired,
+  refreshNodes: PropTypes.func.isRequired,
 };
 
 function mapStateToProps(state) {
diff --git a/apps/coordinator/src/components/Wallet/WalletGenerator.jsx b/apps/coordinator/src/components/Wallet/WalletGenerator.jsx
index 584afe443a..bce74286ad 100644
--- a/apps/coordinator/src/components/Wallet/WalletGenerator.jsx
+++ b/apps/coordinator/src/components/Wallet/WalletGenerator.jsx
@@ -366,7 +366,11 @@ class WalletGenerator extends React.Component {
     if (this.extendedPublicKeyCount() === totalSigners) {
       if (generating && !configuring) {
         return (
-          <WalletControl addNode={this.addNode} updateNode={this.updateNode} />
+          <WalletControl
+            addNode={this.addNode}
+            updateNode={this.updateNode}
+            refreshNodes={this.refreshNodes}
+          />
         );
       }
       if (!hasConflict) {
diff --git a/apps/coordinator/src/reducers/transactionReducer.js b/apps/coordinator/src/reducers/transactionReducer.js
index 2c96f5f8a5..d057eaab2b 100644
--- a/apps/coordinator/src/reducers/transactionReducer.js
+++ b/apps/coordinator/src/reducers/transactionReducer.js
@@ -103,6 +103,12 @@ export const initialState = () => ({
   signingKeys: [0, 0], // default 2 required signers
   spendingStep: SPEND_STEP_CREATE,
   unsignedPSBT: "",
+  transactions: {
+    transactions: [],
+    isLoading: false, // Array to store actual transactions
+    error: null,
+    lastUpdated: null,
+  },
 });
 
 function updateInputs(state, action) {


Reviews:
user: login: bucko13
body: I haven't been able to test because the client method isn't available, but excited for this change! 

I think a lot of my requests for changes are due to you using old code as a reference for this change. Sorry for that confusion with the old code. Hopefully my comments give an idea of the preferred patterns though. 
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-06T23:04:19Z
user: login: Legend101Zz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-07T19:41:08Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>packages/caravan-clients/src/types.ts (1)</summary><blockquote>

`24-24`: _:hammer_and_wrench: Refactor suggestion_

**Replace `any[]` with specific types for input and output vectors.**

Using `any[]` reduces type safety. Consider using the already defined `RawTxInput[]` and `RawTxOutput[]` types.

```diff
-  vin: any[]; // Input vector
-  vout: any[]; // Output vector
+  vin: RawTxInput[]; // Input vector
+  vout: RawTxOutput[]; // Output vector
```


Also applies to: 25-25

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (16)</summary><blockquote>

<details>
<summary>packages/caravan-clients/src/bitcoind.ts (7)</summary><blockquote>

`27-61`: **Consider using incremental request IDs and enhanced error messages.**

While using a static RPC request ID of `0` is harmless for single calls, assigning a unique/bumped ID can help correlate logs in more complex scenarios or when batching requests. Also consider enriching the catch block to include the RPC method name for easier debugging when errors occur.

---

`72-79`: **Ensure robust type guarding for error objects.**

Checking `"response" in error` is valid, but you might improve safety by narrowing the type earlier or using a type predicate to confirm the error structure. This will avoid possible undefined property access on unusual error objects.

---

`90-102`: **Optional validation of client parameters.**

`bitcoindParams` assumes `patient` usage of valid config, but consider validating `username`, `password`, or `url` for empty strings or injecting default values. This would help prevent subtle runtime errors if misconfigured.

---

`117-167`: **Consider batching calls to reduce round-trip times.**

`bitcoindListUnspent` retrieves UTXOs and individually fetches transaction details with multiple `gettransaction` calls. For high-volume addresses, consider leveraging batched RPC requests or a more efficient method like `getrawtransaction` in a single call to improve performance.

---

`245-269`: **Use error logging best practices.**

Printing a generic "Error broadcasting transaction:" to `console.log` is functional, but consider `console.error` or structured logging for clarity and helpful context. This can aid in debugging critical broadcast failures.

---

`282-325`: **Revisit fire-and-forget approach for `rescan` operations.**

Returning an empty result immediately when `rescan` is true may mask potential errors that occur during the asynchronous rescan. If real-time feedback on `rescan` success is needed, consider capturing the promise or offering a callback mechanism.

---

`335-353`: **Align `bitcoindRawTxData` return with standard patterns.**

This method returns the entire RPC response object instead of just `result`, which differs from other wrappers that typically return `resp.result`. Consider returning a consistent shape (e.g., only the transaction details) for uniform caller handling.

</blockquote></details>
<details>
<summary>packages/caravan-clients/src/client.ts (2)</summary><blockquote>

`49-88`: **`normalizeTransactionData` finalizes consistent data representation.**

The function effectively unifies transaction data from different client types. If you anticipate more specialized fields (e.g., witness data or custom block info), consider adding optional props or type narrowing to represent them more thoroughly.

---

`218-264`: **Potential performance overhead with multiple `gettransaction` calls.**

As with the approach in `bitcoindListUnspent`, fetching raw transactions for each record in `listtransactions` can be expensive for large sets. Evaluate batch calls or an alternative if this route is heavily used. Otherwise, this approach is straightforward and consistent.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 242-242: Unnecessary use of boolean literals in conditional expression.

Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
 Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with


(lint/complexity/noUselessTernary)

</details>

</details>

</blockquote></details>
<details>
<summary>packages/caravan-clients/src/client.test.ts (1)</summary><blockquote>

`834-834`: **Mocking import descriptor response.**

Setting `{ result: null, id: 0 }` is fine for testing, though consider verifying correct success/failure handling around the import results if your production code eventually uses that return data.

</blockquote></details>
<details>
<summary>apps/coordinator/src/components/Wallet/WalletControl.jsx (1)</summary><blockquote>

`45-49`: **Consider accessibility improvements for the Transactions tab.**

The tab implementation looks good, but consider adding an `aria-label` to improve accessibility for screen readers.

```diff
 <Tab
   label="Transactions"
   value={WALLET_MODES.TRANSACTIONS}
   key={3}
+  aria-label="View transaction history"
 />
```

</blockquote></details>
<details>
<summary>apps/coordinator/src/components/Wallet/TransactionsTable.tsx (3)</summary><blockquote>

`49-56`: **Consider using more specific types for column definitions.**

The column definitions use string literals for `id` which could be typed more strictly using a union type of valid column keys.

```diff
+type ColumnId = 'txid' | 'blockTime' | 'size' | 'fee' | 'status' | 'actions';
+
 const columns = [
-  { id: "txid", label: "Transaction ID", sortable: false },
+  { id: "txid" as ColumnId, label: "Transaction ID", sortable: false },
   // ... other columns
 ];
```

---

`59-88`: **Optimize FeeDisplay component with memoization.**

The FeeDisplay component recalculates values on every render. Consider using `useMemo` to optimize performance, especially for the fee conversion calculations.

```diff
-const FeeDisplay: React.FC<{ feeInSats?: number }> = ({ feeInSats }) => {
+const FeeDisplay: React.FC<{ feeInSats?: number }> = React.memo(({ feeInSats }) => {
   if (feeInSats === null || feeInSats === undefined) {
     return <Typography color="textSecondary">N/A</Typography>;
   }
 
-  const feeInBTC = satoshisToBitcoins(feeInSats.toString());
+  const feeInBTC = React.useMemo(() => 
+    satoshisToBitcoins(feeInSats.toString()),
+    [feeInSats]
+  );
   // ... rest of the component
-};
+});
```

---

`127-172`: **Optimize transaction row click handler.**

The click handler could be memoized to prevent unnecessary re-renders of table rows.

```diff
-const renderTransactionRow = (tx: Transaction) => (
+const renderTransactionRow = React.useCallback((tx: Transaction) => (
   <TableRow
     key={tx.txid}
     hover
     onClick={() => onClickTransaction?.(tx.txid)}
     style={{ cursor: onClickTransaction ? "pointer" : "default" }}
   >
     // ... row content
   </TableRow>
-);
+), [onClickTransaction]);
```

</blockquote></details>
<details>
<summary>packages/caravan-clients/src/types.ts (1)</summary><blockquote>

`47-60`: **Add JSDoc documentation for transaction-related interfaces.**

Consider adding JSDoc comments to improve code documentation and IDE support.

```diff
+/**
+ * Represents a raw transaction input with optional script and witness data.
+ */
 interface RawTxInput {
   // ... existing properties
 }

+/**
+ * Represents a raw transaction output with script details and optional metadata.
+ */
 interface RawTxOutput {
   // ... existing properties
 }

+/**
+ * Represents the confirmation status of a raw transaction.
+ */
 interface RawTxStatus {
   // ... existing properties
 }
```


Also applies to: 62-68, 70-75

</blockquote></details>
<details>
<summary>apps/coordinator/src/components/Wallet/TransactionsTab.tsx (1)</summary><blockquote>

`138-160`: **Simplify transaction sorting logic.**

The sorting logic could be simplified using a lookup object for sort comparisons.

```diff
 const getSortedTransactions = useCallback(
   (txs: Transaction[]) => {
+    const sortComparisons = {
+      blockTime: (a: Transaction, b: Transaction) => 
+        (a.status.blockTime || 0) - (b.status.blockTime || 0),
+      size: (a: Transaction, b: Transaction) => 
+        a.size - b.size,
+      fee: (a: Transaction, b: Transaction) => 
+        (a.fee || 0) - (b.fee || 0),
+    };
+
     return [...txs].sort((a, b) => {
-      let comparison = 0;
-      switch (sortBy) {
-        case "blockTime":
-          comparison = (a.status.blockTime || 0) - (b.status.blockTime || 0);
-          break;
-        case "size":
-          comparison = a.size - b.size;
-          break;
-        case "fee":
-          comparison = (a.fee || 0) - (b.fee || 0);
-          break;
-        default:
-          return 0;
-      }
+      const comparison = sortComparisons[sortBy as keyof typeof sortComparisons]?.(a, b) || 0;
       return sortDirection === "desc" ? -comparison : comparison;
     });
   },
   [sortBy, sortDirection],
 );
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 205ced73dc1dfa1d10f28e18371dbb70d4dcdf5d and 56d493634288eb71a322558ab9de43e7a9499fd4.

</details>

<details>
<summary>📒 Files selected for processing (13)</summary>

* `.changeset/empty-ants-destroy.md` (1 hunks)
* `apps/coordinator/src/actions/transactionActions.js` (1 hunks)
* `apps/coordinator/src/actions/walletActions.js` (1 hunks)
* `apps/coordinator/src/components/Wallet/TransactionsTab.tsx` (1 hunks)
* `apps/coordinator/src/components/Wallet/TransactionsTable.tsx` (1 hunks)
* `apps/coordinator/src/components/Wallet/WalletControl.jsx` (3 hunks)
* `apps/coordinator/src/reducers/transactionReducer.js` (1 hunks)
* `packages/caravan-clients/src/bitcoind.js` (0 hunks)
* `packages/caravan-clients/src/bitcoind.ts` (1 hunks)
* `packages/caravan-clients/src/client.test.ts` (7 hunks)
* `packages/caravan-clients/src/client.ts` (5 hunks)
* `packages/caravan-clients/src/types.ts` (1 hunks)
* `packages/caravan-clients/src/wallet.ts` (2 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* packages/caravan-clients/src/bitcoind.js

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (2)</summary>

* apps/coordinator/src/actions/transactionActions.js
* packages/caravan-clients/src/wallet.ts

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: ci (20.x)

</details>

<details>
<summary>🔇 Additional comments (14)</summary><blockquote>

<details>
<summary>packages/caravan-clients/src/bitcoind.ts (2)</summary>

`177-207`: **Gracefully handle address not found errors.**

The code properly checks for isolated cases (e.g., watch-only addresses), but you might extend logs or rethrow with additional context so that higher-level callers can differentiate “address not found” from general errors more distinctly.

---

`218-234`: **Validate feerate presence before arithmetic.**

`resp.result.feerate` could theoretically be undefined if `estimatesmartfee` yields no data. Consider guarding against undefined before converting to sats/byte to avoid runtime errors.

</details>
<details>
<summary>packages/caravan-clients/src/client.ts (3)</summary>

`22-30`: **New imports align with expanded functionality.**

These imports (e.g., `TransactionDetails`, `RawTransactionData`, etc.) enhance type safety across the module. Nice approach to ensure consistent usage of typed transaction data.

---

`495-508`: **Clarify the return shape of `getTransactionHex`.**

When the client is PRIVATE, this function returns the entire RPC response, while public explorers typically return just the transaction hex string. Consider normalizing the return type to avoid confusion in upper layers.

---

`510-534`: **`getTransaction` method ensures consistent transaction retrieval.**

Good job bridging multiple backends (PRIVATE/BLOCKSTREAM/MEMPOOL) and normalizing results. If you plan to support more client types, direct error for invalid client type is a safe fail-fast approach.

</details>
<details>
<summary>packages/caravan-clients/src/client.test.ts (4)</summary>

`11-11`: **Expanded import for broader test coverage.**

Importing `RawTransactionData` and `TransactionDetails` ensures the test suite can validate complex transaction scenarios. This helps maintain type consistency.

---

`148-149`: **Concise mock for transaction broadcast.**

Returning a direct TXID string simplifies test assertion. This is a practical improvement over providing a nested object structure when only the ID is required.

---

`895-895`: **Consistent test setup for `getWalletInfo`.**

Same strategy here: returning `{ result: null, id: 0 }` ensures the test passes and remains consistent. If the code later depends on actual wallet info in `result`, ensure to expand the mock accordingly.

---

`1132-1357`: **Thorough test suite for `getTransaction`.**

The new tests cover different client types, ensuring correct normalization and error handling. Great job verifying that invalid client types and failed requests throw appropriate errors. This robust coverage significantly reduces regression risks.

</details>
<details>
<summary>apps/coordinator/src/components/Wallet/WalletControl.jsx (2)</summary>

`19-19`: **LGTM!**

The import statement is correctly placed and follows the existing import pattern.

---

`64-64`: **LGTM!**

The conditional rendering of the TransactionsTab is correctly implemented.

</details>
<details>
<summary>apps/coordinator/src/actions/walletActions.js (1)</summary>

`36-36`: **LGTM!**

The addition of the `TRANSACTIONS` mode aligns with the PR objectives to introduce a transaction history tab with pending/confirmed views.

</details>
<details>
<summary>apps/coordinator/src/reducers/transactionReducer.js (1)</summary>

`106-111`: **LGTM!**

The new `transactions` state follows Redux best practices for managing async data with loading, error, and caching states.

</details>
<details>
<summary>.changeset/empty-ants-destroy.md (1)</summary>

`1-6`: **LGTM!**

The changeset correctly documents the addition of the `getTransaction` method and follows the conventional format for version bumping.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-21T14:23:44Z
user: login: Legend101Zz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-21T14:27:00Z
user: login: Legend101Zz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-21T16:39:25Z
user: login: Legend101Zz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-21T16:39:53Z
user: login: Legend101Zz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-21T16:41:00Z
user: login: Legend101Zz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-21T16:41:39Z
user: login: Legend101Zz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-21T16:51:01Z
user: login: bucko13
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T04:20:43Z
user: login: Legend101Zz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T16:43:32Z
user: login: bucko13
body: I did a little bit of testing. Seems to be working for the most part. I've left some code comments to address, a mix of aesthetic and more substantive. Getting really close though!
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T22:07:39Z
user: login: Legend101Zz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T10:31:54Z
user: login: Legend101Zz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T14:01:37Z
user: login: Legend101Zz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T14:37:25Z
user: login: Legend101Zz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T19:56:09Z
user: login: Legend101Zz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T20:01:41Z
user: login: Legend101Zz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T20:13:37Z
user: login: Legend101Zz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T20:14:24Z
user: login: Legend101Zz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T20:24:07Z
user: login: Legend101Zz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T20:49:17Z
user: login: Legend101Zz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T20:49:25Z
user: login: bucko13
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T21:49:05Z
user: login: bucko13
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T21:49:27Z
user: login: bucko13
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T03:06:28Z
user: login: bucko13
body: just a couple nits but a really nice cleanup!
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T03:11:44Z
user: login: Legend101Zz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T14:11:07Z
user: login: Legend101Zz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T15:05:57Z
user: login: Legend101Zz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T15:06:42Z
user: login: Legend101Zz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T15:07:06Z
user: login: bucko13
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T03:46:52Z
user: login: bucko13
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T03:58:04Z

Review comments:
diff_hunk: @@ -0,0 +1,361 @@
+import React, { useState, useEffect } from "react";
+import PropTypes from "prop-types";
+import { connect } from "react-redux";
+import { styled } from "@mui/material/styles";
+import {
+  Table,
+  TableBody,
+  TableCell,
+  TableContainer,
+  TableHead,
+  TableRow,
+  Paper,
+  Tabs,
+  Tab,
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Card,
+  CardHeader,
+  CardContent,
+  Chip,
+} from "@mui/material";
+import { OpenInNew, Refresh, Schedule } from "@mui/icons-material";
+import { blockExplorerTransactionURL } from "@caravan/bitcoin";
+import {
+  setTransactions,
+  setTransactionsLoading,
+  setTransactionsError,
+} from "../../actions/transactionActions";
+import { updateBlockchainClient } from "../../actions/clientActions";
+
+const StyledCard = styled(Card)(({ theme }) => ({
+  marginBottom: theme.spacing(3),
+  height: "100%",
+}));
+
+const StyledTableCell = styled(TableCell)(({ theme }) => ({
+  fontWeight: theme.typography.fontWeightMedium,
+}));
+
+const StyledChip = styled(Chip)(({ theme }) => ({
+  marginRight: theme.spacing(1),
+}));
+
+// Helper component for tab panels, helps to switch between Pending and Completed Tx's
+function TabPanel({ children, value, index, ...other }) {
+  return (
+    <div
+      role="tabpanel"
+      hidden={value !== index}
+      id={`transaction-tabpanel-${index}`}
+      aria-labelledby={`transaction-tab-${index}`}
+      {...other}
+    >
+      {value === index && <Box py={2}>{children}</Box>}
+    </div>
+  );
+}
+
+TabPanel.propTypes = {
+  children: PropTypes.node,
+  index: PropTypes.number.isRequired,
+  value: PropTypes.number.isRequired,
+};
+
+function TransactionsTab({
+  network,
+  transactions,
+  isLoading,
+  error,
+  getBlockchainClient,
+  deposits,
+  change,
+  setTransactions,
+  setTransactionsLoading,
+  setTransactionsError,
+}) {
+  const [tabValue, setTabValue] = useState(0);
+  const [mounted, setMounted] = useState(true);
+
+  // Helper function to fetch individual transaction details
+  const fetchTransactionDetails = async (client, txid) => {
+    try {
+      const txDetails = await client.getTransaction(txid);
path: apps/coordinator/src/components/Wallet/TransactionsTab.jsx
user: login: bucko13
body: If you built this branch ontop of the #134 changes then we could test this in the vercel deployment 
created_at: 2025-01-06T22:36:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,361 @@
+import React, { useState, useEffect } from "react";
+import PropTypes from "prop-types";
+import { connect } from "react-redux";
+import { styled } from "@mui/material/styles";
+import {
+  Table,
+  TableBody,
+  TableCell,
+  TableContainer,
+  TableHead,
+  TableRow,
+  Paper,
+  Tabs,
+  Tab,
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Card,
+  CardHeader,
+  CardContent,
+  Chip,
+} from "@mui/material";
+import { OpenInNew, Refresh, Schedule } from "@mui/icons-material";
+import { blockExplorerTransactionURL } from "@caravan/bitcoin";
+import {
+  setTransactions,
+  setTransactionsLoading,
+  setTransactionsError,
+} from "../../actions/transactionActions";
+import { updateBlockchainClient } from "../../actions/clientActions";
+
+const StyledCard = styled(Card)(({ theme }) => ({
+  marginBottom: theme.spacing(3),
+  height: "100%",
+}));
+
+const StyledTableCell = styled(TableCell)(({ theme }) => ({
+  fontWeight: theme.typography.fontWeightMedium,
+}));
+
+const StyledChip = styled(Chip)(({ theme }) => ({
+  marginRight: theme.spacing(1),
+}));
+
+// Helper component for tab panels, helps to switch between Pending and Completed Tx's
+function TabPanel({ children, value, index, ...other }) {
+  return (
+    <div
+      role="tabpanel"
+      hidden={value !== index}
+      id={`transaction-tabpanel-${index}`}
+      aria-labelledby={`transaction-tab-${index}`}
+      {...other}
+    >
+      {value === index && <Box py={2}>{children}</Box>}
+    </div>
+  );
+}
+
+TabPanel.propTypes = {
+  children: PropTypes.node,
+  index: PropTypes.number.isRequired,
+  value: PropTypes.number.isRequired,
+};
+
+function TransactionsTab({
+  network,
+  transactions,
+  isLoading,
+  error,
+  getBlockchainClient,
+  deposits,
+  change,
+  setTransactions,
+  setTransactionsLoading,
+  setTransactionsError,
+}) {
+  const [tabValue, setTabValue] = useState(0);
+  const [mounted, setMounted] = useState(true);
+
+  // Helper function to fetch individual transaction details
+  const fetchTransactionDetails = async (client, txid) => {
path: apps/coordinator/src/components/Wallet/TransactionsTab.jsx
user: login: bucko13
body: you can also just use `getBlockchainClient` directly in the function since it's in the scope already rather than pass it around. Not a huge deal though, this is fine too if you prefer. 
created_at: 2025-01-06T22:43:02Z
author_association: CONTRIBUTOR
path: apps/coordinator/src/components/Wallet/TransactionsTab.jsx
user: login: bucko13
body: `tsx` for all new components please. 
created_at: 2025-01-06T22:45:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,361 @@
+import React, { useState, useEffect } from "react";
+import PropTypes from "prop-types";
+import { connect } from "react-redux";
+import { styled } from "@mui/material/styles";
+import {
+  Table,
+  TableBody,
+  TableCell,
+  TableContainer,
+  TableHead,
+  TableRow,
+  Paper,
+  Tabs,
+  Tab,
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Card,
+  CardHeader,
+  CardContent,
+  Chip,
+} from "@mui/material";
+import { OpenInNew, Refresh, Schedule } from "@mui/icons-material";
+import { blockExplorerTransactionURL } from "@caravan/bitcoin";
+import {
+  setTransactions,
+  setTransactionsLoading,
+  setTransactionsError,
+} from "../../actions/transactionActions";
+import { updateBlockchainClient } from "../../actions/clientActions";
+
+const StyledCard = styled(Card)(({ theme }) => ({
+  marginBottom: theme.spacing(3),
+  height: "100%",
+}));
+
+const StyledTableCell = styled(TableCell)(({ theme }) => ({
+  fontWeight: theme.typography.fontWeightMedium,
+}));
+
+const StyledChip = styled(Chip)(({ theme }) => ({
+  marginRight: theme.spacing(1),
+}));
+
+// Helper component for tab panels, helps to switch between Pending and Completed Tx's
+function TabPanel({ children, value, index, ...other }) {
+  return (
+    <div
+      role="tabpanel"
+      hidden={value !== index}
+      id={`transaction-tabpanel-${index}`}
+      aria-labelledby={`transaction-tab-${index}`}
+      {...other}
+    >
+      {value === index && <Box py={2}>{children}</Box>}
+    </div>
+  );
+}
+
+TabPanel.propTypes = {
+  children: PropTypes.node,
+  index: PropTypes.number.isRequired,
+  value: PropTypes.number.isRequired,
+};
+
+function TransactionsTab({
+  network,
+  transactions,
+  isLoading,
+  error,
+  getBlockchainClient,
+  deposits,
+  change,
+  setTransactions,
+  setTransactionsLoading,
+  setTransactionsError,
+}) {
+  const [tabValue, setTabValue] = useState(0);
+  const [mounted, setMounted] = useState(true);
+
+  // Helper function to fetch individual transaction details
+  const fetchTransactionDetails = async (client, txid) => {
+    try {
+      const txDetails = await client.getTransaction(txid);
+      return {
+        ...txDetails,
+        confirmed: txDetails.status.confirmed,
+        timestamp: txDetails.status.block_time,
+      };
+    } catch (err) {
+      console.error(`Error fetching tx ${txid}:`, err);
+      return null;
+    }
+  };
+
+  // Main function to fetch all transaction data
+  const fetchTransactions = async () => {
+    if (!mounted) return;
+    setTransactionsLoading(true);
+
+    try {
+      const blockchainClient = await getBlockchainClient();
+      if (!blockchainClient) {
+        throw new Error("No blockchain client available");
+      }
+
+      // Get unique txids from all UTXOs
+      const txids = new Set();
+      [
+        ...Object.values(deposits.nodes),
+        ...Object.values(change.nodes),
+      ].forEach((node) => {
+        node.utxos.forEach((utxo) => txids.add(utxo.txid));
+      });
+
+      // Fetch details for each transaction
+      const txPromises = Array.from(txids).map((txid) =>
+        fetchTransactionDetails(blockchainClient, txid),
+      );
+
+      const txDetails = (await Promise.all(txPromises))
+        .filter((tx) => tx !== null)
+        .sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); // Sort by timestamp
+
+      if (mounted) {
+        setTransactions(txDetails);
+        setTransactionsError(null);
+      }
+    } catch (err) {
+      if (mounted) {
+        setTransactionsError(err.message);
+      }
+    } finally {
+      if (mounted) {
+        setTransactionsLoading(false);
+      }
+    }
+  };
+
+  // Initial data fetch and cleanup
+  useEffect(() => {
+    setMounted(true);
+    fetchTransactions();
+    return () => setMounted(false);
+  }, [deposits.nodes, change.nodes]);
+
+  // Helper function to format dates
+  const formatDate = (timestamp) => {
+    if (!timestamp) return "Pending";
+    return new Date(timestamp * 1000).toLocaleString();
+  };
+
+  // Helper function to format fees
+  const formatFee = (fee) => {
+    if (!fee) return "N/A";
+    return `${fee.toLocaleString()} sats`;
+  };
+
+  // Split transactions into pending and confirmed
+  const pendingTxs =
+    transactions && Array.isArray(transactions)
+      ? transactions.filter((tx) => !tx.status?.confirmed)
+      : [];
+  const confirmedTxs =
+    transactions && Array.isArray(transactions)
+      ? transactions.filter((tx) => tx.status?.confirmed)
+      : [];
+
+  // Render the transaction table
+  const renderTransactionTable = (txs) => (
+    <TableContainer component={Paper}>
+      <Table>
+        <TableHead>
+          <TableRow>
+            <StyledTableCell>Transaction ID</StyledTableCell>
+            <StyledTableCell>Time</StyledTableCell>
+            <StyledTableCell>Details</StyledTableCell>
+            <StyledTableCell align="right">Fee</StyledTableCell>
+            {tabValue === 1 && (
+              <StyledTableCell align="right">Block</StyledTableCell>
+            )}
+            <StyledTableCell>Actions</StyledTableCell>
+          </TableRow>
+        </TableHead>
+        <TableBody>
+          {txs.map((tx) => (
+            <TableRow key={tx.txid} hover>
+              <TableCell>
+                <Tooltip title={tx.txid}>
+                  <Box sx={{ display: "flex", alignItems: "center" }}>
+                    <span>{`${tx.txid.substring(0, 8)}...`}</span>
+                    {!tx.status.confirmed && (
+                      <Tooltip title="Pending">
+                        <Schedule
+                          color="action"
+                          fontSize="small"
+                          sx={{ ml: 1 }}
+                        />
+                      </Tooltip>
+                    )}
+                  </Box>
+                </Tooltip>
+              </TableCell>
+              <TableCell>{formatDate(tx.status.block_time)}</TableCell>
+              <TableCell>
+                <Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
+                  <StyledChip
+                    label={`${tx.size} vBytes`}
+                    size="small"
+                    variant="outlined"
+                  />
+                  <StyledChip
+                    label={tx.status.confirmed ? "Confirmed" : "Pending"}
+                    color={tx.status.confirmed ? "success" : "warning"}
+                    size="small"
+                  />
+                </Box>
+              </TableCell>
+              <TableCell align="right">{formatFee(tx.fee)}</TableCell>
+              {tabValue === 1 && (
+                <TableCell align="right">
+                  {tx.status.block_height?.toLocaleString()}
+                </TableCell>
+              )}
+              <TableCell>
+                <Tooltip title="View in Block Explorer">
+                  <IconButton
+                    onClick={() =>
+                      window.open(
+                        blockExplorerTransactionURL(tx.txid, network),
+                        "_blank",
+                      )
+                    }
+                    size="small"
+                  >
+                    <OpenInNew fontSize="small" />
+                  </IconButton>
+                </Tooltip>
+              </TableCell>
+            </TableRow>
+          ))}
+        </TableBody>
+      </Table>
+    </TableContainer>
+  );
+
+  return (
+    <StyledCard>
+      <CardHeader
+        title={
+          <Box
+            display="flex"
+            justifyContent="space-between"
+            alignItems="center"
+          >
+            <Typography variant="h6">Transactions</Typography>
+            <Tooltip title="Refresh transactions">
+              <IconButton onClick={fetchTransactions} disabled={isLoading}>
+                <Refresh />
+              </IconButton>
+            </Tooltip>
+          </Box>
+        }
+      />
+      <CardContent>
+        {error && (
+          <Typography color="error" gutterBottom>
+            Error: {error}
+          </Typography>
+        )}
+
+        <Tabs
+          value={tabValue}
+          onChange={(_, newValue) => setTabValue(newValue)}
+          sx={{ borderBottom: 1, borderColor: "divider" }}
+        >
+          <Tab
+            label={`Pending (${pendingTxs.length})`}
+            sx={{ fontWeight: "medium" }}
+          />
+          <Tab
+            label={`Confirmed (${confirmedTxs.length})`}
+            sx={{ fontWeight: "medium" }}
+          />
+        </Tabs>
+
+        <TabPanel value={tabValue} index={0}>
+          {isLoading ? (
+            <Box display="flex" justifyContent="center" p={3}>
+              <CircularProgress />
+            </Box>
+          ) : pendingTxs.length > 0 ? (
+            renderTransactionTable(pendingTxs)
+          ) : (
+            <Typography align="center" color="textSecondary" p={3}>
+              No pending transactions
+            </Typography>
+          )}
+        </TabPanel>
+
+        <TabPanel value={tabValue} index={1}>
+          {isLoading ? (
+            <Box display="flex" justifyContent="center" p={3}>
+              <CircularProgress />
+            </Box>
+          ) : confirmedTxs.length > 0 ? (
+            renderTransactionTable(confirmedTxs)
+          ) : (
+            <Typography align="center" color="textSecondary" p={3}>
+              No confirmed transactions
+            </Typography>
+          )}
+        </TabPanel>
+      </CardContent>
+    </StyledCard>
+  );
+}
+
+TransactionsTab.propTypes = {
path: apps/coordinator/src/components/Wallet/TransactionsTab.jsx
user: login: bucko13
body: Let's use typescript instead of proptypes . 
created_at: 2025-01-06T22:45:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,361 @@
+import React, { useState, useEffect } from "react";
+import PropTypes from "prop-types";
+import { connect } from "react-redux";
+import { styled } from "@mui/material/styles";
+import {
+  Table,
+  TableBody,
+  TableCell,
+  TableContainer,
+  TableHead,
+  TableRow,
+  Paper,
+  Tabs,
+  Tab,
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Card,
+  CardHeader,
+  CardContent,
+  Chip,
+} from "@mui/material";
+import { OpenInNew, Refresh, Schedule } from "@mui/icons-material";
+import { blockExplorerTransactionURL } from "@caravan/bitcoin";
+import {
+  setTransactions,
+  setTransactionsLoading,
+  setTransactionsError,
+} from "../../actions/transactionActions";
+import { updateBlockchainClient } from "../../actions/clientActions";
+
+const StyledCard = styled(Card)(({ theme }) => ({
+  marginBottom: theme.spacing(3),
+  height: "100%",
+}));
+
+const StyledTableCell = styled(TableCell)(({ theme }) => ({
+  fontWeight: theme.typography.fontWeightMedium,
+}));
+
+const StyledChip = styled(Chip)(({ theme }) => ({
+  marginRight: theme.spacing(1),
+}));
+
+// Helper component for tab panels, helps to switch between Pending and Completed Tx's
+function TabPanel({ children, value, index, ...other }) {
+  return (
+    <div
+      role="tabpanel"
+      hidden={value !== index}
+      id={`transaction-tabpanel-${index}`}
+      aria-labelledby={`transaction-tab-${index}`}
+      {...other}
+    >
+      {value === index && <Box py={2}>{children}</Box>}
+    </div>
+  );
+}
+
+TabPanel.propTypes = {
+  children: PropTypes.node,
+  index: PropTypes.number.isRequired,
+  value: PropTypes.number.isRequired,
+};
+
+function TransactionsTab({
+  network,
+  transactions,
+  isLoading,
+  error,
+  getBlockchainClient,
+  deposits,
+  change,
+  setTransactions,
+  setTransactionsLoading,
+  setTransactionsError,
+}) {
+  const [tabValue, setTabValue] = useState(0);
+  const [mounted, setMounted] = useState(true);
+
+  // Helper function to fetch individual transaction details
+  const fetchTransactionDetails = async (client, txid) => {
+    try {
+      const txDetails = await client.getTransaction(txid);
+      return {
+        ...txDetails,
+        confirmed: txDetails.status.confirmed,
+        timestamp: txDetails.status.block_time,
+      };
+    } catch (err) {
+      console.error(`Error fetching tx ${txid}:`, err);
+      return null;
+    }
+  };
+
+  // Main function to fetch all transaction data
+  const fetchTransactions = async () => {
+    if (!mounted) return;
+    setTransactionsLoading(true);
+
+    try {
+      const blockchainClient = await getBlockchainClient();
+      if (!blockchainClient) {
+        throw new Error("No blockchain client available");
+      }
+
+      // Get unique txids from all UTXOs
+      const txids = new Set();
+      [
+        ...Object.values(deposits.nodes),
+        ...Object.values(change.nodes),
+      ].forEach((node) => {
+        node.utxos.forEach((utxo) => txids.add(utxo.txid));
+      });
+
+      // Fetch details for each transaction
+      const txPromises = Array.from(txids).map((txid) =>
+        fetchTransactionDetails(blockchainClient, txid),
+      );
+
+      const txDetails = (await Promise.all(txPromises))
+        .filter((tx) => tx !== null)
+        .sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); // Sort by timestamp
+
+      if (mounted) {
+        setTransactions(txDetails);
+        setTransactionsError(null);
+      }
+    } catch (err) {
+      if (mounted) {
+        setTransactionsError(err.message);
+      }
+    } finally {
+      if (mounted) {
+        setTransactionsLoading(false);
+      }
+    }
+  };
+
+  // Initial data fetch and cleanup
+  useEffect(() => {
+    setMounted(true);
+    fetchTransactions();
+    return () => setMounted(false);
+  }, [deposits.nodes, change.nodes]);
+
+  // Helper function to format dates
+  const formatDate = (timestamp) => {
+    if (!timestamp) return "Pending";
+    return new Date(timestamp * 1000).toLocaleString();
+  };
+
+  // Helper function to format fees
+  const formatFee = (fee) => {
+    if (!fee) return "N/A";
+    return `${fee.toLocaleString()} sats`;
+  };
+
+  // Split transactions into pending and confirmed
+  const pendingTxs =
+    transactions && Array.isArray(transactions)
+      ? transactions.filter((tx) => !tx.status?.confirmed)
+      : [];
+  const confirmedTxs =
+    transactions && Array.isArray(transactions)
+      ? transactions.filter((tx) => tx.status?.confirmed)
+      : [];
+
+  // Render the transaction table
+  const renderTransactionTable = (txs) => (
+    <TableContainer component={Paper}>
+      <Table>
+        <TableHead>
+          <TableRow>
+            <StyledTableCell>Transaction ID</StyledTableCell>
+            <StyledTableCell>Time</StyledTableCell>
+            <StyledTableCell>Details</StyledTableCell>
+            <StyledTableCell align="right">Fee</StyledTableCell>
+            {tabValue === 1 && (
+              <StyledTableCell align="right">Block</StyledTableCell>
+            )}
+            <StyledTableCell>Actions</StyledTableCell>
+          </TableRow>
+        </TableHead>
+        <TableBody>
+          {txs.map((tx) => (
+            <TableRow key={tx.txid} hover>
+              <TableCell>
+                <Tooltip title={tx.txid}>
+                  <Box sx={{ display: "flex", alignItems: "center" }}>
+                    <span>{`${tx.txid.substring(0, 8)}...`}</span>
+                    {!tx.status.confirmed && (
+                      <Tooltip title="Pending">
+                        <Schedule
+                          color="action"
+                          fontSize="small"
+                          sx={{ ml: 1 }}
+                        />
+                      </Tooltip>
+                    )}
+                  </Box>
+                </Tooltip>
+              </TableCell>
+              <TableCell>{formatDate(tx.status.block_time)}</TableCell>
+              <TableCell>
+                <Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
+                  <StyledChip
+                    label={`${tx.size} vBytes`}
+                    size="small"
+                    variant="outlined"
+                  />
+                  <StyledChip
+                    label={tx.status.confirmed ? "Confirmed" : "Pending"}
+                    color={tx.status.confirmed ? "success" : "warning"}
+                    size="small"
+                  />
+                </Box>
+              </TableCell>
+              <TableCell align="right">{formatFee(tx.fee)}</TableCell>
+              {tabValue === 1 && (
+                <TableCell align="right">
+                  {tx.status.block_height?.toLocaleString()}
+                </TableCell>
+              )}
+              <TableCell>
+                <Tooltip title="View in Block Explorer">
+                  <IconButton
+                    onClick={() =>
+                      window.open(
+                        blockExplorerTransactionURL(tx.txid, network),
+                        "_blank",
+                      )
+                    }
+                    size="small"
+                  >
+                    <OpenInNew fontSize="small" />
+                  </IconButton>
+                </Tooltip>
+              </TableCell>
+            </TableRow>
+          ))}
+        </TableBody>
+      </Table>
+    </TableContainer>
+  );
+
+  return (
+    <StyledCard>
+      <CardHeader
+        title={
+          <Box
+            display="flex"
+            justifyContent="space-between"
+            alignItems="center"
+          >
+            <Typography variant="h6">Transactions</Typography>
+            <Tooltip title="Refresh transactions">
+              <IconButton onClick={fetchTransactions} disabled={isLoading}>
+                <Refresh />
+              </IconButton>
+            </Tooltip>
+          </Box>
+        }
+      />
+      <CardContent>
+        {error && (
+          <Typography color="error" gutterBottom>
+            Error: {error}
+          </Typography>
+        )}
+
+        <Tabs
+          value={tabValue}
+          onChange={(_, newValue) => setTabValue(newValue)}
+          sx={{ borderBottom: 1, borderColor: "divider" }}
+        >
+          <Tab
+            label={`Pending (${pendingTxs.length})`}
+            sx={{ fontWeight: "medium" }}
+          />
+          <Tab
+            label={`Confirmed (${confirmedTxs.length})`}
+            sx={{ fontWeight: "medium" }}
+          />
+        </Tabs>
+
+        <TabPanel value={tabValue} index={0}>
+          {isLoading ? (
+            <Box display="flex" justifyContent="center" p={3}>
+              <CircularProgress />
+            </Box>
+          ) : pendingTxs.length > 0 ? (
+            renderTransactionTable(pendingTxs)
+          ) : (
+            <Typography align="center" color="textSecondary" p={3}>
+              No pending transactions
+            </Typography>
+          )}
+        </TabPanel>
+
+        <TabPanel value={tabValue} index={1}>
+          {isLoading ? (
+            <Box display="flex" justifyContent="center" p={3}>
+              <CircularProgress />
+            </Box>
+          ) : confirmedTxs.length > 0 ? (
+            renderTransactionTable(confirmedTxs)
+          ) : (
+            <Typography align="center" color="textSecondary" p={3}>
+              No confirmed transactions
+            </Typography>
+          )}
+        </TabPanel>
+      </CardContent>
+    </StyledCard>
+  );
+}
+
+TransactionsTab.propTypes = {
+  network: PropTypes.string.isRequired,
+  transactions: PropTypes.array,
+  isLoading: PropTypes.bool.isRequired,
+  error: PropTypes.string,
+  getBlockchainClient: PropTypes.func.isRequired,
+  deposits: PropTypes.shape({
+    nodes: PropTypes.object.isRequired,
+  }).isRequired,
+  change: PropTypes.shape({
+    nodes: PropTypes.object.isRequired,
+  }).isRequired,
+  setTransactions: PropTypes.func.isRequired,
+  setTransactionsLoading: PropTypes.func.isRequired,
+  setTransactionsError: PropTypes.func.isRequired,
+};
+
+TransactionsTab.defaultProps = {
+  transactions: [],
+  error: null,
+};
+
+const mapStateToProps = (state) => {
+  return {
+    network: state.settings.network,
+    deposits: state.wallet.deposits,
+    change: state.wallet.change,
+    transactions: state.spend.transaction.transactions.transactions || [], // Access nested transactions array
+    isLoading: state.spend.transaction.transactions.isLoading,
+    error: state.spend.transaction.transactions.error,
+  };
+};
+
+const mapDispatchToProps = {
+  getBlockchainClient: updateBlockchainClient,
+  setTransactions,
+  setTransactionsLoading,
+  setTransactionsError,
+};
path: apps/coordinator/src/components/Wallet/TransactionsTab.jsx
user: login: bucko13
body: even if you do use redux you should be able to use the newer hook pattern to dispatch actions and get props from the store: `useDispatch` and `useSelector`. Take a look at the `WalletDescriptorImporter` component for an example of how "modern" components should be written with newer patterns (I know it's hard in caravan because of all the legacy code still. sorry about that) 
created_at: 2025-01-06T22:50:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,361 @@
+import React, { useState, useEffect } from "react";
+import PropTypes from "prop-types";
+import { connect } from "react-redux";
+import { styled } from "@mui/material/styles";
+import {
+  Table,
+  TableBody,
+  TableCell,
+  TableContainer,
+  TableHead,
+  TableRow,
+  Paper,
+  Tabs,
+  Tab,
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Card,
+  CardHeader,
+  CardContent,
+  Chip,
+} from "@mui/material";
+import { OpenInNew, Refresh, Schedule } from "@mui/icons-material";
+import { blockExplorerTransactionURL } from "@caravan/bitcoin";
+import {
+  setTransactions,
+  setTransactionsLoading,
+  setTransactionsError,
+} from "../../actions/transactionActions";
+import { updateBlockchainClient } from "../../actions/clientActions";
+
+const StyledCard = styled(Card)(({ theme }) => ({
+  marginBottom: theme.spacing(3),
+  height: "100%",
+}));
+
+const StyledTableCell = styled(TableCell)(({ theme }) => ({
+  fontWeight: theme.typography.fontWeightMedium,
+}));
+
+const StyledChip = styled(Chip)(({ theme }) => ({
+  marginRight: theme.spacing(1),
+}));
+
+// Helper component for tab panels, helps to switch between Pending and Completed Tx's
+function TabPanel({ children, value, index, ...other }) {
+  return (
+    <div
+      role="tabpanel"
+      hidden={value !== index}
+      id={`transaction-tabpanel-${index}`}
+      aria-labelledby={`transaction-tab-${index}`}
+      {...other}
+    >
+      {value === index && <Box py={2}>{children}</Box>}
+    </div>
+  );
+}
+
+TabPanel.propTypes = {
+  children: PropTypes.node,
+  index: PropTypes.number.isRequired,
+  value: PropTypes.number.isRequired,
+};
+
+function TransactionsTab({
+  network,
+  transactions,
+  isLoading,
+  error,
+  getBlockchainClient,
+  deposits,
+  change,
+  setTransactions,
+  setTransactionsLoading,
+  setTransactionsError,
+}) {
+  const [tabValue, setTabValue] = useState(0);
+  const [mounted, setMounted] = useState(true);
+
+  // Helper function to fetch individual transaction details
+  const fetchTransactionDetails = async (client, txid) => {
+    try {
+      const txDetails = await client.getTransaction(txid);
+      return {
+        ...txDetails,
+        confirmed: txDetails.status.confirmed,
+        timestamp: txDetails.status.block_time,
+      };
+    } catch (err) {
+      console.error(`Error fetching tx ${txid}:`, err);
+      return null;
+    }
+  };
+
+  // Main function to fetch all transaction data
+  const fetchTransactions = async () => {
+    if (!mounted) return;
+    setTransactionsLoading(true);
+
+    try {
+      const blockchainClient = await getBlockchainClient();
+      if (!blockchainClient) {
+        throw new Error("No blockchain client available");
+      }
+
+      // Get unique txids from all UTXOs
+      const txids = new Set();
+      [
+        ...Object.values(deposits.nodes),
+        ...Object.values(change.nodes),
+      ].forEach((node) => {
+        node.utxos.forEach((utxo) => txids.add(utxo.txid));
+      });
+
+      // Fetch details for each transaction
+      const txPromises = Array.from(txids).map((txid) =>
+        fetchTransactionDetails(blockchainClient, txid),
+      );
+
+      const txDetails = (await Promise.all(txPromises))
+        .filter((tx) => tx !== null)
+        .sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); // Sort by timestamp
+
+      if (mounted) {
+        setTransactions(txDetails);
+        setTransactionsError(null);
+      }
+    } catch (err) {
+      if (mounted) {
+        setTransactionsError(err.message);
+      }
+    } finally {
+      if (mounted) {
+        setTransactionsLoading(false);
+      }
+    }
+  };
+
+  // Initial data fetch and cleanup
+  useEffect(() => {
+    setMounted(true);
+    fetchTransactions();
+    return () => setMounted(false);
+  }, [deposits.nodes, change.nodes]);
+
+  // Helper function to format dates
+  const formatDate = (timestamp) => {
+    if (!timestamp) return "Pending";
+    return new Date(timestamp * 1000).toLocaleString();
+  };
+
+  // Helper function to format fees
+  const formatFee = (fee) => {
+    if (!fee) return "N/A";
+    return `${fee.toLocaleString()} sats`;
+  };
+
+  // Split transactions into pending and confirmed
+  const pendingTxs =
+    transactions && Array.isArray(transactions)
+      ? transactions.filter((tx) => !tx.status?.confirmed)
+      : [];
+  const confirmedTxs =
+    transactions && Array.isArray(transactions)
+      ? transactions.filter((tx) => tx.status?.confirmed)
+      : [];
+
+  // Render the transaction table
+  const renderTransactionTable = (txs) => (
path: apps/coordinator/src/components/Wallet/TransactionsTab.jsx
user: login: bucko13
body: This is a pattern from our legacy code that we'd really like to move away from. It's kind of an anti-pattern for react. This should just be a component `<TransactionTable txs={txs} />`. This way you get react rendering optimizations, type safety, and a cleaner parent component because you can pull this out as a standalone component. 
created_at: 2025-01-06T23:00:09Z
author_association: CONTRIBUTOR
path: apps/coordinator/src/actions/transactionActions.js
user: login: bucko13
body: is there a reason this needs to be in redux? Are there any other components that need to use this outside of the component tree you've added? If you need to pass this data down through multiple children, you can also use [context](https://react.dev/reference/react/useContext) instead of redux. You'll still have access to what you need from the store use useSelector or useDispatch as needed. If we can avoid adding to our redux code that's preferable (when possible). 
created_at: 2025-01-06T23:02:43Z
author_association: CONTRIBUTOR
path: apps/coordinator/src/actions/transactionActions.js
user: login: Legend101Zz
body: Ahh ... yes I realise it now ... maybe I over-thought of having the use of redux for passing the states onto the CPFP/RBF components ... but now that you have mentioned that you'd refer not to use redux and rely on modern react practices... I revert back to the version I wrote without the use of redux :)
created_at: 2025-01-07T19:41:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+import React, { useState, useEffect, useCallback } from "react";
+import { connect } from "react-redux";
+import {
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Tabs,
+  Tab,
+  Chip,
+} from "@mui/material";
+import { Refresh } from "@mui/icons-material";
+import { BlockchainClient } from "@caravan/clients";
+import { blockExplorerTransactionURL, Network } from "@caravan/bitcoin";
+import { updateBlockchainClient } from "../../actions/clientActions";
+import { TransactionTable } from "./TransactionsTable";
+
+// How are Transaction should look like
+interface Transaction {
+  txid: string;
+  version: number;
+  locktime: number;
+  vin: any[]; // Input vector
+  vout: any[]; // Output vector
+  weight: number;
+  status: {
+    confirmed: boolean;
+    blockTime?: number;
+    blockHeight?: number;
+  };
+  size: number;
+  fee: number;
+}
+
+interface TransactionsTabProps {
+  network: Network;
+  getBlockchainClient: () => Promise<BlockchainClient>;
+  deposits: {
+    nodes: Record<string, any>;
+  };
+  change: {
+    nodes: Record<string, any>;
+  };
+  client: {
+    type: string;
+  };
+}
+
+const TransactionsTab: React.FC<TransactionsTabProps> = ({
+  network,
+  getBlockchainClient,
+  deposits,
+  change,
+  client,
+}) => {
+  const [tabValue, setTabValue] = useState(0);
+  const [transactions, setTransactions] = useState<Transaction[]>([]);
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const [sortBy, setSortBy] = useState<string>("blockTime");
+  const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
+
+  // Fetch transactions
+  const fetchTransactions = useCallback(async () => {
+    setIsLoading(true);
+    setError(null);
+
+    try {
+      const blockchainClient = await getBlockchainClient();
+      if (!blockchainClient) {
+        throw new Error("No blockchain client available");
+      }
+
+      // Get unique transaction IDs from UTXOs
+      const txids = new Set<string>();
+      [
+        ...Object.values(deposits.nodes),
+        ...Object.values(change.nodes),
+      ].forEach((node) => {
+        node.utxos.forEach((utxo: { txid: string }) => {
+          txids.add(utxo.txid);
+        });
+      });
+
+      // Fetch transaction details in parallel
+      const txDetails = await Promise.all(
+        Array.from(txids).map(async (txid) => {
+          try {
+            const tx = await blockchainClient.getTransaction(txid);
+            return tx;
+          } catch (err) {
+            console.error(`Error fetching tx ${txid}:`, err);
+            return null;
+          }
+        }),
+      );
+      setTransactions(txDetails.filter((tx): tx is Transaction => tx !== null));
+      setError(null);
+    } catch (err) {
+      setError((err as Error).message);
+    } finally {
+      setIsLoading(false);
+    }
+  }, [getBlockchainClient, deposits.nodes, change.nodes]);
+
+  // Initial loading
+  useEffect(() => {
+    fetchTransactions();
+  }, [fetchTransactions]);
+
+  // Handle sorting
+  const handleSort = (property: string) => {
+    const isAsc = sortBy === property && sortDirection === "asc";
+    setSortDirection(isAsc ? "desc" : "asc");
+    setSortBy(property);
+  };
+
+  const handleTransactionClick = useCallback(
+    (txid: string) => {
+      // Check if user is using a private node
+      const isPrivateNode = client.type === "private";
+
+      if (isPrivateNode) {
+        // If using private node, warn about privacy implications
+        const confirmed = window.confirm(
+          "Opening in a block explorer may expose your wallet activity to third parties. Continue?",
+        );
+        if (!confirmed) return;
+      }
+
+      window.open(blockExplorerTransactionURL(txid, network), "_blank");
+    },
path: apps/coordinator/src/components/Wallet/TransactionsTab.tsx
user: login: coderabbitai[bot]
body: _:hammer_and_wrench: Refactor suggestion_

**Enhance privacy warning for block explorer navigation.**

The current privacy warning could be more informative about the specific risks.

```diff
 if (isPrivateNode) {
   const confirmed = window.confirm(
-    "Opening in a block explorer may expose your wallet activity to third parties. Continue?",
+    "Warning: Opening in a block explorer will:\n\n" +
+    "1. Connect to external servers\n" +
+    "2. Potentially expose your IP address\n" +
+    "3. Link your wallet activity to your browser fingerprint\n\n" +
+    "Do you want to continue?"
   );
   if (!confirmed) return;
 }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
      if (isPrivateNode) {
        // If using private node, warn about privacy implications
        const confirmed = window.confirm(
          "Warning: Opening in a block explorer will:\n\n" +
          "1. Connect to external servers\n" +
          "2. Potentially expose your IP address\n" +
          "3. Link your wallet activity to your browser fingerprint\n\n" +
          "Do you want to continue?"
        );
        if (!confirmed) return;
      }

      window.open(blockExplorerTransactionURL(txid, network), "_blank");
    },
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-02-21T14:23:41Z
author_association: NONE
diff_hunk: @@ -0,0 +1,246 @@
+import React, { useState, useEffect, useCallback } from "react";
+import { connect } from "react-redux";
+import {
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Tabs,
+  Tab,
+  Chip,
+} from "@mui/material";
+import { Refresh } from "@mui/icons-material";
+import { BlockchainClient } from "@caravan/clients";
+import { blockExplorerTransactionURL, Network } from "@caravan/bitcoin";
+import { updateBlockchainClient } from "../../actions/clientActions";
+import { TransactionTable } from "./TransactionsTable";
+
+// How are Transaction should look like
+interface Transaction {
+  txid: string;
+  version: number;
+  locktime: number;
+  vin: any[]; // Input vector
+  vout: any[]; // Output vector
+  weight: number;
+  status: {
+    confirmed: boolean;
+    blockTime?: number;
+    blockHeight?: number;
+  };
+  size: number;
+  fee: number;
+}
+
+interface TransactionsTabProps {
+  network: Network;
+  getBlockchainClient: () => Promise<BlockchainClient>;
+  deposits: {
+    nodes: Record<string, any>;
+  };
+  change: {
+    nodes: Record<string, any>;
+  };
+  client: {
+    type: string;
+  };
+}
+
+const TransactionsTab: React.FC<TransactionsTabProps> = ({
+  network,
+  getBlockchainClient,
+  deposits,
+  change,
+  client,
+}) => {
+  const [tabValue, setTabValue] = useState(0);
+  const [transactions, setTransactions] = useState<Transaction[]>([]);
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const [sortBy, setSortBy] = useState<string>("blockTime");
+  const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
+
+  // Fetch transactions
+  const fetchTransactions = useCallback(async () => {
+    setIsLoading(true);
+    setError(null);
+
+    try {
+      const blockchainClient = await getBlockchainClient();
+      if (!blockchainClient) {
+        throw new Error("No blockchain client available");
+      }
+
+      // Get unique transaction IDs from UTXOs
+      const txids = new Set<string>();
+      [
+        ...Object.values(deposits.nodes),
+        ...Object.values(change.nodes),
+      ].forEach((node) => {
+        node.utxos.forEach((utxo: { txid: string }) => {
+          txids.add(utxo.txid);
+        });
+      });
+
+      // Fetch transaction details in parallel
+      const txDetails = await Promise.all(
+        Array.from(txids).map(async (txid) => {
+          try {
+            const tx = await blockchainClient.getTransaction(txid);
+            return tx;
+          } catch (err) {
+            console.error(`Error fetching tx ${txid}:`, err);
+            return null;
+          }
+        }),
path: apps/coordinator/src/components/Wallet/TransactionsTab.tsx
user: login: coderabbitai[bot]
body: _:hammer_and_wrench: Refactor suggestion_

**Enhance error handling for transaction fetching.**

The current error handling silently logs errors and returns null. Consider collecting errors to display a summary to users.

```diff
+const errors: Array<{ txid: string; error: string }> = [];
 const txDetails = await Promise.all(
   Array.from(txids).map(async (txid) => {
     try {
       const tx = await blockchainClient.getTransaction(txid);
       return tx;
     } catch (err) {
       console.error(`Error fetching tx ${txid}:`, err);
+      errors.push({ txid, error: (err as Error).message });
       return null;
     }
   }),
 );
+if (errors.length > 0) {
+  setError(`Failed to fetch ${errors.length} transaction(s). Check console for details.`);
+}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
const errors: Array<{ txid: string; error: string }> = [];
const txDetails = await Promise.all(
  Array.from(txids).map(async (txid) => {
    try {
      const tx = await blockchainClient.getTransaction(txid);
      return tx;
    } catch (err) {
      console.error(`Error fetching tx ${txid}:`, err);
      errors.push({ txid, error: (err as Error).message });
      return null;
    }
  }),
);
if (errors.length > 0) {
  setError(`Failed to fetch ${errors.length} transaction(s). Check console for details.`);
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-02-21T14:23:42Z
author_association: NONE
diff_hunk: @@ -0,0 +1,361 @@
+import React, { useState, useEffect } from "react";
+import PropTypes from "prop-types";
+import { connect } from "react-redux";
+import { styled } from "@mui/material/styles";
+import {
+  Table,
+  TableBody,
+  TableCell,
+  TableContainer,
+  TableHead,
+  TableRow,
+  Paper,
+  Tabs,
+  Tab,
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Card,
+  CardHeader,
+  CardContent,
+  Chip,
+} from "@mui/material";
+import { OpenInNew, Refresh, Schedule } from "@mui/icons-material";
+import { blockExplorerTransactionURL } from "@caravan/bitcoin";
+import {
+  setTransactions,
+  setTransactionsLoading,
+  setTransactionsError,
+} from "../../actions/transactionActions";
+import { updateBlockchainClient } from "../../actions/clientActions";
+
+const StyledCard = styled(Card)(({ theme }) => ({
+  marginBottom: theme.spacing(3),
+  height: "100%",
+}));
+
+const StyledTableCell = styled(TableCell)(({ theme }) => ({
+  fontWeight: theme.typography.fontWeightMedium,
+}));
+
+const StyledChip = styled(Chip)(({ theme }) => ({
+  marginRight: theme.spacing(1),
+}));
+
+// Helper component for tab panels, helps to switch between Pending and Completed Tx's
+function TabPanel({ children, value, index, ...other }) {
+  return (
+    <div
+      role="tabpanel"
+      hidden={value !== index}
+      id={`transaction-tabpanel-${index}`}
+      aria-labelledby={`transaction-tab-${index}`}
+      {...other}
+    >
+      {value === index && <Box py={2}>{children}</Box>}
+    </div>
+  );
+}
+
+TabPanel.propTypes = {
+  children: PropTypes.node,
+  index: PropTypes.number.isRequired,
+  value: PropTypes.number.isRequired,
+};
+
+function TransactionsTab({
+  network,
+  transactions,
+  isLoading,
+  error,
+  getBlockchainClient,
+  deposits,
+  change,
+  setTransactions,
+  setTransactionsLoading,
+  setTransactionsError,
+}) {
+  const [tabValue, setTabValue] = useState(0);
+  const [mounted, setMounted] = useState(true);
+
+  // Helper function to fetch individual transaction details
+  const fetchTransactionDetails = async (client, txid) => {
+    try {
+      const txDetails = await client.getTransaction(txid);
path: apps/coordinator/src/components/Wallet/TransactionsTab.jsx
user: login: Legend101Zz
body: Thanks for the suggestion ... I have now rebased the current branch to use #134 as base branch
created_at: 2025-02-21T14:27:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,361 @@
+import React, { useState, useEffect } from "react";
+import PropTypes from "prop-types";
+import { connect } from "react-redux";
+import { styled } from "@mui/material/styles";
+import {
+  Table,
+  TableBody,
+  TableCell,
+  TableContainer,
+  TableHead,
+  TableRow,
+  Paper,
+  Tabs,
+  Tab,
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Card,
+  CardHeader,
+  CardContent,
+  Chip,
+} from "@mui/material";
+import { OpenInNew, Refresh, Schedule } from "@mui/icons-material";
+import { blockExplorerTransactionURL } from "@caravan/bitcoin";
+import {
+  setTransactions,
+  setTransactionsLoading,
+  setTransactionsError,
+} from "../../actions/transactionActions";
+import { updateBlockchainClient } from "../../actions/clientActions";
+
+const StyledCard = styled(Card)(({ theme }) => ({
+  marginBottom: theme.spacing(3),
+  height: "100%",
+}));
+
+const StyledTableCell = styled(TableCell)(({ theme }) => ({
+  fontWeight: theme.typography.fontWeightMedium,
+}));
+
+const StyledChip = styled(Chip)(({ theme }) => ({
+  marginRight: theme.spacing(1),
+}));
+
+// Helper component for tab panels, helps to switch between Pending and Completed Tx's
+function TabPanel({ children, value, index, ...other }) {
+  return (
+    <div
+      role="tabpanel"
+      hidden={value !== index}
+      id={`transaction-tabpanel-${index}`}
+      aria-labelledby={`transaction-tab-${index}`}
+      {...other}
+    >
+      {value === index && <Box py={2}>{children}</Box>}
+    </div>
+  );
+}
+
+TabPanel.propTypes = {
+  children: PropTypes.node,
+  index: PropTypes.number.isRequired,
+  value: PropTypes.number.isRequired,
+};
+
+function TransactionsTab({
+  network,
+  transactions,
+  isLoading,
+  error,
+  getBlockchainClient,
+  deposits,
+  change,
+  setTransactions,
+  setTransactionsLoading,
+  setTransactionsError,
+}) {
+  const [tabValue, setTabValue] = useState(0);
+  const [mounted, setMounted] = useState(true);
+
+  // Helper function to fetch individual transaction details
+  const fetchTransactionDetails = async (client, txid) => {
+    try {
+      const txDetails = await client.getTransaction(txid);
+      return {
+        ...txDetails,
+        confirmed: txDetails.status.confirmed,
+        timestamp: txDetails.status.block_time,
+      };
+    } catch (err) {
+      console.error(`Error fetching tx ${txid}:`, err);
+      return null;
+    }
+  };
+
+  // Main function to fetch all transaction data
+  const fetchTransactions = async () => {
+    if (!mounted) return;
+    setTransactionsLoading(true);
+
+    try {
+      const blockchainClient = await getBlockchainClient();
+      if (!blockchainClient) {
+        throw new Error("No blockchain client available");
+      }
+
+      // Get unique txids from all UTXOs
+      const txids = new Set();
+      [
+        ...Object.values(deposits.nodes),
+        ...Object.values(change.nodes),
+      ].forEach((node) => {
+        node.utxos.forEach((utxo) => txids.add(utxo.txid));
+      });
+
+      // Fetch details for each transaction
+      const txPromises = Array.from(txids).map((txid) =>
+        fetchTransactionDetails(blockchainClient, txid),
+      );
+
+      const txDetails = (await Promise.all(txPromises))
+        .filter((tx) => tx !== null)
+        .sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); // Sort by timestamp
+
+      if (mounted) {
+        setTransactions(txDetails);
+        setTransactionsError(null);
+      }
+    } catch (err) {
+      if (mounted) {
+        setTransactionsError(err.message);
+      }
+    } finally {
+      if (mounted) {
+        setTransactionsLoading(false);
+      }
+    }
+  };
+
+  // Initial data fetch and cleanup
+  useEffect(() => {
+    setMounted(true);
+    fetchTransactions();
+    return () => setMounted(false);
+  }, [deposits.nodes, change.nodes]);
+
+  // Helper function to format dates
+  const formatDate = (timestamp) => {
+    if (!timestamp) return "Pending";
+    return new Date(timestamp * 1000).toLocaleString();
+  };
+
+  // Helper function to format fees
+  const formatFee = (fee) => {
+    if (!fee) return "N/A";
+    return `${fee.toLocaleString()} sats`;
+  };
+
+  // Split transactions into pending and confirmed
+  const pendingTxs =
+    transactions && Array.isArray(transactions)
+      ? transactions.filter((tx) => !tx.status?.confirmed)
+      : [];
+  const confirmedTxs =
+    transactions && Array.isArray(transactions)
+      ? transactions.filter((tx) => tx.status?.confirmed)
+      : [];
+
+  // Render the transaction table
+  const renderTransactionTable = (txs) => (
+    <TableContainer component={Paper}>
+      <Table>
+        <TableHead>
+          <TableRow>
+            <StyledTableCell>Transaction ID</StyledTableCell>
+            <StyledTableCell>Time</StyledTableCell>
+            <StyledTableCell>Details</StyledTableCell>
+            <StyledTableCell align="right">Fee</StyledTableCell>
+            {tabValue === 1 && (
+              <StyledTableCell align="right">Block</StyledTableCell>
+            )}
+            <StyledTableCell>Actions</StyledTableCell>
+          </TableRow>
+        </TableHead>
+        <TableBody>
+          {txs.map((tx) => (
+            <TableRow key={tx.txid} hover>
+              <TableCell>
+                <Tooltip title={tx.txid}>
+                  <Box sx={{ display: "flex", alignItems: "center" }}>
+                    <span>{`${tx.txid.substring(0, 8)}...`}</span>
+                    {!tx.status.confirmed && (
+                      <Tooltip title="Pending">
+                        <Schedule
+                          color="action"
+                          fontSize="small"
+                          sx={{ ml: 1 }}
+                        />
+                      </Tooltip>
+                    )}
+                  </Box>
+                </Tooltip>
+              </TableCell>
+              <TableCell>{formatDate(tx.status.block_time)}</TableCell>
+              <TableCell>
+                <Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
+                  <StyledChip
+                    label={`${tx.size} vBytes`}
+                    size="small"
+                    variant="outlined"
+                  />
+                  <StyledChip
+                    label={tx.status.confirmed ? "Confirmed" : "Pending"}
+                    color={tx.status.confirmed ? "success" : "warning"}
+                    size="small"
+                  />
+                </Box>
+              </TableCell>
+              <TableCell align="right">{formatFee(tx.fee)}</TableCell>
+              {tabValue === 1 && (
+                <TableCell align="right">
+                  {tx.status.block_height?.toLocaleString()}
+                </TableCell>
+              )}
+              <TableCell>
+                <Tooltip title="View in Block Explorer">
+                  <IconButton
+                    onClick={() =>
+                      window.open(
+                        blockExplorerTransactionURL(tx.txid, network),
+                        "_blank",
+                      )
+                    }
+                    size="small"
+                  >
+                    <OpenInNew fontSize="small" />
+                  </IconButton>
+                </Tooltip>
+              </TableCell>
+            </TableRow>
+          ))}
+        </TableBody>
+      </Table>
+    </TableContainer>
+  );
+
+  return (
+    <StyledCard>
+      <CardHeader
+        title={
+          <Box
+            display="flex"
+            justifyContent="space-between"
+            alignItems="center"
+          >
+            <Typography variant="h6">Transactions</Typography>
+            <Tooltip title="Refresh transactions">
+              <IconButton onClick={fetchTransactions} disabled={isLoading}>
+                <Refresh />
+              </IconButton>
+            </Tooltip>
+          </Box>
+        }
+      />
+      <CardContent>
+        {error && (
+          <Typography color="error" gutterBottom>
+            Error: {error}
+          </Typography>
+        )}
+
+        <Tabs
+          value={tabValue}
+          onChange={(_, newValue) => setTabValue(newValue)}
+          sx={{ borderBottom: 1, borderColor: "divider" }}
+        >
+          <Tab
+            label={`Pending (${pendingTxs.length})`}
+            sx={{ fontWeight: "medium" }}
+          />
+          <Tab
+            label={`Confirmed (${confirmedTxs.length})`}
+            sx={{ fontWeight: "medium" }}
+          />
+        </Tabs>
+
+        <TabPanel value={tabValue} index={0}>
+          {isLoading ? (
+            <Box display="flex" justifyContent="center" p={3}>
+              <CircularProgress />
+            </Box>
+          ) : pendingTxs.length > 0 ? (
+            renderTransactionTable(pendingTxs)
+          ) : (
+            <Typography align="center" color="textSecondary" p={3}>
+              No pending transactions
+            </Typography>
+          )}
+        </TabPanel>
+
+        <TabPanel value={tabValue} index={1}>
+          {isLoading ? (
+            <Box display="flex" justifyContent="center" p={3}>
+              <CircularProgress />
+            </Box>
+          ) : confirmedTxs.length > 0 ? (
+            renderTransactionTable(confirmedTxs)
+          ) : (
+            <Typography align="center" color="textSecondary" p={3}>
+              No confirmed transactions
+            </Typography>
+          )}
+        </TabPanel>
+      </CardContent>
+    </StyledCard>
+  );
+}
+
+TransactionsTab.propTypes = {
path: apps/coordinator/src/components/Wallet/TransactionsTab.jsx
user: login: Legend101Zz
body: refactoring [done](https://github.com/caravan-bitcoin/caravan/pull/168/commits/0a5c90f6bdaea3ee0b73198e3bb08139179a81c3)
created_at: 2025-02-21T16:39:25Z
author_association: CONTRIBUTOR
path: apps/coordinator/src/components/Wallet/TransactionsTab.jsx
user: login: Legend101Zz
body: refactoring [done](https://github.com/caravan-bitcoin/caravan/pull/168/commits/0a5c90f6bdaea3ee0b73198e3bb08139179a81c3)
created_at: 2025-02-21T16:39:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,361 @@
+import React, { useState, useEffect } from "react";
+import PropTypes from "prop-types";
+import { connect } from "react-redux";
+import { styled } from "@mui/material/styles";
+import {
+  Table,
+  TableBody,
+  TableCell,
+  TableContainer,
+  TableHead,
+  TableRow,
+  Paper,
+  Tabs,
+  Tab,
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Card,
+  CardHeader,
+  CardContent,
+  Chip,
+} from "@mui/material";
+import { OpenInNew, Refresh, Schedule } from "@mui/icons-material";
+import { blockExplorerTransactionURL } from "@caravan/bitcoin";
+import {
+  setTransactions,
+  setTransactionsLoading,
+  setTransactionsError,
+} from "../../actions/transactionActions";
+import { updateBlockchainClient } from "../../actions/clientActions";
+
+const StyledCard = styled(Card)(({ theme }) => ({
+  marginBottom: theme.spacing(3),
+  height: "100%",
+}));
+
+const StyledTableCell = styled(TableCell)(({ theme }) => ({
+  fontWeight: theme.typography.fontWeightMedium,
+}));
+
+const StyledChip = styled(Chip)(({ theme }) => ({
+  marginRight: theme.spacing(1),
+}));
+
+// Helper component for tab panels, helps to switch between Pending and Completed Tx's
+function TabPanel({ children, value, index, ...other }) {
+  return (
+    <div
+      role="tabpanel"
+      hidden={value !== index}
+      id={`transaction-tabpanel-${index}`}
+      aria-labelledby={`transaction-tab-${index}`}
+      {...other}
+    >
+      {value === index && <Box py={2}>{children}</Box>}
+    </div>
+  );
+}
+
+TabPanel.propTypes = {
+  children: PropTypes.node,
+  index: PropTypes.number.isRequired,
+  value: PropTypes.number.isRequired,
+};
+
+function TransactionsTab({
+  network,
+  transactions,
+  isLoading,
+  error,
+  getBlockchainClient,
+  deposits,
+  change,
+  setTransactions,
+  setTransactionsLoading,
+  setTransactionsError,
+}) {
+  const [tabValue, setTabValue] = useState(0);
+  const [mounted, setMounted] = useState(true);
+
+  // Helper function to fetch individual transaction details
+  const fetchTransactionDetails = async (client, txid) => {
+    try {
+      const txDetails = await client.getTransaction(txid);
+      return {
+        ...txDetails,
+        confirmed: txDetails.status.confirmed,
+        timestamp: txDetails.status.block_time,
+      };
+    } catch (err) {
+      console.error(`Error fetching tx ${txid}:`, err);
+      return null;
+    }
+  };
+
+  // Main function to fetch all transaction data
+  const fetchTransactions = async () => {
+    if (!mounted) return;
+    setTransactionsLoading(true);
+
+    try {
+      const blockchainClient = await getBlockchainClient();
+      if (!blockchainClient) {
+        throw new Error("No blockchain client available");
+      }
+
+      // Get unique txids from all UTXOs
+      const txids = new Set();
+      [
+        ...Object.values(deposits.nodes),
+        ...Object.values(change.nodes),
+      ].forEach((node) => {
+        node.utxos.forEach((utxo) => txids.add(utxo.txid));
+      });
+
+      // Fetch details for each transaction
+      const txPromises = Array.from(txids).map((txid) =>
+        fetchTransactionDetails(blockchainClient, txid),
+      );
+
+      const txDetails = (await Promise.all(txPromises))
+        .filter((tx) => tx !== null)
+        .sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); // Sort by timestamp
+
+      if (mounted) {
+        setTransactions(txDetails);
+        setTransactionsError(null);
+      }
+    } catch (err) {
+      if (mounted) {
+        setTransactionsError(err.message);
+      }
+    } finally {
+      if (mounted) {
+        setTransactionsLoading(false);
+      }
+    }
+  };
+
+  // Initial data fetch and cleanup
+  useEffect(() => {
+    setMounted(true);
+    fetchTransactions();
+    return () => setMounted(false);
+  }, [deposits.nodes, change.nodes]);
+
+  // Helper function to format dates
+  const formatDate = (timestamp) => {
+    if (!timestamp) return "Pending";
+    return new Date(timestamp * 1000).toLocaleString();
+  };
+
+  // Helper function to format fees
+  const formatFee = (fee) => {
+    if (!fee) return "N/A";
+    return `${fee.toLocaleString()} sats`;
+  };
+
+  // Split transactions into pending and confirmed
+  const pendingTxs =
+    transactions && Array.isArray(transactions)
+      ? transactions.filter((tx) => !tx.status?.confirmed)
+      : [];
+  const confirmedTxs =
+    transactions && Array.isArray(transactions)
+      ? transactions.filter((tx) => tx.status?.confirmed)
+      : [];
+
+  // Render the transaction table
+  const renderTransactionTable = (txs) => (
+    <TableContainer component={Paper}>
+      <Table>
+        <TableHead>
+          <TableRow>
+            <StyledTableCell>Transaction ID</StyledTableCell>
+            <StyledTableCell>Time</StyledTableCell>
+            <StyledTableCell>Details</StyledTableCell>
+            <StyledTableCell align="right">Fee</StyledTableCell>
+            {tabValue === 1 && (
+              <StyledTableCell align="right">Block</StyledTableCell>
+            )}
+            <StyledTableCell>Actions</StyledTableCell>
+          </TableRow>
+        </TableHead>
+        <TableBody>
+          {txs.map((tx) => (
+            <TableRow key={tx.txid} hover>
+              <TableCell>
+                <Tooltip title={tx.txid}>
+                  <Box sx={{ display: "flex", alignItems: "center" }}>
+                    <span>{`${tx.txid.substring(0, 8)}...`}</span>
+                    {!tx.status.confirmed && (
+                      <Tooltip title="Pending">
+                        <Schedule
+                          color="action"
+                          fontSize="small"
+                          sx={{ ml: 1 }}
+                        />
+                      </Tooltip>
+                    )}
+                  </Box>
+                </Tooltip>
+              </TableCell>
+              <TableCell>{formatDate(tx.status.block_time)}</TableCell>
+              <TableCell>
+                <Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
+                  <StyledChip
+                    label={`${tx.size} vBytes`}
+                    size="small"
+                    variant="outlined"
+                  />
+                  <StyledChip
+                    label={tx.status.confirmed ? "Confirmed" : "Pending"}
+                    color={tx.status.confirmed ? "success" : "warning"}
+                    size="small"
+                  />
+                </Box>
+              </TableCell>
+              <TableCell align="right">{formatFee(tx.fee)}</TableCell>
+              {tabValue === 1 && (
+                <TableCell align="right">
+                  {tx.status.block_height?.toLocaleString()}
+                </TableCell>
+              )}
+              <TableCell>
+                <Tooltip title="View in Block Explorer">
+                  <IconButton
+                    onClick={() =>
+                      window.open(
+                        blockExplorerTransactionURL(tx.txid, network),
+                        "_blank",
+                      )
+                    }
+                    size="small"
+                  >
+                    <OpenInNew fontSize="small" />
+                  </IconButton>
+                </Tooltip>
+              </TableCell>
+            </TableRow>
+          ))}
+        </TableBody>
+      </Table>
+    </TableContainer>
+  );
+
+  return (
+    <StyledCard>
+      <CardHeader
+        title={
+          <Box
+            display="flex"
+            justifyContent="space-between"
+            alignItems="center"
+          >
+            <Typography variant="h6">Transactions</Typography>
+            <Tooltip title="Refresh transactions">
+              <IconButton onClick={fetchTransactions} disabled={isLoading}>
+                <Refresh />
+              </IconButton>
+            </Tooltip>
+          </Box>
+        }
+      />
+      <CardContent>
+        {error && (
+          <Typography color="error" gutterBottom>
+            Error: {error}
+          </Typography>
+        )}
+
+        <Tabs
+          value={tabValue}
+          onChange={(_, newValue) => setTabValue(newValue)}
+          sx={{ borderBottom: 1, borderColor: "divider" }}
+        >
+          <Tab
+            label={`Pending (${pendingTxs.length})`}
+            sx={{ fontWeight: "medium" }}
+          />
+          <Tab
+            label={`Confirmed (${confirmedTxs.length})`}
+            sx={{ fontWeight: "medium" }}
+          />
+        </Tabs>
+
+        <TabPanel value={tabValue} index={0}>
+          {isLoading ? (
+            <Box display="flex" justifyContent="center" p={3}>
+              <CircularProgress />
+            </Box>
+          ) : pendingTxs.length > 0 ? (
+            renderTransactionTable(pendingTxs)
+          ) : (
+            <Typography align="center" color="textSecondary" p={3}>
+              No pending transactions
+            </Typography>
+          )}
+        </TabPanel>
+
+        <TabPanel value={tabValue} index={1}>
+          {isLoading ? (
+            <Box display="flex" justifyContent="center" p={3}>
+              <CircularProgress />
+            </Box>
+          ) : confirmedTxs.length > 0 ? (
+            renderTransactionTable(confirmedTxs)
+          ) : (
+            <Typography align="center" color="textSecondary" p={3}>
+              No confirmed transactions
+            </Typography>
+          )}
+        </TabPanel>
+      </CardContent>
+    </StyledCard>
+  );
+}
+
+TransactionsTab.propTypes = {
+  network: PropTypes.string.isRequired,
+  transactions: PropTypes.array,
+  isLoading: PropTypes.bool.isRequired,
+  error: PropTypes.string,
+  getBlockchainClient: PropTypes.func.isRequired,
+  deposits: PropTypes.shape({
+    nodes: PropTypes.object.isRequired,
+  }).isRequired,
+  change: PropTypes.shape({
+    nodes: PropTypes.object.isRequired,
+  }).isRequired,
+  setTransactions: PropTypes.func.isRequired,
+  setTransactionsLoading: PropTypes.func.isRequired,
+  setTransactionsError: PropTypes.func.isRequired,
+};
+
+TransactionsTab.defaultProps = {
+  transactions: [],
+  error: null,
+};
+
+const mapStateToProps = (state) => {
+  return {
+    network: state.settings.network,
+    deposits: state.wallet.deposits,
+    change: state.wallet.change,
+    transactions: state.spend.transaction.transactions.transactions || [], // Access nested transactions array
+    isLoading: state.spend.transaction.transactions.isLoading,
+    error: state.spend.transaction.transactions.error,
+  };
+};
+
+const mapDispatchToProps = {
+  getBlockchainClient: updateBlockchainClient,
+  setTransactions,
+  setTransactionsLoading,
+  setTransactionsError,
+};
path: apps/coordinator/src/components/Wallet/TransactionsTab.jsx
user: login: Legend101Zz
body: removed the unecessary redux [states](https://github.com/caravan-bitcoin/caravan/pull/168/commits/fa37dbc85d5f08120d3cf77805fa18a94a735536)
created_at: 2025-02-21T16:41:00Z
author_association: CONTRIBUTOR
path: apps/coordinator/src/actions/transactionActions.js
user: login: Legend101Zz
body: changes [done](https://github.com/caravan-bitcoin/caravan/pull/168/commits/fa37dbc85d5f08120d3cf77805fa18a94a735536)
created_at: 2025-02-21T16:41:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,361 @@
+import React, { useState, useEffect } from "react";
+import PropTypes from "prop-types";
+import { connect } from "react-redux";
+import { styled } from "@mui/material/styles";
+import {
+  Table,
+  TableBody,
+  TableCell,
+  TableContainer,
+  TableHead,
+  TableRow,
+  Paper,
+  Tabs,
+  Tab,
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Card,
+  CardHeader,
+  CardContent,
+  Chip,
+} from "@mui/material";
+import { OpenInNew, Refresh, Schedule } from "@mui/icons-material";
+import { blockExplorerTransactionURL } from "@caravan/bitcoin";
+import {
+  setTransactions,
+  setTransactionsLoading,
+  setTransactionsError,
+} from "../../actions/transactionActions";
+import { updateBlockchainClient } from "../../actions/clientActions";
+
+const StyledCard = styled(Card)(({ theme }) => ({
+  marginBottom: theme.spacing(3),
+  height: "100%",
+}));
+
+const StyledTableCell = styled(TableCell)(({ theme }) => ({
+  fontWeight: theme.typography.fontWeightMedium,
+}));
+
+const StyledChip = styled(Chip)(({ theme }) => ({
+  marginRight: theme.spacing(1),
+}));
+
+// Helper component for tab panels, helps to switch between Pending and Completed Tx's
+function TabPanel({ children, value, index, ...other }) {
+  return (
+    <div
+      role="tabpanel"
+      hidden={value !== index}
+      id={`transaction-tabpanel-${index}`}
+      aria-labelledby={`transaction-tab-${index}`}
+      {...other}
+    >
+      {value === index && <Box py={2}>{children}</Box>}
+    </div>
+  );
+}
+
+TabPanel.propTypes = {
+  children: PropTypes.node,
+  index: PropTypes.number.isRequired,
+  value: PropTypes.number.isRequired,
+};
+
+function TransactionsTab({
+  network,
+  transactions,
+  isLoading,
+  error,
+  getBlockchainClient,
+  deposits,
+  change,
+  setTransactions,
+  setTransactionsLoading,
+  setTransactionsError,
+}) {
+  const [tabValue, setTabValue] = useState(0);
+  const [mounted, setMounted] = useState(true);
+
+  // Helper function to fetch individual transaction details
+  const fetchTransactionDetails = async (client, txid) => {
+    try {
+      const txDetails = await client.getTransaction(txid);
+      return {
+        ...txDetails,
+        confirmed: txDetails.status.confirmed,
+        timestamp: txDetails.status.block_time,
+      };
+    } catch (err) {
+      console.error(`Error fetching tx ${txid}:`, err);
+      return null;
+    }
+  };
+
+  // Main function to fetch all transaction data
+  const fetchTransactions = async () => {
+    if (!mounted) return;
+    setTransactionsLoading(true);
+
+    try {
+      const blockchainClient = await getBlockchainClient();
+      if (!blockchainClient) {
+        throw new Error("No blockchain client available");
+      }
+
+      // Get unique txids from all UTXOs
+      const txids = new Set();
+      [
+        ...Object.values(deposits.nodes),
+        ...Object.values(change.nodes),
+      ].forEach((node) => {
+        node.utxos.forEach((utxo) => txids.add(utxo.txid));
+      });
+
+      // Fetch details for each transaction
+      const txPromises = Array.from(txids).map((txid) =>
+        fetchTransactionDetails(blockchainClient, txid),
+      );
+
+      const txDetails = (await Promise.all(txPromises))
+        .filter((tx) => tx !== null)
+        .sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); // Sort by timestamp
+
+      if (mounted) {
+        setTransactions(txDetails);
+        setTransactionsError(null);
+      }
+    } catch (err) {
+      if (mounted) {
+        setTransactionsError(err.message);
+      }
+    } finally {
+      if (mounted) {
+        setTransactionsLoading(false);
+      }
+    }
+  };
+
+  // Initial data fetch and cleanup
+  useEffect(() => {
+    setMounted(true);
+    fetchTransactions();
+    return () => setMounted(false);
+  }, [deposits.nodes, change.nodes]);
+
+  // Helper function to format dates
+  const formatDate = (timestamp) => {
+    if (!timestamp) return "Pending";
+    return new Date(timestamp * 1000).toLocaleString();
+  };
+
+  // Helper function to format fees
+  const formatFee = (fee) => {
+    if (!fee) return "N/A";
+    return `${fee.toLocaleString()} sats`;
+  };
+
+  // Split transactions into pending and confirmed
+  const pendingTxs =
+    transactions && Array.isArray(transactions)
+      ? transactions.filter((tx) => !tx.status?.confirmed)
+      : [];
+  const confirmedTxs =
+    transactions && Array.isArray(transactions)
+      ? transactions.filter((tx) => tx.status?.confirmed)
+      : [];
+
+  // Render the transaction table
+  const renderTransactionTable = (txs) => (
path: apps/coordinator/src/components/Wallet/TransactionsTab.jsx
user: login: Legend101Zz
body: changes [done](https://github.com/caravan-bitcoin/caravan/pull/168/commits/0a5c90f6bdaea3ee0b73198e3bb08139179a81c3)
created_at: 2025-02-21T16:51:01Z
author_association: CONTRIBUTOR
path: .changeset/empty-ants-destroy.md
user: login: bucko13
body: cleanup. This is probably from an old merge. 
created_at: 2025-03-17T04:20:43Z
author_association: CONTRIBUTOR
path: .changeset/empty-ants-destroy.md
user: login: Legend101Zz
body: changes [done](https://github.com/caravan-bitcoin/caravan/pull/168/commits/c3d1e3b4cbbf4cf8512c99a813420da0775165e0#diff-4a1788cdad5570add1ee5b095b22fa1e919225b1bdb94973e920be11895b34b9)
created_at: 2025-03-17T16:43:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,273 @@
+import React, { useState, useEffect, useCallback } from "react";
+import { connect } from "react-redux";
+import {
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Tabs,
+  Tab,
+  Chip,
+} from "@mui/material";
+import { Refresh } from "@mui/icons-material";
+import { BlockchainClient } from "@caravan/clients";
+import { blockExplorerTransactionURL, Network } from "@caravan/bitcoin";
+import { updateBlockchainClient } from "../../actions/clientActions";
+import { TransactionTable } from "./TransactionsTable";
+
+// How are Transaction should look like
+interface Transaction {
path: apps/coordinator/src/components/Wallet/TransactionsTab.tsx
user: login: bucko13
body: is this not available from the clients package now? 
created_at: 2025-03-17T21:39:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,273 @@
+import React, { useState, useEffect, useCallback } from "react";
+import { connect } from "react-redux";
+import {
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Tabs,
+  Tab,
+  Chip,
+} from "@mui/material";
+import { Refresh } from "@mui/icons-material";
+import { BlockchainClient } from "@caravan/clients";
+import { blockExplorerTransactionURL, Network } from "@caravan/bitcoin";
+import { updateBlockchainClient } from "../../actions/clientActions";
+import { TransactionTable } from "./TransactionsTable";
+
+// How are Transaction should look like
+interface Transaction {
+  txid: string;
+  version: number;
+  locktime: number;
+  vin: any[]; // Input vector
+  vout: any[]; // Output vector
+  weight: number;
+  status: {
+    confirmed: boolean;
+    blockTime?: number;
+    blockHeight?: number;
+  };
+  size: number;
+  fee: number;
+}
+
+interface TransactionsTabProps {
+  network: Network;
+  getBlockchainClient: () => Promise<BlockchainClient>;
+  deposits: {
+    nodes: Record<string, any>;
+  };
+  change: {
+    nodes: Record<string, any>;
+  };
+  client: {
+    type: string;
+    blockchainClient?: BlockchainClient;
+  };
+}
+
+const TransactionsTab: React.FC<TransactionsTabProps> = ({
+  network,
+  getBlockchainClient,
+  deposits,
+  change,
+  client,
+}) => {
+  const [tabValue, setTabValue] = useState(0);
+  const [transactions, setTransactions] = useState<Transaction[]>([]);
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const [sortBy, setSortBy] = useState<string>("blockTime");
+  const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
+
+  // Fetch transactions
+  const fetchTransactions = useCallback(async () => {
+    setIsLoading(true);
+    setError(null);
+
+    try {
+      const blockchainClient = await getBlockchainClient();
+      if (!blockchainClient) {
+        throw new Error("No blockchain client available");
+      }
+
+      // Get unique transaction IDs from UTXOs
+      const txids = new Set<string>();
+      [
+        ...Object.values(deposits.nodes),
+        ...Object.values(change.nodes),
+      ].forEach((node) => {
+        node.utxos.forEach((utxo: { txid: string }) => {
+          txids.add(utxo.txid);
+        });
+      });
+
+      // Fetch transaction details in parallel
+      const txDetails = await Promise.all(
+        Array.from(txids).map(async (txid) => {
+          try {
+            const tx = await blockchainClient.getTransaction(txid);
path: apps/coordinator/src/components/Wallet/TransactionsTab.tsx
user: login: bucko13
body: I noticed when testing this against a local regtest node that there was no fee for the private client requests. I think this is because in the clients package we're using getrawtransaction rpc call, which looking at the [docs](https://developer.bitcoin.org/reference/rpc/getrawtransaction.html) does not include fee.

I think we might want to do `gettransaction` instead. From the [docs](https://developer.bitcoin.org/reference/rpc/gettransaction.html):

```
Get detailed information about in-wallet transaction <txid>
```

So we could create a second method called `getWalletTransaction` that uses this and then we see that we get some more useful information in the response including fee. I think this should work since in order for caravan to work with a local node you'll have to have imported the descriptors into your node's wallet. The wallet tracking gives fees since it will track prevout values too when validating wallet txs which is necessary for fees, otherwise you'd have to depend on having tx indexing turned on. 
created_at: 2025-03-17T21:51:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,273 @@
+import React, { useState, useEffect, useCallback } from "react";
+import { connect } from "react-redux";
+import {
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Tabs,
+  Tab,
+  Chip,
+} from "@mui/material";
+import { Refresh } from "@mui/icons-material";
+import { BlockchainClient } from "@caravan/clients";
+import { blockExplorerTransactionURL, Network } from "@caravan/bitcoin";
+import { updateBlockchainClient } from "../../actions/clientActions";
+import { TransactionTable } from "./TransactionsTable";
+
+// How are Transaction should look like
+interface Transaction {
+  txid: string;
+  version: number;
+  locktime: number;
+  vin: any[]; // Input vector
+  vout: any[]; // Output vector
+  weight: number;
+  status: {
+    confirmed: boolean;
+    blockTime?: number;
+    blockHeight?: number;
+  };
+  size: number;
+  fee: number;
+}
+
+interface TransactionsTabProps {
+  network: Network;
+  getBlockchainClient: () => Promise<BlockchainClient>;
+  deposits: {
+    nodes: Record<string, any>;
+  };
+  change: {
+    nodes: Record<string, any>;
+  };
+  client: {
+    type: string;
+    blockchainClient?: BlockchainClient;
+  };
+}
+
+const TransactionsTab: React.FC<TransactionsTabProps> = ({
+  network,
+  getBlockchainClient,
+  deposits,
+  change,
+  client,
+}) => {
+  const [tabValue, setTabValue] = useState(0);
+  const [transactions, setTransactions] = useState<Transaction[]>([]);
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const [sortBy, setSortBy] = useState<string>("blockTime");
+  const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
+
+  // Fetch transactions
+  const fetchTransactions = useCallback(async () => {
+    setIsLoading(true);
+    setError(null);
+
+    try {
+      const blockchainClient = await getBlockchainClient();
path: apps/coordinator/src/components/Wallet/TransactionsTab.tsx
user: login: bucko13
body: did you try the `useGetClient` hook for this? 
created_at: 2025-03-17T21:54:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,273 @@
+import React, { useState, useEffect, useCallback } from "react";
+import { connect } from "react-redux";
+import {
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Tabs,
+  Tab,
+  Chip,
+} from "@mui/material";
+import { Refresh } from "@mui/icons-material";
+import { BlockchainClient } from "@caravan/clients";
+import { blockExplorerTransactionURL, Network } from "@caravan/bitcoin";
+import { updateBlockchainClient } from "../../actions/clientActions";
+import { TransactionTable } from "./TransactionsTable";
+
+// How are Transaction should look like
+interface Transaction {
+  txid: string;
+  version: number;
+  locktime: number;
+  vin: any[]; // Input vector
+  vout: any[]; // Output vector
+  weight: number;
+  status: {
+    confirmed: boolean;
+    blockTime?: number;
+    blockHeight?: number;
+  };
+  size: number;
+  fee: number;
+}
+
+interface TransactionsTabProps {
+  network: Network;
+  getBlockchainClient: () => Promise<BlockchainClient>;
+  deposits: {
+    nodes: Record<string, any>;
+  };
+  change: {
+    nodes: Record<string, any>;
+  };
+  client: {
+    type: string;
+    blockchainClient?: BlockchainClient;
+  };
+}
+
+const TransactionsTab: React.FC<TransactionsTabProps> = ({
+  network,
+  getBlockchainClient,
+  deposits,
+  change,
+  client,
+}) => {
+  const [tabValue, setTabValue] = useState(0);
+  const [transactions, setTransactions] = useState<Transaction[]>([]);
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const [sortBy, setSortBy] = useState<string>("blockTime");
+  const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
+
+  // Fetch transactions
+  const fetchTransactions = useCallback(async () => {
+    setIsLoading(true);
+    setError(null);
+
+    try {
+      const blockchainClient = await getBlockchainClient();
+      if (!blockchainClient) {
+        throw new Error("No blockchain client available");
+      }
+
+      // Get unique transaction IDs from UTXOs
+      const txids = new Set<string>();
+      [
+        ...Object.values(deposits.nodes),
+        ...Object.values(change.nodes),
+      ].forEach((node) => {
+        node.utxos.forEach((utxo: { txid: string }) => {
+          txids.add(utxo.txid);
+        });
+      });
+
+      // Fetch transaction details in parallel
+      const txDetails = await Promise.all(
+        Array.from(txids).map(async (txid) => {
+          try {
+            const tx = await blockchainClient.getTransaction(txid);
+            return tx;
+          } catch (err) {
+            console.error(`Error fetching tx ${txid}:`, err);
+            return null;
+          }
+        }),
+      );
+      setTransactions(txDetails.filter((tx): tx is Transaction => tx !== null));
+      setError(null);
+    } catch (err) {
+      setError((err as Error).message);
+    } finally {
+      setIsLoading(false);
+    }
+  }, [getBlockchainClient, deposits.nodes, change.nodes]);
+
+  // Initial loading
+  useEffect(() => {
+    fetchTransactions();
+  }, [fetchTransactions]);
+
+  // Handle sorting
+  const handleSort = (property: string) => {
+    const isAsc = sortBy === property && sortDirection === "asc";
+    setSortDirection(isAsc ? "desc" : "asc");
+    setSortBy(property);
+  };
+
+  const handleTransactionClick = useCallback(
+    (txid: string) => {
+      // Check if user is using a private node
+      const isPrivateNode = client.type === "private";
+
+      if (isPrivateNode) {
+        // If using private node, warn about privacy implications
+        const confirmed = window.confirm(
+          "Opening in a block explorer may expose your wallet activity to third parties. Continue?",
+        );
+        if (!confirmed) return;
+      }
+      // Determine which block explorer to use based on the blockchain client type
+      let explorerUrl;
+
+      if (client.blockchainClient && client.blockchainClient.type) {
+        const clientType = client.blockchainClient.type;
+
+        switch (clientType) {
+          case "mempool":
+            // Use mempool.space explorer
+            explorerUrl = `https://${network === "mainnet" ? "" : "testnet."}mempool.space/tx/${txid}`;
+            break;
+
+          case "blockstream":
+            // Use blockstream.info explorer
+            explorerUrl = `https://blockstream.info/${network === "mainnet" ? "" : "testnet/"}tx/${txid}`;
+            break;
+
+          default:
+            // Fall back to the default explorer
+            explorerUrl = blockExplorerTransactionURL(txid, network);
+        }
+      } else {
+        // If no blockchain client specified, use default
+        explorerUrl = blockExplorerTransactionURL(txid, network);
+      }
+
+      window.open(explorerUrl, "_blank");
+      window.open(explorerUrl, "_blank");
path: apps/coordinator/src/components/Wallet/TransactionsTab.tsx
user: login: bucko13
body: why did this need to be duplicated? 
created_at: 2025-03-17T21:57:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,273 @@
+import React, { useState, useEffect, useCallback } from "react";
+import { connect } from "react-redux";
+import {
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Tabs,
+  Tab,
+  Chip,
+} from "@mui/material";
+import { Refresh } from "@mui/icons-material";
+import { BlockchainClient } from "@caravan/clients";
+import { blockExplorerTransactionURL, Network } from "@caravan/bitcoin";
+import { updateBlockchainClient } from "../../actions/clientActions";
+import { TransactionTable } from "./TransactionsTable";
+
+// How are Transaction should look like
+interface Transaction {
+  txid: string;
+  version: number;
+  locktime: number;
+  vin: any[]; // Input vector
+  vout: any[]; // Output vector
+  weight: number;
+  status: {
+    confirmed: boolean;
+    blockTime?: number;
+    blockHeight?: number;
+  };
+  size: number;
+  fee: number;
+}
+
+interface TransactionsTabProps {
+  network: Network;
+  getBlockchainClient: () => Promise<BlockchainClient>;
+  deposits: {
+    nodes: Record<string, any>;
+  };
+  change: {
+    nodes: Record<string, any>;
+  };
+  client: {
+    type: string;
+    blockchainClient?: BlockchainClient;
+  };
+}
+
+const TransactionsTab: React.FC<TransactionsTabProps> = ({
+  network,
+  getBlockchainClient,
+  deposits,
+  change,
+  client,
+}) => {
+  const [tabValue, setTabValue] = useState(0);
+  const [transactions, setTransactions] = useState<Transaction[]>([]);
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const [sortBy, setSortBy] = useState<string>("blockTime");
+  const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
+
+  // Fetch transactions
+  const fetchTransactions = useCallback(async () => {
+    setIsLoading(true);
+    setError(null);
+
+    try {
+      const blockchainClient = await getBlockchainClient();
+      if (!blockchainClient) {
+        throw new Error("No blockchain client available");
+      }
+
+      // Get unique transaction IDs from UTXOs
+      const txids = new Set<string>();
+      [
+        ...Object.values(deposits.nodes),
+        ...Object.values(change.nodes),
+      ].forEach((node) => {
+        node.utxos.forEach((utxo: { txid: string }) => {
+          txids.add(utxo.txid);
+        });
+      });
+
+      // Fetch transaction details in parallel
+      const txDetails = await Promise.all(
+        Array.from(txids).map(async (txid) => {
+          try {
+            const tx = await blockchainClient.getTransaction(txid);
+            return tx;
+          } catch (err) {
+            console.error(`Error fetching tx ${txid}:`, err);
+            return null;
+          }
+        }),
+      );
+      setTransactions(txDetails.filter((tx): tx is Transaction => tx !== null));
+      setError(null);
+    } catch (err) {
+      setError((err as Error).message);
+    } finally {
+      setIsLoading(false);
+    }
+  }, [getBlockchainClient, deposits.nodes, change.nodes]);
+
+  // Initial loading
+  useEffect(() => {
+    fetchTransactions();
+  }, [fetchTransactions]);
+
+  // Handle sorting
+  const handleSort = (property: string) => {
+    const isAsc = sortBy === property && sortDirection === "asc";
+    setSortDirection(isAsc ? "desc" : "asc");
+    setSortBy(property);
+  };
+
+  const handleTransactionClick = useCallback(
+    (txid: string) => {
+      // Check if user is using a private node
+      const isPrivateNode = client.type === "private";
+
+      if (isPrivateNode) {
+        // If using private node, warn about privacy implications
+        const confirmed = window.confirm(
+          "Opening in a block explorer may expose your wallet activity to third parties. Continue?",
+        );
+        if (!confirmed) return;
+      }
+      // Determine which block explorer to use based on the blockchain client type
+      let explorerUrl;
+
+      if (client.blockchainClient && client.blockchainClient.type) {
+        const clientType = client.blockchainClient.type;
+
+        switch (clientType) {
+          case "mempool":
+            // Use mempool.space explorer
+            explorerUrl = `https://${network === "mainnet" ? "" : "testnet."}mempool.space/tx/${txid}`;
+            break;
+
+          case "blockstream":
+            // Use blockstream.info explorer
+            explorerUrl = `https://blockstream.info/${network === "mainnet" ? "" : "testnet/"}tx/${txid}`;
+            break;
+
+          default:
+            // Fall back to the default explorer
+            explorerUrl = blockExplorerTransactionURL(txid, network);
path: apps/coordinator/src/components/Wallet/TransactionsTab.tsx
user: login: bucko13
body: if you updated this to support client type as an optional arg then you could get rid of all the other code above. 
created_at: 2025-03-17T21:59:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,273 @@
+import React, { useState, useEffect, useCallback } from "react";
+import { connect } from "react-redux";
+import {
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Tabs,
+  Tab,
+  Chip,
+} from "@mui/material";
+import { Refresh } from "@mui/icons-material";
+import { BlockchainClient } from "@caravan/clients";
+import { blockExplorerTransactionURL, Network } from "@caravan/bitcoin";
+import { updateBlockchainClient } from "../../actions/clientActions";
+import { TransactionTable } from "./TransactionsTable";
+
+// How are Transaction should look like
+interface Transaction {
+  txid: string;
+  version: number;
+  locktime: number;
+  vin: any[]; // Input vector
+  vout: any[]; // Output vector
+  weight: number;
+  status: {
+    confirmed: boolean;
+    blockTime?: number;
+    blockHeight?: number;
+  };
+  size: number;
+  fee: number;
+}
+
+interface TransactionsTabProps {
+  network: Network;
+  getBlockchainClient: () => Promise<BlockchainClient>;
+  deposits: {
+    nodes: Record<string, any>;
+  };
+  change: {
+    nodes: Record<string, any>;
+  };
+  client: {
+    type: string;
+    blockchainClient?: BlockchainClient;
+  };
+}
+
+const TransactionsTab: React.FC<TransactionsTabProps> = ({
+  network,
+  getBlockchainClient,
+  deposits,
+  change,
+  client,
+}) => {
+  const [tabValue, setTabValue] = useState(0);
+  const [transactions, setTransactions] = useState<Transaction[]>([]);
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const [sortBy, setSortBy] = useState<string>("blockTime");
+  const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
+
+  // Fetch transactions
+  const fetchTransactions = useCallback(async () => {
+    setIsLoading(true);
+    setError(null);
+
+    try {
+      const blockchainClient = await getBlockchainClient();
+      if (!blockchainClient) {
+        throw new Error("No blockchain client available");
+      }
+
+      // Get unique transaction IDs from UTXOs
+      const txids = new Set<string>();
+      [
+        ...Object.values(deposits.nodes),
+        ...Object.values(change.nodes),
+      ].forEach((node) => {
+        node.utxos.forEach((utxo: { txid: string }) => {
+          txids.add(utxo.txid);
+        });
+      });
+
+      // Fetch transaction details in parallel
+      const txDetails = await Promise.all(
+        Array.from(txids).map(async (txid) => {
+          try {
+            const tx = await blockchainClient.getTransaction(txid);
+            return tx;
+          } catch (err) {
+            console.error(`Error fetching tx ${txid}:`, err);
+            return null;
+          }
+        }),
+      );
+      setTransactions(txDetails.filter((tx): tx is Transaction => tx !== null));
+      setError(null);
+    } catch (err) {
+      setError((err as Error).message);
+    } finally {
+      setIsLoading(false);
+    }
+  }, [getBlockchainClient, deposits.nodes, change.nodes]);
+
+  // Initial loading
+  useEffect(() => {
+    fetchTransactions();
+  }, [fetchTransactions]);
+
+  // Handle sorting
+  const handleSort = (property: string) => {
+    const isAsc = sortBy === property && sortDirection === "asc";
+    setSortDirection(isAsc ? "desc" : "asc");
+    setSortBy(property);
+  };
+
+  const handleTransactionClick = useCallback(
+    (txid: string) => {
+      // Check if user is using a private node
+      const isPrivateNode = client.type === "private";
+
+      if (isPrivateNode) {
+        // If using private node, warn about privacy implications
+        const confirmed = window.confirm(
+          "Opening in a block explorer may expose your wallet activity to third parties. Continue?",
+        );
+        if (!confirmed) return;
+      }
+      // Determine which block explorer to use based on the blockchain client type
+      let explorerUrl;
+
+      if (client.blockchainClient && client.blockchainClient.type) {
+        const clientType = client.blockchainClient.type;
+
+        switch (clientType) {
+          case "mempool":
+            // Use mempool.space explorer
+            explorerUrl = `https://${network === "mainnet" ? "" : "testnet."}mempool.space/tx/${txid}`;
+            break;
+
+          case "blockstream":
+            // Use blockstream.info explorer
+            explorerUrl = `https://blockstream.info/${network === "mainnet" ? "" : "testnet/"}tx/${txid}`;
+            break;
+
+          default:
+            // Fall back to the default explorer
+            explorerUrl = blockExplorerTransactionURL(txid, network);
+        }
+      } else {
+        // If no blockchain client specified, use default
+        explorerUrl = blockExplorerTransactionURL(txid, network);
path: apps/coordinator/src/components/Wallet/TransactionsTab.tsx
user: login: bucko13
body: feels like this is duplicative of the default case above. 
created_at: 2025-03-17T22:00:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,273 @@
+import React, { useState, useEffect, useCallback } from "react";
+import { connect } from "react-redux";
+import {
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Tabs,
+  Tab,
+  Chip,
+} from "@mui/material";
+import { Refresh } from "@mui/icons-material";
+import { BlockchainClient } from "@caravan/clients";
+import { blockExplorerTransactionURL, Network } from "@caravan/bitcoin";
+import { updateBlockchainClient } from "../../actions/clientActions";
+import { TransactionTable } from "./TransactionsTable";
+
+// How are Transaction should look like
+interface Transaction {
+  txid: string;
+  version: number;
+  locktime: number;
+  vin: any[]; // Input vector
+  vout: any[]; // Output vector
+  weight: number;
+  status: {
+    confirmed: boolean;
+    blockTime?: number;
+    blockHeight?: number;
+  };
+  size: number;
+  fee: number;
+}
+
+interface TransactionsTabProps {
+  network: Network;
+  getBlockchainClient: () => Promise<BlockchainClient>;
+  deposits: {
+    nodes: Record<string, any>;
+  };
+  change: {
+    nodes: Record<string, any>;
+  };
+  client: {
+    type: string;
+    blockchainClient?: BlockchainClient;
+  };
+}
+
+const TransactionsTab: React.FC<TransactionsTabProps> = ({
+  network,
+  getBlockchainClient,
+  deposits,
+  change,
+  client,
+}) => {
+  const [tabValue, setTabValue] = useState(0);
+  const [transactions, setTransactions] = useState<Transaction[]>([]);
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const [sortBy, setSortBy] = useState<string>("blockTime");
+  const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
+
+  // Fetch transactions
+  const fetchTransactions = useCallback(async () => {
+    setIsLoading(true);
+    setError(null);
+
+    try {
+      const blockchainClient = await getBlockchainClient();
+      if (!blockchainClient) {
+        throw new Error("No blockchain client available");
+      }
+
+      // Get unique transaction IDs from UTXOs
+      const txids = new Set<string>();
+      [
+        ...Object.values(deposits.nodes),
+        ...Object.values(change.nodes),
+      ].forEach((node) => {
+        node.utxos.forEach((utxo: { txid: string }) => {
+          txids.add(utxo.txid);
+        });
+      });
+
+      // Fetch transaction details in parallel
+      const txDetails = await Promise.all(
+        Array.from(txids).map(async (txid) => {
+          try {
+            const tx = await blockchainClient.getTransaction(txid);
+            return tx;
+          } catch (err) {
+            console.error(`Error fetching tx ${txid}:`, err);
+            return null;
+          }
+        }),
+      );
+      setTransactions(txDetails.filter((tx): tx is Transaction => tx !== null));
+      setError(null);
+    } catch (err) {
+      setError((err as Error).message);
+    } finally {
+      setIsLoading(false);
+    }
+  }, [getBlockchainClient, deposits.nodes, change.nodes]);
+
+  // Initial loading
+  useEffect(() => {
+    fetchTransactions();
+  }, [fetchTransactions]);
+
+  // Handle sorting
+  const handleSort = (property: string) => {
+    const isAsc = sortBy === property && sortDirection === "asc";
+    setSortDirection(isAsc ? "desc" : "asc");
+    setSortBy(property);
+  };
+
+  const handleTransactionClick = useCallback(
path: apps/coordinator/src/components/Wallet/TransactionsTab.tsx
user: login: bucko13
body: `handleExplorerLinkClick` feels like a better function name. 
created_at: 2025-03-17T22:00:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,273 @@
+import React, { useState, useEffect, useCallback } from "react";
+import { connect } from "react-redux";
+import {
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Tabs,
+  Tab,
+  Chip,
+} from "@mui/material";
+import { Refresh } from "@mui/icons-material";
+import { BlockchainClient } from "@caravan/clients";
+import { blockExplorerTransactionURL, Network } from "@caravan/bitcoin";
+import { updateBlockchainClient } from "../../actions/clientActions";
+import { TransactionTable } from "./TransactionsTable";
+
+// How are Transaction should look like
+interface Transaction {
+  txid: string;
+  version: number;
+  locktime: number;
+  vin: any[]; // Input vector
+  vout: any[]; // Output vector
+  weight: number;
+  status: {
+    confirmed: boolean;
+    blockTime?: number;
+    blockHeight?: number;
+  };
+  size: number;
+  fee: number;
+}
+
+interface TransactionsTabProps {
+  network: Network;
+  getBlockchainClient: () => Promise<BlockchainClient>;
+  deposits: {
+    nodes: Record<string, any>;
+  };
+  change: {
+    nodes: Record<string, any>;
+  };
+  client: {
+    type: string;
+    blockchainClient?: BlockchainClient;
+  };
+}
+
+const TransactionsTab: React.FC<TransactionsTabProps> = ({
+  network,
+  getBlockchainClient,
+  deposits,
+  change,
+  client,
+}) => {
+  const [tabValue, setTabValue] = useState(0);
+  const [transactions, setTransactions] = useState<Transaction[]>([]);
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const [sortBy, setSortBy] = useState<string>("blockTime");
+  const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
+
+  // Fetch transactions
+  const fetchTransactions = useCallback(async () => {
path: apps/coordinator/src/components/Wallet/TransactionsTab.tsx
user: login: bucko13
body: Do you think it would be worthwhile to pull these `useCallbacks` out into their own file to clean up the component? You can make them reusable hooks, create a `TransactionsTable` directory and then put them in `TransactionsTable/hooks.ts`. It might be nice to cleanup the component to focus just on the view elements. 
created_at: 2025-03-17T22:03:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,273 @@
+import React, { useState, useEffect, useCallback } from "react";
+import { connect } from "react-redux";
+import {
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Tabs,
+  Tab,
+  Chip,
+} from "@mui/material";
+import { Refresh } from "@mui/icons-material";
+import { BlockchainClient } from "@caravan/clients";
+import { blockExplorerTransactionURL, Network } from "@caravan/bitcoin";
+import { updateBlockchainClient } from "../../actions/clientActions";
+import { TransactionTable } from "./TransactionsTable";
+
+// How are Transaction should look like
+interface Transaction {
+  txid: string;
+  version: number;
+  locktime: number;
+  vin: any[]; // Input vector
+  vout: any[]; // Output vector
+  weight: number;
+  status: {
+    confirmed: boolean;
+    blockTime?: number;
+    blockHeight?: number;
+  };
+  size: number;
+  fee: number;
+}
+
+interface TransactionsTabProps {
+  network: Network;
+  getBlockchainClient: () => Promise<BlockchainClient>;
+  deposits: {
+    nodes: Record<string, any>;
+  };
+  change: {
+    nodes: Record<string, any>;
+  };
+  client: {
+    type: string;
+    blockchainClient?: BlockchainClient;
+  };
+}
+
+const TransactionsTab: React.FC<TransactionsTabProps> = ({
+  network,
+  getBlockchainClient,
+  deposits,
+  change,
+  client,
+}) => {
+  const [tabValue, setTabValue] = useState(0);
+  const [transactions, setTransactions] = useState<Transaction[]>([]);
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const [sortBy, setSortBy] = useState<string>("blockTime");
+  const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
+
+  // Fetch transactions
+  const fetchTransactions = useCallback(async () => {
+    setIsLoading(true);
+    setError(null);
+
+    try {
+      const blockchainClient = await getBlockchainClient();
+      if (!blockchainClient) {
+        throw new Error("No blockchain client available");
+      }
+
+      // Get unique transaction IDs from UTXOs
+      const txids = new Set<string>();
+      [
+        ...Object.values(deposits.nodes),
+        ...Object.values(change.nodes),
+      ].forEach((node) => {
+        node.utxos.forEach((utxo: { txid: string }) => {
+          txids.add(utxo.txid);
+        });
+      });
+
+      // Fetch transaction details in parallel
+      const txDetails = await Promise.all(
+        Array.from(txids).map(async (txid) => {
+          try {
+            const tx = await blockchainClient.getTransaction(txid);
+            return tx;
+          } catch (err) {
+            console.error(`Error fetching tx ${txid}:`, err);
+            return null;
+          }
+        }),
+      );
+      setTransactions(txDetails.filter((tx): tx is Transaction => tx !== null));
+      setError(null);
+    } catch (err) {
+      setError((err as Error).message);
+    } finally {
+      setIsLoading(false);
+    }
+  }, [getBlockchainClient, deposits.nodes, change.nodes]);
+
+  // Initial loading
+  useEffect(() => {
+    fetchTransactions();
+  }, [fetchTransactions]);
+
+  // Handle sorting
+  const handleSort = (property: string) => {
+    const isAsc = sortBy === property && sortDirection === "asc";
+    setSortDirection(isAsc ? "desc" : "asc");
+    setSortBy(property);
+  };
+
+  const handleTransactionClick = useCallback(
path: apps/coordinator/src/components/Wallet/TransactionsTab.tsx
user: login: bucko13
body: could also be a reusable hook that is in a `TransactionsTable/hooks.ts` file, something like `useHandleExplorerTxLinkClick` that returns a function to be called. 
created_at: 2025-03-17T22:03:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,273 @@
+import React, { useState, useEffect, useCallback } from "react";
+import { connect } from "react-redux";
+import {
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Tabs,
+  Tab,
+  Chip,
+} from "@mui/material";
+import { Refresh } from "@mui/icons-material";
+import { BlockchainClient } from "@caravan/clients";
+import { blockExplorerTransactionURL, Network } from "@caravan/bitcoin";
+import { updateBlockchainClient } from "../../actions/clientActions";
+import { TransactionTable } from "./TransactionsTable";
+
+// How are Transaction should look like
+interface Transaction {
+  txid: string;
+  version: number;
+  locktime: number;
+  vin: any[]; // Input vector
+  vout: any[]; // Output vector
+  weight: number;
+  status: {
+    confirmed: boolean;
+    blockTime?: number;
+    blockHeight?: number;
+  };
+  size: number;
+  fee: number;
+}
+
+interface TransactionsTabProps {
+  network: Network;
+  getBlockchainClient: () => Promise<BlockchainClient>;
+  deposits: {
+    nodes: Record<string, any>;
+  };
+  change: {
+    nodes: Record<string, any>;
+  };
+  client: {
+    type: string;
+    blockchainClient?: BlockchainClient;
+  };
+}
+
+const TransactionsTab: React.FC<TransactionsTabProps> = ({
+  network,
+  getBlockchainClient,
+  deposits,
+  change,
+  client,
+}) => {
+  const [tabValue, setTabValue] = useState(0);
+  const [transactions, setTransactions] = useState<Transaction[]>([]);
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const [sortBy, setSortBy] = useState<string>("blockTime");
+  const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
+
+  // Fetch transactions
+  const fetchTransactions = useCallback(async () => {
+    setIsLoading(true);
+    setError(null);
+
+    try {
+      const blockchainClient = await getBlockchainClient();
+      if (!blockchainClient) {
+        throw new Error("No blockchain client available");
+      }
+
+      // Get unique transaction IDs from UTXOs
+      const txids = new Set<string>();
+      [
+        ...Object.values(deposits.nodes),
+        ...Object.values(change.nodes),
+      ].forEach((node) => {
+        node.utxos.forEach((utxo: { txid: string }) => {
+          txids.add(utxo.txid);
+        });
+      });
+
+      // Fetch transaction details in parallel
+      const txDetails = await Promise.all(
+        Array.from(txids).map(async (txid) => {
+          try {
+            const tx = await blockchainClient.getTransaction(txid);
+            return tx;
+          } catch (err) {
+            console.error(`Error fetching tx ${txid}:`, err);
+            return null;
+          }
+        }),
+      );
+      setTransactions(txDetails.filter((tx): tx is Transaction => tx !== null));
+      setError(null);
+    } catch (err) {
+      setError((err as Error).message);
+    } finally {
+      setIsLoading(false);
+    }
+  }, [getBlockchainClient, deposits.nodes, change.nodes]);
+
+  // Initial loading
+  useEffect(() => {
+    fetchTransactions();
+  }, [fetchTransactions]);
+
+  // Handle sorting
+  const handleSort = (property: string) => {
+    const isAsc = sortBy === property && sortDirection === "asc";
+    setSortDirection(isAsc ? "desc" : "asc");
+    setSortBy(property);
+  };
+
+  const handleTransactionClick = useCallback(
+    (txid: string) => {
+      // Check if user is using a private node
+      const isPrivateNode = client.type === "private";
+
+      if (isPrivateNode) {
+        // If using private node, warn about privacy implications
+        const confirmed = window.confirm(
+          "Opening in a block explorer may expose your wallet activity to third parties. Continue?",
+        );
+        if (!confirmed) return;
+      }
+      // Determine which block explorer to use based on the blockchain client type
+      let explorerUrl;
+
+      if (client.blockchainClient && client.blockchainClient.type) {
+        const clientType = client.blockchainClient.type;
+
+        switch (clientType) {
+          case "mempool":
+            // Use mempool.space explorer
+            explorerUrl = `https://${network === "mainnet" ? "" : "testnet."}mempool.space/tx/${txid}`;
+            break;
+
+          case "blockstream":
+            // Use blockstream.info explorer
+            explorerUrl = `https://blockstream.info/${network === "mainnet" ? "" : "testnet/"}tx/${txid}`;
+            break;
+
+          default:
+            // Fall back to the default explorer
+            explorerUrl = blockExplorerTransactionURL(txid, network);
+        }
+      } else {
+        // If no blockchain client specified, use default
+        explorerUrl = blockExplorerTransactionURL(txid, network);
+      }
+
+      window.open(explorerUrl, "_blank");
+      window.open(explorerUrl, "_blank");
+    },
+    [network, client],
+  );
+
+  // Filter and sort transactions
+  const getSortedTransactions = useCallback(
+    (txs: Transaction[]) => {
+      return [...txs].sort((a, b) => {
+        // Handle different sorting properties
+        let comparison = 0;
+        switch (sortBy) {
+          case "blockTime":
+            comparison = (a.status.blockTime || 0) - (b.status.blockTime || 0);
+            break;
+          case "size":
+            comparison = a.size - b.size;
+            break;
+          case "fee":
+            comparison = (a.fee || 0) - (b.fee || 0);
+            break;
+          default:
+            return 0;
+        }
+        return sortDirection === "desc" ? -comparison : comparison;
+      });
+    },
+    [sortBy, sortDirection],
+  );
+
+  // Split and sort transactions
+  const pendingTxs = getSortedTransactions(
+    transactions.filter((tx) => !tx.status.confirmed),
+  );
+  const confirmedTxs = getSortedTransactions(
+    transactions.filter((tx) => tx.status.confirmed),
+  );
+
+  return (
+    <div>
+      <Box
+        display="flex"
+        justifyContent="space-between"
+        alignItems="center"
+        mb={2}
+      >
+        <Typography variant="h6">Transactions</Typography>
+        <Tooltip title="Refresh transactions">
+          <IconButton onClick={fetchTransactions} disabled={isLoading}>
+            <Refresh />
+          </IconButton>
+        </Tooltip>
+      </Box>
+
+      {error && (
+        <Typography color="error" gutterBottom>
+          Error: {error}
+        </Typography>
+      )}
+
+      <Tabs value={tabValue} onChange={(_, value) => setTabValue(value)}>
+        <Tab
+          label={
+            <Box display="flex" alignItems="center" gap={1}>
+              <span>Pending</span>
+              <Chip
+                label={pendingTxs.length}
+                size="small"
+                color={pendingTxs.length > 0 ? "primary" : "default"}
+              />
+            </Box>
+          }
+        />
+        <Tab
+          label={
+            <Box display="flex" alignItems="center" gap={1}>
+              <span>Confirmed</span>
+              <Chip label={confirmedTxs.length} size="small" />
+            </Box>
+          }
+        />
+      </Tabs>
+
+      <Box mt={2}>
+        {isLoading ? (
+          <Box display="flex" justifyContent="center" p={3}>
+            <CircularProgress />
+          </Box>
+        ) : (
+          <TransactionTable
+            transactions={tabValue === 0 ? pendingTxs : confirmedTxs}
+            onSort={handleSort}
+            sortBy={sortBy}
+            sortDirection={sortDirection}
+            network={network}
+            onClickTransaction={handleTransactionClick}
+          />
+        )}
+      </Box>
+    </div>
+  );
+};
+
+const mapStateToProps = (state: any) => ({
+  network: state.settings.network,
+  deposits: state.wallet.deposits,
+  change: state.wallet.change,
+  client: state.client,
+});
+
+const mapDispatchToProps = {
+  getBlockchainClient: updateBlockchainClient,
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(TransactionsTab);
path: apps/coordinator/src/components/Wallet/TransactionsTab.tsx
user: login: bucko13
body: shouldn't need connect or mapStateToProps anymore. There is a useDispatch and useSelector hook that you can use and not require the props at all.
created_at: 2025-03-17T22:04:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,218 @@
+import React, { useState } from "react";
+import {
+  Table,
+  TableBody,
+  TableCell,
+  TableContainer,
+  TableHead,
+  TableRow,
+  TableSortLabel,
+  Paper,
+  Chip,
+  IconButton,
+  Box,
+  Tooltip,
+  Typography,
+  Alert,
+  Snackbar,
+} from "@mui/material";
+import { satoshisToBitcoins } from "@caravan/bitcoin";
+import { formatDistanceToNow } from "date-fns";
+import { OpenInNew } from "@mui/icons-material";
+
+interface TransactionTableProps {
+  transactions: Transaction[];
+  onSort: (property: keyof Transaction) => void;
+  sortBy: string;
+  sortDirection: "asc" | "desc";
+  network?: string;
+  onClickTransaction?: (txid: string) => void;
+}
+
+// How our Transaction should look like
+interface Transaction {
+  txid: string;
+  status: {
+    confirmed: boolean;
+    blockTime?: number;
+    blockHeight?: number;
+  };
+  size: number;
+  fee: number;
+}
+
+// Helper function to format the relative time
+const formatRelativeTime = (timestamp?: number): string => {
+  if (!timestamp) return "Pending";
+  return formatDistanceToNow(new Date(timestamp * 1000), { addSuffix: true });
+};
+
+// Column definitions with sorting configuration
+const columns = [
+  { id: "txid", label: "Transaction ID", sortable: false },
+  { id: "blockTime", label: "Time", sortable: true },
+  { id: "size", label: "Size (vBytes)", sortable: true },
+  { id: "fee", label: "Fee (sats)", sortable: true },
+  { id: "status", label: "Status", sortable: false },
+  { id: "actions", label: "", sortable: false },
+];
+
+// FeeDisplay component to display fees display in both sats and BTC
+const FeeDisplay: React.FC<{ feeInSats?: number }> = ({ feeInSats }) => {
+  if (feeInSats === null || feeInSats === undefined) {
+    return <Typography color="textSecondary">N/A</Typography>;
+  }
+
+  const feeInBTC = satoshisToBitcoins(feeInSats.toString());
+
+  return (
+    <Tooltip
+      title={
+        <Box>
+          <Typography variant="caption">
+            {`${feeInSats.toLocaleString()} sats`}
+          </Typography>
+          <br />
+          <Typography variant="caption">{`${feeInBTC} BTC`}</Typography>
+        </Box>
+      }
+    >
+      <Box>
+        <Typography variant="body2" color="textPrimary">
+          {feeInSats.toLocaleString()} sats
+        </Typography>
+        <Typography variant="caption" color="textSecondary">
+          {feeInBTC} BTC
+        </Typography>
+      </Box>
+    </Tooltip>
+  );
+};
+
+export const TransactionTable: React.FC<TransactionTableProps> = ({
+  transactions,
+  onSort,
+  sortBy,
+  sortDirection,
+  network,
+  onClickTransaction,
+}) => {
+  const [snackbarOpen, setSnackbarOpen] = useState(false);
+
+  // Handler for sorting
+  const createSortHandler = (property: keyof Transaction) => () => {
+    onSort(property);
+  };
+
+  // Render table header with sort labels
+  const renderTableHeader = () => (
path: apps/coordinator/src/components/Wallet/TransactionsTable.tsx
user: login: bucko13
body: Confusing because I know this pattern is used elsewhere in the codebase, but that's largely legacy code and this is a pattern that should be avoided. You can just pull these out as their own standalone component `<TransactionTableHeader />` and use that int he component, or just put them directly in the main render section. 
created_at: 2025-03-17T22:06:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,273 @@
+import React, { useState, useEffect, useCallback } from "react";
+import { connect } from "react-redux";
+import {
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Tabs,
+  Tab,
+  Chip,
+} from "@mui/material";
+import { Refresh } from "@mui/icons-material";
+import { BlockchainClient } from "@caravan/clients";
+import { blockExplorerTransactionURL, Network } from "@caravan/bitcoin";
+import { updateBlockchainClient } from "../../actions/clientActions";
+import { TransactionTable } from "./TransactionsTable";
+
+// How are Transaction should look like
+interface Transaction {
path: apps/coordinator/src/components/Wallet/TransactionsTab.tsx
user: login: Legend101Zz
body: Yes, it's available as the `TransactionDetails` type , but it looks like we aren’t exporting all the necessary types from the client package.
```ts
export { bitcoindImportDescriptors } from "./wallet";
export { BlockchainClient, ClientType } from "./client";
export type { UTXO, Transaction, FeeRatePercentile } from "./types";
```
Would it be better to create a separate PR to fix this first by exporting the missing types, and then update the code to use the proper types provided by the package?
created_at: 2025-03-20T10:31:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,273 @@
+import React, { useState, useEffect, useCallback } from "react";
+import { connect } from "react-redux";
+import {
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Tabs,
+  Tab,
+  Chip,
+} from "@mui/material";
+import { Refresh } from "@mui/icons-material";
+import { BlockchainClient } from "@caravan/clients";
+import { blockExplorerTransactionURL, Network } from "@caravan/bitcoin";
+import { updateBlockchainClient } from "../../actions/clientActions";
+import { TransactionTable } from "./TransactionsTable";
+
+// How are Transaction should look like
+interface Transaction {
+  txid: string;
+  version: number;
+  locktime: number;
+  vin: any[]; // Input vector
+  vout: any[]; // Output vector
+  weight: number;
+  status: {
+    confirmed: boolean;
+    blockTime?: number;
+    blockHeight?: number;
+  };
+  size: number;
+  fee: number;
+}
+
+interface TransactionsTabProps {
+  network: Network;
+  getBlockchainClient: () => Promise<BlockchainClient>;
+  deposits: {
+    nodes: Record<string, any>;
+  };
+  change: {
+    nodes: Record<string, any>;
+  };
+  client: {
+    type: string;
+    blockchainClient?: BlockchainClient;
+  };
+}
+
+const TransactionsTab: React.FC<TransactionsTabProps> = ({
+  network,
+  getBlockchainClient,
+  deposits,
+  change,
+  client,
+}) => {
+  const [tabValue, setTabValue] = useState(0);
+  const [transactions, setTransactions] = useState<Transaction[]>([]);
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const [sortBy, setSortBy] = useState<string>("blockTime");
+  const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
+
+  // Fetch transactions
+  const fetchTransactions = useCallback(async () => {
+    setIsLoading(true);
+    setError(null);
+
+    try {
+      const blockchainClient = await getBlockchainClient();
+      if (!blockchainClient) {
+        throw new Error("No blockchain client available");
+      }
+
+      // Get unique transaction IDs from UTXOs
+      const txids = new Set<string>();
+      [
+        ...Object.values(deposits.nodes),
+        ...Object.values(change.nodes),
+      ].forEach((node) => {
+        node.utxos.forEach((utxo: { txid: string }) => {
+          txids.add(utxo.txid);
+        });
+      });
+
+      // Fetch transaction details in parallel
+      const txDetails = await Promise.all(
+        Array.from(txids).map(async (txid) => {
+          try {
+            const tx = await blockchainClient.getTransaction(txid);
path: apps/coordinator/src/components/Wallet/TransactionsTab.tsx
user: login: Legend101Zz
body: Understood ... 
`"fee" : n, (numeric) The amount of the fee in BTC. This is negative and only available for the
                                           'send' category of transactions.`
  So I tested out the `getWalletTransaction` by implementing it in the client's package :
  
  ```ts
  /**
 * Gets detailed information about a wallet transaction
 *
 * This function uses the "gettransaction" RPC call which includes fee information
 * for wallet transactions, unlike getrawtransaction which requires txindex=1 and
 * doesn't include fee data.
 *
 * @param options - Connection details and transaction ID
 * @returns Detailed transaction data with fee information
 */
export async function bitcoindGetWalletTransaction({
  url,
  auth,
  walletName,
  txid,
  includeWatchonly = true,
  verbose = true,
}: BaseBitcoindParams & {
  txid: string;
  includeWatchonly?: boolean;
  verbose?: boolean;
}): Promise<any> {
  try {
    const response = await callBitcoindWallet({
      baseUrl: url,
      walletName,
      auth,
      method: "gettransaction",
      params: [txid, includeWatchonly, verbose],
    });

    if (typeof response?.result === "undefined") {
      throw new BitcoindWalletClientError(
        `Error: invalid response from ${url} for transaction ${txid}`,
      );
    }

    return response.result;
  } catch (e) {
    const error = e as Error;
    // Handle specific error for transaction not found in wallet
    if ((e as any)?.response?.data?.error?.code === -5) {
      throw new BitcoindWalletClientError(
        `Transaction ${txid} not found in wallet. Either the transaction doesn't exist or it's not in this wallet.`,
      );
    }
    console.error("Error getting wallet transaction:", error.message);
    throw e;
  }
}
  ```
  
  
- Since the fee field is only available for sent transactions (and absent for received ones), we could use this to categorize transactions as sent or received. ( else we'll still see fees as 0 on the receiver end)
- However, for public clients, where we need to determine the net sent/received amount, we'll need an explicit calculation.
                                           
and as discussed for :
> It would be helpful to show a column with the bitcoin value of the UTXO
That we'll need a way to figure out if the net vout's are to one of our addresses or not ...

So should we keep this change for that PR or should I push a new `client feat PR`  changing the `BlockchainClient.getTransaction` to use the `bitcoindGetWalletTransaction` instead of `bitcoindRawTxData`                                
                                      
created_at: 2025-03-20T14:01:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,273 @@
+import React, { useState, useEffect, useCallback } from "react";
+import { connect } from "react-redux";
+import {
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Tabs,
+  Tab,
+  Chip,
+} from "@mui/material";
+import { Refresh } from "@mui/icons-material";
+import { BlockchainClient } from "@caravan/clients";
+import { blockExplorerTransactionURL, Network } from "@caravan/bitcoin";
+import { updateBlockchainClient } from "../../actions/clientActions";
+import { TransactionTable } from "./TransactionsTable";
+
+// How are Transaction should look like
+interface Transaction {
+  txid: string;
+  version: number;
+  locktime: number;
+  vin: any[]; // Input vector
+  vout: any[]; // Output vector
+  weight: number;
+  status: {
+    confirmed: boolean;
+    blockTime?: number;
+    blockHeight?: number;
+  };
+  size: number;
+  fee: number;
+}
+
+interface TransactionsTabProps {
+  network: Network;
+  getBlockchainClient: () => Promise<BlockchainClient>;
+  deposits: {
+    nodes: Record<string, any>;
+  };
+  change: {
+    nodes: Record<string, any>;
+  };
+  client: {
+    type: string;
+    blockchainClient?: BlockchainClient;
+  };
+}
+
+const TransactionsTab: React.FC<TransactionsTabProps> = ({
+  network,
+  getBlockchainClient,
+  deposits,
+  change,
+  client,
+}) => {
+  const [tabValue, setTabValue] = useState(0);
+  const [transactions, setTransactions] = useState<Transaction[]>([]);
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const [sortBy, setSortBy] = useState<string>("blockTime");
+  const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
+
+  // Fetch transactions
+  const fetchTransactions = useCallback(async () => {
+    setIsLoading(true);
+    setError(null);
+
+    try {
+      const blockchainClient = await getBlockchainClient();
+      if (!blockchainClient) {
+        throw new Error("No blockchain client available");
+      }
+
+      // Get unique transaction IDs from UTXOs
+      const txids = new Set<string>();
+      [
+        ...Object.values(deposits.nodes),
+        ...Object.values(change.nodes),
+      ].forEach((node) => {
+        node.utxos.forEach((utxo: { txid: string }) => {
+          txids.add(utxo.txid);
+        });
+      });
+
+      // Fetch transaction details in parallel
+      const txDetails = await Promise.all(
+        Array.from(txids).map(async (txid) => {
+          try {
+            const tx = await blockchainClient.getTransaction(txid);
+            return tx;
+          } catch (err) {
+            console.error(`Error fetching tx ${txid}:`, err);
+            return null;
+          }
+        }),
+      );
+      setTransactions(txDetails.filter((tx): tx is Transaction => tx !== null));
+      setError(null);
+    } catch (err) {
+      setError((err as Error).message);
+    } finally {
+      setIsLoading(false);
+    }
+  }, [getBlockchainClient, deposits.nodes, change.nodes]);
+
+  // Initial loading
+  useEffect(() => {
+    fetchTransactions();
+  }, [fetchTransactions]);
+
+  // Handle sorting
+  const handleSort = (property: string) => {
+    const isAsc = sortBy === property && sortDirection === "asc";
+    setSortDirection(isAsc ? "desc" : "asc");
+    setSortBy(property);
+  };
+
+  const handleTransactionClick = useCallback(
+    (txid: string) => {
+      // Check if user is using a private node
+      const isPrivateNode = client.type === "private";
+
+      if (isPrivateNode) {
+        // If using private node, warn about privacy implications
+        const confirmed = window.confirm(
+          "Opening in a block explorer may expose your wallet activity to third parties. Continue?",
+        );
+        if (!confirmed) return;
+      }
+      // Determine which block explorer to use based on the blockchain client type
+      let explorerUrl;
+
+      if (client.blockchainClient && client.blockchainClient.type) {
+        const clientType = client.blockchainClient.type;
+
+        switch (clientType) {
+          case "mempool":
+            // Use mempool.space explorer
+            explorerUrl = `https://${network === "mainnet" ? "" : "testnet."}mempool.space/tx/${txid}`;
+            break;
+
+          case "blockstream":
+            // Use blockstream.info explorer
+            explorerUrl = `https://blockstream.info/${network === "mainnet" ? "" : "testnet/"}tx/${txid}`;
+            break;
+
+          default:
+            // Fall back to the default explorer
+            explorerUrl = blockExplorerTransactionURL(txid, network);
+        }
+      } else {
+        // If no blockchain client specified, use default
+        explorerUrl = blockExplorerTransactionURL(txid, network);
+      }
+
+      window.open(explorerUrl, "_blank");
+      window.open(explorerUrl, "_blank");
path: apps/coordinator/src/components/Wallet/TransactionsTab.tsx
user: login: Legend101Zz
body: Ahh ... sorry that seems to be a typo 

created_at: 2025-03-20T14:37:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,273 @@
+import React, { useState, useEffect, useCallback } from "react";
+import { connect } from "react-redux";
+import {
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Tabs,
+  Tab,
+  Chip,
+} from "@mui/material";
+import { Refresh } from "@mui/icons-material";
+import { BlockchainClient } from "@caravan/clients";
+import { blockExplorerTransactionURL, Network } from "@caravan/bitcoin";
+import { updateBlockchainClient } from "../../actions/clientActions";
+import { TransactionTable } from "./TransactionsTable";
+
+// How are Transaction should look like
+interface Transaction {
+  txid: string;
+  version: number;
+  locktime: number;
+  vin: any[]; // Input vector
+  vout: any[]; // Output vector
+  weight: number;
+  status: {
+    confirmed: boolean;
+    blockTime?: number;
+    blockHeight?: number;
+  };
+  size: number;
+  fee: number;
+}
+
+interface TransactionsTabProps {
+  network: Network;
+  getBlockchainClient: () => Promise<BlockchainClient>;
+  deposits: {
+    nodes: Record<string, any>;
+  };
+  change: {
+    nodes: Record<string, any>;
+  };
+  client: {
+    type: string;
+    blockchainClient?: BlockchainClient;
+  };
+}
+
+const TransactionsTab: React.FC<TransactionsTabProps> = ({
+  network,
+  getBlockchainClient,
+  deposits,
+  change,
+  client,
+}) => {
+  const [tabValue, setTabValue] = useState(0);
+  const [transactions, setTransactions] = useState<Transaction[]>([]);
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const [sortBy, setSortBy] = useState<string>("blockTime");
+  const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
+
+  // Fetch transactions
+  const fetchTransactions = useCallback(async () => {
+    setIsLoading(true);
+    setError(null);
+
+    try {
+      const blockchainClient = await getBlockchainClient();
+      if (!blockchainClient) {
+        throw new Error("No blockchain client available");
+      }
+
+      // Get unique transaction IDs from UTXOs
+      const txids = new Set<string>();
+      [
+        ...Object.values(deposits.nodes),
+        ...Object.values(change.nodes),
+      ].forEach((node) => {
+        node.utxos.forEach((utxo: { txid: string }) => {
+          txids.add(utxo.txid);
+        });
+      });
+
+      // Fetch transaction details in parallel
+      const txDetails = await Promise.all(
+        Array.from(txids).map(async (txid) => {
+          try {
+            const tx = await blockchainClient.getTransaction(txid);
+            return tx;
+          } catch (err) {
+            console.error(`Error fetching tx ${txid}:`, err);
+            return null;
+          }
+        }),
+      );
+      setTransactions(txDetails.filter((tx): tx is Transaction => tx !== null));
+      setError(null);
+    } catch (err) {
+      setError((err as Error).message);
+    } finally {
+      setIsLoading(false);
+    }
+  }, [getBlockchainClient, deposits.nodes, change.nodes]);
+
+  // Initial loading
+  useEffect(() => {
+    fetchTransactions();
+  }, [fetchTransactions]);
+
+  // Handle sorting
+  const handleSort = (property: string) => {
+    const isAsc = sortBy === property && sortDirection === "asc";
+    setSortDirection(isAsc ? "desc" : "asc");
+    setSortBy(property);
+  };
+
+  const handleTransactionClick = useCallback(
+    (txid: string) => {
+      // Check if user is using a private node
+      const isPrivateNode = client.type === "private";
+
+      if (isPrivateNode) {
+        // If using private node, warn about privacy implications
+        const confirmed = window.confirm(
+          "Opening in a block explorer may expose your wallet activity to third parties. Continue?",
+        );
+        if (!confirmed) return;
+      }
+      // Determine which block explorer to use based on the blockchain client type
+      let explorerUrl;
+
+      if (client.blockchainClient && client.blockchainClient.type) {
+        const clientType = client.blockchainClient.type;
+
+        switch (clientType) {
+          case "mempool":
+            // Use mempool.space explorer
+            explorerUrl = `https://${network === "mainnet" ? "" : "testnet."}mempool.space/tx/${txid}`;
+            break;
+
+          case "blockstream":
+            // Use blockstream.info explorer
+            explorerUrl = `https://blockstream.info/${network === "mainnet" ? "" : "testnet/"}tx/${txid}`;
+            break;
+
+          default:
+            // Fall back to the default explorer
+            explorerUrl = blockExplorerTransactionURL(txid, network);
path: apps/coordinator/src/components/Wallet/TransactionsTab.tsx
user: login: Legend101Zz
body: Ahh got it .. so maybe a `PR` to add this feature into the `bitcoin package` ?
created_at: 2025-03-20T19:56:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,273 @@
+import React, { useState, useEffect, useCallback } from "react";
+import { connect } from "react-redux";
+import {
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Tabs,
+  Tab,
+  Chip,
+} from "@mui/material";
+import { Refresh } from "@mui/icons-material";
+import { BlockchainClient } from "@caravan/clients";
+import { blockExplorerTransactionURL, Network } from "@caravan/bitcoin";
+import { updateBlockchainClient } from "../../actions/clientActions";
+import { TransactionTable } from "./TransactionsTable";
+
+// How are Transaction should look like
+interface Transaction {
+  txid: string;
+  version: number;
+  locktime: number;
+  vin: any[]; // Input vector
+  vout: any[]; // Output vector
+  weight: number;
+  status: {
+    confirmed: boolean;
+    blockTime?: number;
+    blockHeight?: number;
+  };
+  size: number;
+  fee: number;
+}
+
+interface TransactionsTabProps {
+  network: Network;
+  getBlockchainClient: () => Promise<BlockchainClient>;
+  deposits: {
+    nodes: Record<string, any>;
+  };
+  change: {
+    nodes: Record<string, any>;
+  };
+  client: {
+    type: string;
+    blockchainClient?: BlockchainClient;
+  };
+}
+
+const TransactionsTab: React.FC<TransactionsTabProps> = ({
+  network,
+  getBlockchainClient,
+  deposits,
+  change,
+  client,
+}) => {
+  const [tabValue, setTabValue] = useState(0);
+  const [transactions, setTransactions] = useState<Transaction[]>([]);
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const [sortBy, setSortBy] = useState<string>("blockTime");
+  const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
+
+  // Fetch transactions
+  const fetchTransactions = useCallback(async () => {
+    setIsLoading(true);
+    setError(null);
+
+    try {
+      const blockchainClient = await getBlockchainClient();
path: apps/coordinator/src/components/Wallet/TransactionsTab.tsx
user: login: Legend101Zz
body: Did not explored it till now ... but now have updated the component to use that [hook](https://github.com/caravan-bitcoin/caravan/pull/168/commits/15969b3e955eb785c75b76a492c74adebb102636) 
created_at: 2025-03-20T20:01:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,273 @@
+import React, { useState, useEffect, useCallback } from "react";
+import { connect } from "react-redux";
+import {
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Tabs,
+  Tab,
+  Chip,
+} from "@mui/material";
+import { Refresh } from "@mui/icons-material";
+import { BlockchainClient } from "@caravan/clients";
+import { blockExplorerTransactionURL, Network } from "@caravan/bitcoin";
+import { updateBlockchainClient } from "../../actions/clientActions";
+import { TransactionTable } from "./TransactionsTable";
+
+// How are Transaction should look like
+interface Transaction {
+  txid: string;
+  version: number;
+  locktime: number;
+  vin: any[]; // Input vector
+  vout: any[]; // Output vector
+  weight: number;
+  status: {
+    confirmed: boolean;
+    blockTime?: number;
+    blockHeight?: number;
+  };
+  size: number;
+  fee: number;
+}
+
+interface TransactionsTabProps {
+  network: Network;
+  getBlockchainClient: () => Promise<BlockchainClient>;
+  deposits: {
+    nodes: Record<string, any>;
+  };
+  change: {
+    nodes: Record<string, any>;
+  };
+  client: {
+    type: string;
+    blockchainClient?: BlockchainClient;
+  };
+}
+
+const TransactionsTab: React.FC<TransactionsTabProps> = ({
+  network,
+  getBlockchainClient,
+  deposits,
+  change,
+  client,
+}) => {
+  const [tabValue, setTabValue] = useState(0);
+  const [transactions, setTransactions] = useState<Transaction[]>([]);
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const [sortBy, setSortBy] = useState<string>("blockTime");
+  const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
+
+  // Fetch transactions
+  const fetchTransactions = useCallback(async () => {
+    setIsLoading(true);
+    setError(null);
+
+    try {
+      const blockchainClient = await getBlockchainClient();
+      if (!blockchainClient) {
+        throw new Error("No blockchain client available");
+      }
+
+      // Get unique transaction IDs from UTXOs
+      const txids = new Set<string>();
+      [
+        ...Object.values(deposits.nodes),
+        ...Object.values(change.nodes),
+      ].forEach((node) => {
+        node.utxos.forEach((utxo: { txid: string }) => {
+          txids.add(utxo.txid);
+        });
+      });
+
+      // Fetch transaction details in parallel
+      const txDetails = await Promise.all(
+        Array.from(txids).map(async (txid) => {
+          try {
+            const tx = await blockchainClient.getTransaction(txid);
+            return tx;
+          } catch (err) {
+            console.error(`Error fetching tx ${txid}:`, err);
+            return null;
+          }
+        }),
+      );
+      setTransactions(txDetails.filter((tx): tx is Transaction => tx !== null));
+      setError(null);
+    } catch (err) {
+      setError((err as Error).message);
+    } finally {
+      setIsLoading(false);
+    }
+  }, [getBlockchainClient, deposits.nodes, change.nodes]);
+
+  // Initial loading
+  useEffect(() => {
+    fetchTransactions();
+  }, [fetchTransactions]);
+
+  // Handle sorting
+  const handleSort = (property: string) => {
+    const isAsc = sortBy === property && sortDirection === "asc";
+    setSortDirection(isAsc ? "desc" : "asc");
+    setSortBy(property);
+  };
+
+  const handleTransactionClick = useCallback(
+    (txid: string) => {
+      // Check if user is using a private node
+      const isPrivateNode = client.type === "private";
+
+      if (isPrivateNode) {
+        // If using private node, warn about privacy implications
+        const confirmed = window.confirm(
+          "Opening in a block explorer may expose your wallet activity to third parties. Continue?",
+        );
+        if (!confirmed) return;
+      }
+      // Determine which block explorer to use based on the blockchain client type
+      let explorerUrl;
+
+      if (client.blockchainClient && client.blockchainClient.type) {
+        const clientType = client.blockchainClient.type;
+
+        switch (clientType) {
+          case "mempool":
+            // Use mempool.space explorer
+            explorerUrl = `https://${network === "mainnet" ? "" : "testnet."}mempool.space/tx/${txid}`;
+            break;
+
+          case "blockstream":
+            // Use blockstream.info explorer
+            explorerUrl = `https://blockstream.info/${network === "mainnet" ? "" : "testnet/"}tx/${txid}`;
+            break;
+
+          default:
+            // Fall back to the default explorer
+            explorerUrl = blockExplorerTransactionURL(txid, network);
+        }
+      } else {
+        // If no blockchain client specified, use default
+        explorerUrl = blockExplorerTransactionURL(txid, network);
+      }
+
+      window.open(explorerUrl, "_blank");
+      window.open(explorerUrl, "_blank");
+    },
+    [network, client],
+  );
+
+  // Filter and sort transactions
+  const getSortedTransactions = useCallback(
+    (txs: Transaction[]) => {
+      return [...txs].sort((a, b) => {
+        // Handle different sorting properties
+        let comparison = 0;
+        switch (sortBy) {
+          case "blockTime":
+            comparison = (a.status.blockTime || 0) - (b.status.blockTime || 0);
+            break;
+          case "size":
+            comparison = a.size - b.size;
+            break;
+          case "fee":
+            comparison = (a.fee || 0) - (b.fee || 0);
+            break;
+          default:
+            return 0;
+        }
+        return sortDirection === "desc" ? -comparison : comparison;
+      });
+    },
+    [sortBy, sortDirection],
+  );
+
+  // Split and sort transactions
+  const pendingTxs = getSortedTransactions(
+    transactions.filter((tx) => !tx.status.confirmed),
+  );
+  const confirmedTxs = getSortedTransactions(
+    transactions.filter((tx) => tx.status.confirmed),
+  );
+
+  return (
+    <div>
+      <Box
+        display="flex"
+        justifyContent="space-between"
+        alignItems="center"
+        mb={2}
+      >
+        <Typography variant="h6">Transactions</Typography>
+        <Tooltip title="Refresh transactions">
+          <IconButton onClick={fetchTransactions} disabled={isLoading}>
+            <Refresh />
+          </IconButton>
+        </Tooltip>
+      </Box>
+
+      {error && (
+        <Typography color="error" gutterBottom>
+          Error: {error}
+        </Typography>
+      )}
+
+      <Tabs value={tabValue} onChange={(_, value) => setTabValue(value)}>
+        <Tab
+          label={
+            <Box display="flex" alignItems="center" gap={1}>
+              <span>Pending</span>
+              <Chip
+                label={pendingTxs.length}
+                size="small"
+                color={pendingTxs.length > 0 ? "primary" : "default"}
+              />
+            </Box>
+          }
+        />
+        <Tab
+          label={
+            <Box display="flex" alignItems="center" gap={1}>
+              <span>Confirmed</span>
+              <Chip label={confirmedTxs.length} size="small" />
+            </Box>
+          }
+        />
+      </Tabs>
+
+      <Box mt={2}>
+        {isLoading ? (
+          <Box display="flex" justifyContent="center" p={3}>
+            <CircularProgress />
+          </Box>
+        ) : (
+          <TransactionTable
+            transactions={tabValue === 0 ? pendingTxs : confirmedTxs}
+            onSort={handleSort}
+            sortBy={sortBy}
+            sortDirection={sortDirection}
+            network={network}
+            onClickTransaction={handleTransactionClick}
+          />
+        )}
+      </Box>
+    </div>
+  );
+};
+
+const mapStateToProps = (state: any) => ({
+  network: state.settings.network,
+  deposits: state.wallet.deposits,
+  change: state.wallet.change,
+  client: state.client,
+});
+
+const mapDispatchToProps = {
+  getBlockchainClient: updateBlockchainClient,
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(TransactionsTab);
path: apps/coordinator/src/components/Wallet/TransactionsTab.tsx
user: login: Legend101Zz
body: Thanks for pointing that out ... changes [done](https://github.com/caravan-bitcoin/caravan/pull/168/commits/6a0abda7e8da9610f05eb969ed144a7a689b1fbd)
created_at: 2025-03-20T20:13:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,273 @@
+import React, { useState, useEffect, useCallback } from "react";
+import { connect } from "react-redux";
+import {
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Tabs,
+  Tab,
+  Chip,
+} from "@mui/material";
+import { Refresh } from "@mui/icons-material";
+import { BlockchainClient } from "@caravan/clients";
+import { blockExplorerTransactionURL, Network } from "@caravan/bitcoin";
+import { updateBlockchainClient } from "../../actions/clientActions";
+import { TransactionTable } from "./TransactionsTable";
+
+// How are Transaction should look like
+interface Transaction {
+  txid: string;
+  version: number;
+  locktime: number;
+  vin: any[]; // Input vector
+  vout: any[]; // Output vector
+  weight: number;
+  status: {
+    confirmed: boolean;
+    blockTime?: number;
+    blockHeight?: number;
+  };
+  size: number;
+  fee: number;
+}
+
+interface TransactionsTabProps {
+  network: Network;
+  getBlockchainClient: () => Promise<BlockchainClient>;
+  deposits: {
+    nodes: Record<string, any>;
+  };
+  change: {
+    nodes: Record<string, any>;
+  };
+  client: {
+    type: string;
+    blockchainClient?: BlockchainClient;
+  };
+}
+
+const TransactionsTab: React.FC<TransactionsTabProps> = ({
+  network,
+  getBlockchainClient,
+  deposits,
+  change,
+  client,
+}) => {
+  const [tabValue, setTabValue] = useState(0);
+  const [transactions, setTransactions] = useState<Transaction[]>([]);
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const [sortBy, setSortBy] = useState<string>("blockTime");
+  const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
+
+  // Fetch transactions
+  const fetchTransactions = useCallback(async () => {
+    setIsLoading(true);
+    setError(null);
+
+    try {
+      const blockchainClient = await getBlockchainClient();
+      if (!blockchainClient) {
+        throw new Error("No blockchain client available");
+      }
+
+      // Get unique transaction IDs from UTXOs
+      const txids = new Set<string>();
+      [
+        ...Object.values(deposits.nodes),
+        ...Object.values(change.nodes),
+      ].forEach((node) => {
+        node.utxos.forEach((utxo: { txid: string }) => {
+          txids.add(utxo.txid);
+        });
+      });
+
+      // Fetch transaction details in parallel
+      const txDetails = await Promise.all(
+        Array.from(txids).map(async (txid) => {
+          try {
+            const tx = await blockchainClient.getTransaction(txid);
+            return tx;
+          } catch (err) {
+            console.error(`Error fetching tx ${txid}:`, err);
+            return null;
+          }
+        }),
+      );
+      setTransactions(txDetails.filter((tx): tx is Transaction => tx !== null));
+      setError(null);
+    } catch (err) {
+      setError((err as Error).message);
+    } finally {
+      setIsLoading(false);
+    }
+  }, [getBlockchainClient, deposits.nodes, change.nodes]);
+
+  // Initial loading
+  useEffect(() => {
+    fetchTransactions();
+  }, [fetchTransactions]);
+
+  // Handle sorting
+  const handleSort = (property: string) => {
+    const isAsc = sortBy === property && sortDirection === "asc";
+    setSortDirection(isAsc ? "desc" : "asc");
+    setSortBy(property);
+  };
+
+  const handleTransactionClick = useCallback(
+    (txid: string) => {
+      // Check if user is using a private node
+      const isPrivateNode = client.type === "private";
+
+      if (isPrivateNode) {
+        // If using private node, warn about privacy implications
+        const confirmed = window.confirm(
+          "Opening in a block explorer may expose your wallet activity to third parties. Continue?",
+        );
+        if (!confirmed) return;
+      }
+      // Determine which block explorer to use based on the blockchain client type
+      let explorerUrl;
+
+      if (client.blockchainClient && client.blockchainClient.type) {
+        const clientType = client.blockchainClient.type;
+
+        switch (clientType) {
+          case "mempool":
+            // Use mempool.space explorer
+            explorerUrl = `https://${network === "mainnet" ? "" : "testnet."}mempool.space/tx/${txid}`;
+            break;
+
+          case "blockstream":
+            // Use blockstream.info explorer
+            explorerUrl = `https://blockstream.info/${network === "mainnet" ? "" : "testnet/"}tx/${txid}`;
+            break;
+
+          default:
+            // Fall back to the default explorer
+            explorerUrl = blockExplorerTransactionURL(txid, network);
path: apps/coordinator/src/components/Wallet/TransactionsTab.tsx
user: login: Legend101Zz
body: also how does this [look](https://github.com/caravan-bitcoin/caravan/pull/168/commits/f795f11efbf325f2664769f08f41242b81692eb2)  ?
created_at: 2025-03-20T20:14:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,218 @@
+import React, { useState } from "react";
+import {
+  Table,
+  TableBody,
+  TableCell,
+  TableContainer,
+  TableHead,
+  TableRow,
+  TableSortLabel,
+  Paper,
+  Chip,
+  IconButton,
+  Box,
+  Tooltip,
+  Typography,
+  Alert,
+  Snackbar,
+} from "@mui/material";
+import { satoshisToBitcoins } from "@caravan/bitcoin";
+import { formatDistanceToNow } from "date-fns";
+import { OpenInNew } from "@mui/icons-material";
+
+interface TransactionTableProps {
+  transactions: Transaction[];
+  onSort: (property: keyof Transaction) => void;
+  sortBy: string;
+  sortDirection: "asc" | "desc";
+  network?: string;
+  onClickTransaction?: (txid: string) => void;
+}
+
+// How our Transaction should look like
+interface Transaction {
+  txid: string;
+  status: {
+    confirmed: boolean;
+    blockTime?: number;
+    blockHeight?: number;
+  };
+  size: number;
+  fee: number;
+}
+
+// Helper function to format the relative time
+const formatRelativeTime = (timestamp?: number): string => {
+  if (!timestamp) return "Pending";
+  return formatDistanceToNow(new Date(timestamp * 1000), { addSuffix: true });
+};
+
+// Column definitions with sorting configuration
+const columns = [
+  { id: "txid", label: "Transaction ID", sortable: false },
+  { id: "blockTime", label: "Time", sortable: true },
+  { id: "size", label: "Size (vBytes)", sortable: true },
+  { id: "fee", label: "Fee (sats)", sortable: true },
+  { id: "status", label: "Status", sortable: false },
+  { id: "actions", label: "", sortable: false },
+];
+
+// FeeDisplay component to display fees display in both sats and BTC
+const FeeDisplay: React.FC<{ feeInSats?: number }> = ({ feeInSats }) => {
+  if (feeInSats === null || feeInSats === undefined) {
+    return <Typography color="textSecondary">N/A</Typography>;
+  }
+
+  const feeInBTC = satoshisToBitcoins(feeInSats.toString());
+
+  return (
+    <Tooltip
+      title={
+        <Box>
+          <Typography variant="caption">
+            {`${feeInSats.toLocaleString()} sats`}
+          </Typography>
+          <br />
+          <Typography variant="caption">{`${feeInBTC} BTC`}</Typography>
+        </Box>
+      }
+    >
+      <Box>
+        <Typography variant="body2" color="textPrimary">
+          {feeInSats.toLocaleString()} sats
+        </Typography>
+        <Typography variant="caption" color="textSecondary">
+          {feeInBTC} BTC
+        </Typography>
+      </Box>
+    </Tooltip>
+  );
+};
+
+export const TransactionTable: React.FC<TransactionTableProps> = ({
+  transactions,
+  onSort,
+  sortBy,
+  sortDirection,
+  network,
+  onClickTransaction,
+}) => {
+  const [snackbarOpen, setSnackbarOpen] = useState(false);
+
+  // Handler for sorting
+  const createSortHandler = (property: keyof Transaction) => () => {
+    onSort(property);
+  };
+
+  // Render table header with sort labels
+  const renderTableHeader = () => (
path: apps/coordinator/src/components/Wallet/TransactionsTable.tsx
user: login: Legend101Zz
body: Sorry for the miss .. I seem have not refactored these into new modern React styled components ...
Changes done [now](https://github.com/caravan-bitcoin/caravan/pull/168/commits/a057e88304d2b2cd8efb9d42a78cf9a79f1e98fd)
created_at: 2025-03-20T20:24:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,273 @@
+import React, { useState, useEffect, useCallback } from "react";
+import { connect } from "react-redux";
+import {
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Tabs,
+  Tab,
+  Chip,
+} from "@mui/material";
+import { Refresh } from "@mui/icons-material";
+import { BlockchainClient } from "@caravan/clients";
+import { blockExplorerTransactionURL, Network } from "@caravan/bitcoin";
+import { updateBlockchainClient } from "../../actions/clientActions";
+import { TransactionTable } from "./TransactionsTable";
+
+// How are Transaction should look like
+interface Transaction {
+  txid: string;
+  version: number;
+  locktime: number;
+  vin: any[]; // Input vector
+  vout: any[]; // Output vector
+  weight: number;
+  status: {
+    confirmed: boolean;
+    blockTime?: number;
+    blockHeight?: number;
+  };
+  size: number;
+  fee: number;
+}
+
+interface TransactionsTabProps {
+  network: Network;
+  getBlockchainClient: () => Promise<BlockchainClient>;
+  deposits: {
+    nodes: Record<string, any>;
+  };
+  change: {
+    nodes: Record<string, any>;
+  };
+  client: {
+    type: string;
+    blockchainClient?: BlockchainClient;
+  };
+}
+
+const TransactionsTab: React.FC<TransactionsTabProps> = ({
+  network,
+  getBlockchainClient,
+  deposits,
+  change,
+  client,
+}) => {
+  const [tabValue, setTabValue] = useState(0);
+  const [transactions, setTransactions] = useState<Transaction[]>([]);
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const [sortBy, setSortBy] = useState<string>("blockTime");
+  const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
+
+  // Fetch transactions
+  const fetchTransactions = useCallback(async () => {
path: apps/coordinator/src/components/Wallet/TransactionsTab.tsx
user: login: Legend101Zz
body: yes that sounds great ... It would increase the maintainability and testing of the component ...

I have made the changes [here](https://github.com/caravan-bitcoin/caravan/pull/168/commits/5b4a3c400b3d33966d707cf16c9d33da26ef0f1e)
created_at: 2025-03-20T20:49:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,273 @@
+import React, { useState, useEffect, useCallback } from "react";
+import { connect } from "react-redux";
+import {
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Tabs,
+  Tab,
+  Chip,
+} from "@mui/material";
+import { Refresh } from "@mui/icons-material";
+import { BlockchainClient } from "@caravan/clients";
+import { blockExplorerTransactionURL, Network } from "@caravan/bitcoin";
+import { updateBlockchainClient } from "../../actions/clientActions";
+import { TransactionTable } from "./TransactionsTable";
+
+// How are Transaction should look like
+interface Transaction {
+  txid: string;
+  version: number;
+  locktime: number;
+  vin: any[]; // Input vector
+  vout: any[]; // Output vector
+  weight: number;
+  status: {
+    confirmed: boolean;
+    blockTime?: number;
+    blockHeight?: number;
+  };
+  size: number;
+  fee: number;
+}
+
+interface TransactionsTabProps {
+  network: Network;
+  getBlockchainClient: () => Promise<BlockchainClient>;
+  deposits: {
+    nodes: Record<string, any>;
+  };
+  change: {
+    nodes: Record<string, any>;
+  };
+  client: {
+    type: string;
+    blockchainClient?: BlockchainClient;
+  };
+}
+
+const TransactionsTab: React.FC<TransactionsTabProps> = ({
+  network,
+  getBlockchainClient,
+  deposits,
+  change,
+  client,
+}) => {
+  const [tabValue, setTabValue] = useState(0);
+  const [transactions, setTransactions] = useState<Transaction[]>([]);
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const [sortBy, setSortBy] = useState<string>("blockTime");
+  const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
+
+  // Fetch transactions
+  const fetchTransactions = useCallback(async () => {
+    setIsLoading(true);
+    setError(null);
+
+    try {
+      const blockchainClient = await getBlockchainClient();
+      if (!blockchainClient) {
+        throw new Error("No blockchain client available");
+      }
+
+      // Get unique transaction IDs from UTXOs
+      const txids = new Set<string>();
+      [
+        ...Object.values(deposits.nodes),
+        ...Object.values(change.nodes),
+      ].forEach((node) => {
+        node.utxos.forEach((utxo: { txid: string }) => {
+          txids.add(utxo.txid);
+        });
+      });
+
+      // Fetch transaction details in parallel
+      const txDetails = await Promise.all(
+        Array.from(txids).map(async (txid) => {
+          try {
+            const tx = await blockchainClient.getTransaction(txid);
+            return tx;
+          } catch (err) {
+            console.error(`Error fetching tx ${txid}:`, err);
+            return null;
+          }
+        }),
+      );
+      setTransactions(txDetails.filter((tx): tx is Transaction => tx !== null));
+      setError(null);
+    } catch (err) {
+      setError((err as Error).message);
+    } finally {
+      setIsLoading(false);
+    }
+  }, [getBlockchainClient, deposits.nodes, change.nodes]);
+
+  // Initial loading
+  useEffect(() => {
+    fetchTransactions();
+  }, [fetchTransactions]);
+
+  // Handle sorting
+  const handleSort = (property: string) => {
+    const isAsc = sortBy === property && sortDirection === "asc";
+    setSortDirection(isAsc ? "desc" : "asc");
+    setSortBy(property);
+  };
+
+  const handleTransactionClick = useCallback(
path: apps/coordinator/src/components/Wallet/TransactionsTab.tsx
user: login: Legend101Zz
body: 
I have made the changes [here](https://github.com/caravan-bitcoin/caravan/pull/168/commits/5b4a3c400b3d33966d707cf16c9d33da26ef0f1e)
created_at: 2025-03-20T20:49:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,273 @@
+import React, { useState, useEffect, useCallback } from "react";
+import { connect } from "react-redux";
+import {
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Tabs,
+  Tab,
+  Chip,
+} from "@mui/material";
+import { Refresh } from "@mui/icons-material";
+import { BlockchainClient } from "@caravan/clients";
+import { blockExplorerTransactionURL, Network } from "@caravan/bitcoin";
+import { updateBlockchainClient } from "../../actions/clientActions";
+import { TransactionTable } from "./TransactionsTable";
+
+// How are Transaction should look like
+interface Transaction {
+  txid: string;
+  version: number;
+  locktime: number;
+  vin: any[]; // Input vector
+  vout: any[]; // Output vector
+  weight: number;
+  status: {
+    confirmed: boolean;
+    blockTime?: number;
+    blockHeight?: number;
+  };
+  size: number;
+  fee: number;
+}
+
+interface TransactionsTabProps {
+  network: Network;
+  getBlockchainClient: () => Promise<BlockchainClient>;
+  deposits: {
+    nodes: Record<string, any>;
+  };
+  change: {
+    nodes: Record<string, any>;
+  };
+  client: {
+    type: string;
+    blockchainClient?: BlockchainClient;
+  };
+}
+
+const TransactionsTab: React.FC<TransactionsTabProps> = ({
+  network,
+  getBlockchainClient,
+  deposits,
+  change,
+  client,
+}) => {
+  const [tabValue, setTabValue] = useState(0);
+  const [transactions, setTransactions] = useState<Transaction[]>([]);
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const [sortBy, setSortBy] = useState<string>("blockTime");
+  const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
+
+  // Fetch transactions
+  const fetchTransactions = useCallback(async () => {
+    setIsLoading(true);
+    setError(null);
+
+    try {
+      const blockchainClient = await getBlockchainClient();
+      if (!blockchainClient) {
+        throw new Error("No blockchain client available");
+      }
+
+      // Get unique transaction IDs from UTXOs
+      const txids = new Set<string>();
+      [
+        ...Object.values(deposits.nodes),
+        ...Object.values(change.nodes),
+      ].forEach((node) => {
+        node.utxos.forEach((utxo: { txid: string }) => {
+          txids.add(utxo.txid);
+        });
+      });
+
+      // Fetch transaction details in parallel
+      const txDetails = await Promise.all(
+        Array.from(txids).map(async (txid) => {
+          try {
+            const tx = await blockchainClient.getTransaction(txid);
path: apps/coordinator/src/components/Wallet/TransactionsTab.tsx
user: login: bucko13
body: > So should we keep this change for that PR or should I push a new client feat PR changing the BlockchainClient.getTransaction to use the bitcoindGetWalletTransaction instead of bitcoindRawTxData

I think we could keep the existing method and just add a new method to the client so both are supported. That way it's also not a breaking change to the api. but a separate PR would be great and should be a quick approval and merge as well. 

Good catch about fees only being available for sent. I think that's a great idea that we can use that for identifying internal wallet transactions. I think for public clients like mempool, they'll always include the fee since they have much greater ability to do indexing, so it might be tricky to rely just on that. for dealing with incoming txs we want to bump (can only do CPFP for those though) we'll have to calculate the fees, or to start just leave it blank for private clients but if someone is waiting a long time they can go to a public client like mempool or just bump with cpfp if they're getting impatient.
created_at: 2025-03-20T21:49:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,273 @@
+import React, { useState, useEffect, useCallback } from "react";
+import { connect } from "react-redux";
+import {
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Tabs,
+  Tab,
+  Chip,
+} from "@mui/material";
+import { Refresh } from "@mui/icons-material";
+import { BlockchainClient } from "@caravan/clients";
+import { blockExplorerTransactionURL, Network } from "@caravan/bitcoin";
+import { updateBlockchainClient } from "../../actions/clientActions";
+import { TransactionTable } from "./TransactionsTable";
+
+// How are Transaction should look like
+interface Transaction {
path: apps/coordinator/src/components/Wallet/TransactionsTab.tsx
user: login: bucko13
body: > Would it be better to create a separate PR to fix this first by exporting the missing types, and then update the code to use the proper types provided by the package?

Yeah, that'd be great!
created_at: 2025-03-20T21:49:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,253 @@
+import { useState, useEffect, useCallback } from "react";
+import { useSelector } from "react-redux";
+import { blockExplorerTransactionURL } from "@caravan/bitcoin";
+import { Transaction, SortDirection, SortBy } from "./types";
+import { useGetClient } from "../../../hooks/client";
+
+/**
+ * Custom hook to manage transaction fetching and state
+ */
+export const useTransactions = () => {
path: apps/coordinator/src/components/Wallet/TransactionsTab/hooks.ts
user: login: bucko13
body: naming nit, i prefer having a verb in there as well where applicable like this. so `useFetchTransactions`
created_at: 2025-03-21T03:06:28Z
author_association: CONTRIBUTOR
path: apps/coordinator/src/components/Wallet/TransactionsTab/hooks.ts
user: login: bucko13
body: very nice!
created_at: 2025-03-21T03:07:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,216 @@
+import React, { useState } from "react";
+import {
+  Table,
+  TableBody,
+  TableCell,
+  TableContainer,
+  TableHead,
+  TableRow,
+  TableSortLabel,
+  Paper,
+  Chip,
+  IconButton,
+  Box,
+  Tooltip,
+  Typography,
+  Alert,
+  Snackbar,
+} from "@mui/material";
+import { satoshisToBitcoins } from "@caravan/bitcoin";
+import { formatDistanceToNow } from "date-fns";
+import { OpenInNew } from "@mui/icons-material";
+import { TransactionT, TransactionTableProps } from "./types";
+
+// Helper function to format the relative time
+const formatRelativeTime = (timestamp?: number): string => {
+  if (!timestamp) return "Pending";
+  return formatDistanceToNow(new Date(timestamp * 1000), { addSuffix: true });
+};
+
+// Column definitions with sorting configuration
+const columns = [
+  { id: "txid", label: "Transaction ID", sortable: false },
+  { id: "blockTime", label: "Time", sortable: true },
+  { id: "size", label: "Size (vBytes)", sortable: true },
+  { id: "fee", label: "Fee (sats)", sortable: true },
+  { id: "status", label: "Status", sortable: false },
+  { id: "actions", label: "", sortable: false },
+];
+
+// FeeDisplay component to display fees display in both sats and BTC
path: apps/coordinator/src/components/Wallet/TransactionsTab/TransactionsTable.tsx
user: login: bucko13
body: ```suggestion
// FeeDisplay component to display fees in both sats and BTC
```
created_at: 2025-03-21T03:08:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,46 @@
+// Types for the transaction data
+export interface Transaction {
path: apps/coordinator/src/components/Wallet/TransactionsTab/types.ts
user: login: bucko13
body: should we put a TODO here that this should come from @caravan/clients? 
created_at: 2025-03-21T03:11:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,273 @@
+import React, { useState, useEffect, useCallback } from "react";
+import { connect } from "react-redux";
+import {
+  Box,
+  Typography,
+  Tooltip,
+  IconButton,
+  CircularProgress,
+  Tabs,
+  Tab,
+  Chip,
+} from "@mui/material";
+import { Refresh } from "@mui/icons-material";
+import { BlockchainClient } from "@caravan/clients";
+import { blockExplorerTransactionURL, Network } from "@caravan/bitcoin";
+import { updateBlockchainClient } from "../../actions/clientActions";
+import { TransactionTable } from "./TransactionsTable";
+
+// How are Transaction should look like
+interface Transaction {
+  txid: string;
+  version: number;
+  locktime: number;
+  vin: any[]; // Input vector
+  vout: any[]; // Output vector
+  weight: number;
+  status: {
+    confirmed: boolean;
+    blockTime?: number;
+    blockHeight?: number;
+  };
+  size: number;
+  fee: number;
+}
+
+interface TransactionsTabProps {
+  network: Network;
+  getBlockchainClient: () => Promise<BlockchainClient>;
+  deposits: {
+    nodes: Record<string, any>;
+  };
+  change: {
+    nodes: Record<string, any>;
+  };
+  client: {
+    type: string;
+    blockchainClient?: BlockchainClient;
+  };
+}
+
+const TransactionsTab: React.FC<TransactionsTabProps> = ({
+  network,
+  getBlockchainClient,
+  deposits,
+  change,
+  client,
+}) => {
+  const [tabValue, setTabValue] = useState(0);
+  const [transactions, setTransactions] = useState<Transaction[]>([]);
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const [sortBy, setSortBy] = useState<string>("blockTime");
+  const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
+
+  // Fetch transactions
+  const fetchTransactions = useCallback(async () => {
+    setIsLoading(true);
+    setError(null);
+
+    try {
+      const blockchainClient = await getBlockchainClient();
+      if (!blockchainClient) {
+        throw new Error("No blockchain client available");
+      }
+
+      // Get unique transaction IDs from UTXOs
+      const txids = new Set<string>();
+      [
+        ...Object.values(deposits.nodes),
+        ...Object.values(change.nodes),
+      ].forEach((node) => {
+        node.utxos.forEach((utxo: { txid: string }) => {
+          txids.add(utxo.txid);
+        });
+      });
+
+      // Fetch transaction details in parallel
+      const txDetails = await Promise.all(
+        Array.from(txids).map(async (txid) => {
+          try {
+            const tx = await blockchainClient.getTransaction(txid);
path: apps/coordinator/src/components/Wallet/TransactionsTab.tsx
user: login: Legend101Zz
body: Yes, that makes sense ... I'll research further into checking how other wallets handle RBF/CPFP for private clients 
created_at: 2025-03-21T14:11:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,253 @@
+import { useState, useEffect, useCallback } from "react";
+import { useSelector } from "react-redux";
+import { blockExplorerTransactionURL } from "@caravan/bitcoin";
+import { Transaction, SortDirection, SortBy } from "./types";
+import { useGetClient } from "../../../hooks/client";
+
+/**
+ * Custom hook to manage transaction fetching and state
+ */
+export const useTransactions = () => {
path: apps/coordinator/src/components/Wallet/TransactionsTab/hooks.ts
user: login: Legend101Zz
body: changes [done](https://github.com/caravan-bitcoin/caravan/pull/168/commits/a73c2e9fb02b7f5d6281a1030138eac693ef8e88#diff-511018b155f4393fa6f4bc530aff6da3ac23c2527ff6ce2af32404419e4cf4b6R10)
created_at: 2025-03-21T15:05:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,216 @@
+import React, { useState } from "react";
+import {
+  Table,
+  TableBody,
+  TableCell,
+  TableContainer,
+  TableHead,
+  TableRow,
+  TableSortLabel,
+  Paper,
+  Chip,
+  IconButton,
+  Box,
+  Tooltip,
+  Typography,
+  Alert,
+  Snackbar,
+} from "@mui/material";
+import { satoshisToBitcoins } from "@caravan/bitcoin";
+import { formatDistanceToNow } from "date-fns";
+import { OpenInNew } from "@mui/icons-material";
+import { TransactionT, TransactionTableProps } from "./types";
+
+// Helper function to format the relative time
+const formatRelativeTime = (timestamp?: number): string => {
+  if (!timestamp) return "Pending";
+  return formatDistanceToNow(new Date(timestamp * 1000), { addSuffix: true });
+};
+
+// Column definitions with sorting configuration
+const columns = [
+  { id: "txid", label: "Transaction ID", sortable: false },
+  { id: "blockTime", label: "Time", sortable: true },
+  { id: "size", label: "Size (vBytes)", sortable: true },
+  { id: "fee", label: "Fee (sats)", sortable: true },
+  { id: "status", label: "Status", sortable: false },
+  { id: "actions", label: "", sortable: false },
+];
+
+// FeeDisplay component to display fees display in both sats and BTC
path: apps/coordinator/src/components/Wallet/TransactionsTab/TransactionsTable.tsx
user: login: Legend101Zz
body: changes [done](https://github.com/caravan-bitcoin/caravan/pull/168/commits/a73c2e9fb02b7f5d6281a1030138eac693ef8e88#diff-aa6184bfb819aafbee501095575fece6791c921de24e56e8ccbe590af24ec312R40)
created_at: 2025-03-21T15:06:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,46 @@
+// Types for the transaction data
+export interface Transaction {
path: apps/coordinator/src/components/Wallet/TransactionsTab/types.ts
user: login: Legend101Zz
body: changes [done](https://github.com/caravan-bitcoin/caravan/pull/168/commits/a73c2e9fb02b7f5d6281a1030138eac693ef8e88#diff-4a0b69244c694fd95d0106a3eaef6a945cd690f68507107d38d02cddc6bc5e62R1)
created_at: 2025-03-21T15:07:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,280 @@
+import { useState, useEffect, useCallback } from "react";
+import { useSelector } from "react-redux";
+import { blockExplorerTransactionURL } from "@caravan/bitcoin";
+import { Transaction, SortDirection, SortBy } from "./types";
+import { useGetClient } from "../../../hooks/client";
+
+/**
+ * Custom hook to manage transaction fetching and state
+ */
+export const useFetchTransactions = () => {
+  const [transactions, setTransactions] = useState<Transaction[]>([]);
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const [mounted, setMounted] = useState(true);
+
+  // Use selectors to get state from Redux store
+  const deposits = useSelector((state: any) => state.wallet.deposits);
+  const change = useSelector((state: any) => state.wallet.change);
+
+  const blockchainClient = useGetClient();
+
+  // Helper function to fetch individual transaction details
+  const fetchTransactionDetails = async (txid: string) => {
+    try {
+      if (!blockchainClient) {
+        throw new Error("No blockchain client available");
+      }
+      return await blockchainClient.getTransaction(txid);
+    } catch (err) {
+      console.error(`Error fetching tx ${txid}:`, err);
+      return null;
+    }
+  };
+
+  // Fetch transactions
+  const fetchTransactions = useCallback(async () => {
+    if (!mounted) return;
+
+    setIsLoading(true);
+    setError(null);
+
+    try {
+      if (!blockchainClient) {
+        throw new Error("No blockchain client available");
+      }
+
+      // Set to store transaction IDs
+      const txids = new Set<string>();
+
+      // As some addresses can belong to UTXO's that no longer belong to the wallet so we separate addresses with active UTXOs and spent UTXOs
+      const addressesWithActiveUTXOs = new Set<string>();
+      const addressesWithSpentUTXOs = new Set<string>();
+
+      // Track which transactions are from spent UTXOs
+      const spentTxids = new Set<string>();
+
+      //Now we process all nodes to categorize addresses
+      [
+        ...Object.values(deposits.nodes),
+        ...Object.values(change.nodes),
+      ].forEach((node: any) => {
+        if (node && node.multisig && node.multisig.address) {
+          if (node.utxos && node.utxos.length > 0) {
+            // Address has active UTXOs
+            addressesWithActiveUTXOs.add(node.multisig.address);
+
+            // Add transaction IDs from active UTXOs
+            node.utxos.forEach((utxo: any) => {
+              if (utxo.txid) txids.add(utxo.txid);
+            });
+          } else if (node.addressUsed) {
+            // Address has been used but has no UTXOs (spent)
+            addressesWithSpentUTXOs.add(node.multisig.address);
path: apps/coordinator/src/components/Wallet/TransactionsTab/hooks.ts
user: login: bucko13
body: curious why this was necessary? 
created_at: 2025-03-25T03:46:52Z
author_association: CONTRIBUTOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-01-05T18:29:19Z
body: ###  🦋  Changeset detected

Latest commit: f8ac83db11f9de6c91745c8ce7630d2c5a7f119b

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 2 packages</summary>

  | Name                | Type  |
| ------------------- | ----- |
| @caravan/clients    | Patch |
| caravan-coordinator | Minor |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/Legend101Zz/caravan/new/transactions-tab?filename=.changeset/healthy-pianos-tie.md&value=---%0A%22caravan-coordinator%22%3A%20patch%0A---%0A%0Afeat(wallet)%3A%20Add%20transaction%20history%20tab%20with%20pending%2Fconfirmed%20views%0A)


author_association: NONE
user: login: vercel[bot]
created_at: 2025-01-05T18:29:21Z
body: [vc]: #xaLt8kDxTUQR/oDxiFJ3tQLG9BUL5J9e6yWwG83x/No=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJjYXJhdmFuLWNvb3JkaW5hdG9yIiwicm9vdERpcmVjdG9yeSI6ImFwcHMvY29vcmRpbmF0b3IiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vdW5jaGFpbmVkL2NhcmF2YW4tY29vcmRpbmF0b3IvRlRmdlR1VVRScEtMangxUThkVWptbXJFUzhXMyIsInByZXZpZXdVcmwiOiJjYXJhdmFuLWNvb3JkaW5hdG9yLWdpdC1mb3JrLWxlZ2VuZDEwMXp6LXRyYW5zLTcwNTBmMC11bmNoYWluZWQudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJjYXJhdmFuLWNvb3JkaW5hdG9yLWdpdC1mb3JrLWxlZ2VuZDEwMXp6LXRyYW5zLTcwNTBmMC11bmNoYWluZWQudmVyY2VsLmFwcCJ9fV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **caravan-coordinator** | ✅ Ready ([Inspect](https://vercel.com/unchained/caravan-coordinator/FTfvTuUTRpKLjx1Q8dUjmmrES8W3)) | [Visit Preview](https://caravan-coordinator-git-fork-legend101zz-trans-7050f0-unchained.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/caravan-coordinator-git-fork-legend101zz-trans-7050f0-unchained.vercel.app?via=pr-comment-feedback-link) | Mar 24, 2025 8:34pm |


author_association: NONE
user: login: Legend101Zz
created_at: 2025-01-05T18:30:44Z
body: Here’s a preview of the new **Transactions** tab, showing the split view for pending and confirmed transactions:

<img width="1335" alt="Screenshot 2025-01-05 at 23 38 04" src="https://github.com/user-attachments/assets/5ef6d4aa-bf77-4b69-ab7b-53ddeed3abd2" />





author_association: CONTRIBUTOR
user: login: Legend101Zz
created_at: 2025-01-05T18:59:36Z
body: ### Dependency Notice  

This PR depends on #134 as it uses a newly added transaction method we will need here. So we need to ensure #134 is reviewed and merged first. 
author_association: CONTRIBUTOR
user: login: bucko13
created_at: 2025-01-05T19:58:42Z
body: > ### Dependency Notice
> This PR depends on #134 as it uses a newly added transaction method we will need here. So we need to ensure #134 is reviewed and merged first.

Submitted another review. Should be almost ready to go. I'll make sure to prioritize review and merge of #134 ASAP
author_association: CONTRIBUTOR
user: login: bucko13
created_at: 2025-01-08T17:45:16Z
body: FYI: we'll need a changeset for the coordinator as well.
author_association: CONTRIBUTOR
user: login: coderabbitai[bot]
created_at: 2025-02-21T14:19:46Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
This update introduces a new transaction retrieval method in the `BlockchainClient` along with its normalization logic and corresponding tests. Changes have been made to add a new wallet mode and associated React components to support transaction viewing within the coordinator app. Additionally, state management for transactions is expanded in the reducer. In the caravan-clients package, the legacy Bitcoind JavaScript file is removed and replaced by a comprehensive TypeScript implementation including new type definitions. Minor adjustments, such as adding constant imports and TypeScript error suppression comments, are also included.

## Changes

| File(s) | Change Summary |
|---------|----------------|
| `@caravan/clients`<br>`client.ts`, `client.test.ts` | Added `getTransaction` and `normalizeTransactionData` in `BlockchainClient`; updated tests for transaction retrieval and data normalization. |
| `@caravan/clients`<br>`types.ts` | Introduced multiple new interfaces/types for raw transactions, transaction details, RPC interactions, and Bitcoin node parameters. |
| `@caravan/clients`<br>`bitcoind.js` (removed), `bitcoind.ts` | Removed legacy Bitcoind JS file; added a new TypeScript file with functions for RPC calls, error handling, and transaction data handling. |
| `@caravan/clients`<br>`wallet.ts` | Added TypeScript error suppression comments in `callBitcoindWallet` and `bitcoindListUnspent` functions. |
| `apps/coordinator/src/actions/transactionActions.js`<br>`walletActions.js` | Added new import of `DUST_IN_BTC` and introduced a new `TRANSACTIONS` mode with value `3` for wallet actions. |
| `apps/coordinator/src/components/Wallet/TransactionsTab.tsx`<br>`TransactionsTable.tsx` | Created new React components (`TransactionsTab`, `TransactionTable`) with interfaces, state management, and Redux connections to display and manage blockchain transactions. |
| `apps/coordinator/src/components/Wallet/WalletControl.jsx` | Integrated a new "Transactions" tab in the wallet, with conditional rendering to load the `TransactionsTab` component when selected. |
| `apps/coordinator/src/reducers/transactionReducer.js` | Expanded the initial state to include a structured `transactions` object with properties for transaction data, loading state, errors, and timestamp. |

## Sequence Diagram(s)
```mermaid
sequenceDiagram
    participant U as User
    participant WC as WalletControl
    participant TT as TransactionsTab
    participant BC as BlockchainClient
    participant B as bitcoind.ts

    U->>WC: Select "Transactions" tab
    WC->>TT: Render TransactionsTab component
    TT->>BC: Call fetchTransactions() → getTransaction(txid)
    BC->>B: Make RPC call for raw transaction data
    B-->>BC: Return raw transaction data
    BC->>BC: Normalize transaction data
    BC-->>TT: Return TransactionDetails
    TT->>U: Display updated transactions
```

## Poem
> I'm a rabbit, hopping through the code,  
> With transactions bright, my whiskers explode.  
> New tabs and tests in every byte,  
> A burrow of features shining so light.  
> CodeRabbit cheers with a joyful hop,  
> In this realm of updates, I never want to stop!  
> 🐰💻✨ Enjoy the new digital crop!

<!-- walkthrough_end -->
<!-- This is an auto-generated comment: all tool run failures by coderabbit.ai -->

> [!WARNING]
> There were issues while running some tools. Please review the errors and either fix the tool’s configuration or disable the tool if it’s a critical failure.
> 
> <details>
> <summary>🔧 ESLint</summary>
> 
> > If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.
> 
> <details>
> <summary>apps/coordinator/src/components/Wallet/TransactionsTab.tsx</summary>
> 
> 
> Oops! Something went wrong! :(
> 
> ESLint: 7.32.0
> 
> ESLint couldn't find the plugin "eslint-plugin-testing-library".
> 
> (The package "eslint-plugin-testing-library" was not found when loaded as a Node module from the directory "/apps/coordinator".)
> 
> It's likely that the plugin isn't installed correctly. Try reinstalling by running the following:
> 
>     npm install eslint-plugin-testing-library@latest --save-dev
> 
> The plugin "eslint-plugin-testing-library" was referenced from the config file in "apps/coordinator/.eslintrc.cjs » @caravan/eslint-config/app.js".
> 
> If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team.
> 
> 
> </details>
> 
> <details>
> <summary>apps/coordinator/src/actions/transactionActions.js</summary>
> 
> 
> Oops! Something went wrong! :(
> 
> ESLint: 7.32.0
> 
> ESLint couldn't find the plugin "eslint-plugin-testing-library".
> 
> (The package "eslint-plugin-testing-library" was not found when loaded as a Node module from the directory "/apps/coordinator".)
> 
> It's likely that the plugin isn't installed correctly. Try reinstalling by running the following:
> 
>     npm install eslint-plugin-testing-library@latest --save-dev
> 
> The plugin "eslint-plugin-testing-library" was referenced from the config file in "apps/coordinator/.eslintrc.cjs » @caravan/eslint-config/app.js".
> 
> If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team.
> 
> 
> </details>
> 
> <details>
> <summary>apps/coordinator/src/actions/walletActions.js</summary>
> 
> 
> Oops! Something went wrong! :(
> 
> ESLint: 7.32.0
> 
> ESLint couldn't find the plugin "eslint-plugin-testing-library".
> 
> (The package "eslint-plugin-testing-library" was not found when loaded as a Node module from the directory "/apps/coordinator".)
> 
> It's likely that the plugin isn't installed correctly. Try reinstalling by running the following:
> 
>     npm install eslint-plugin-testing-library@latest --save-dev
> 
> The plugin "eslint-plugin-testing-library" was referenced from the config file in "apps/coordinator/.eslintrc.cjs » @caravan/eslint-config/app.js".
> 
> If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team.
> 
> 
> </details>
> 
> + 8 others
> 
> </details>

<!-- end of auto-generated comment: all tool run failures by coderabbit.ai -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings (Beta)

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR. ([Beta](https://docs.coderabbit.ai/early-access#beta))
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: Legend101Zz
created_at: 2025-02-21T14:24:01Z
body: Here are some screenshots to how the transaction tab looks in both private and non-private nodes ...

## `Private`
<img width="1440" alt="Screenshot 2025-02-21 at 19 06 48" src="https://github.com/user-attachments/assets/7dd712cf-81d5-4ebc-9324-08544ecdb997" />

<img width="1430" alt="Screenshot 2025-02-21 at 19 08 00" src="https://github.com/user-attachments/assets/6d1ed7f2-84b3-4266-987d-60fabe90e5dc" />


## `Normal`

<img width="1328" alt="Screenshot 2025-02-21 at 19 10 28" src="https://github.com/user-attachments/assets/713b0dc3-db2d-4c29-bb60-9b6a5bf7c01a" />

author_association: CONTRIBUTOR
user: login: jevidon
created_at: 2025-02-24T16:25:10Z
body: Thanks for the updates here! Some feedback on the changes:

- It would be helpful to show a column with the bitcoin value of the UTXO
- The "Transaction ID" pill in the left column should just copy the ID to the clipboard instead of opening a block explorer
- The “open in preferred explorer” link at the right end seems to always open in mempool.space - is it possible to have that use whatever block explorer was selected by the user when loading the wallet?

author_association: CONTRIBUTOR
user: login: Legend101Zz
created_at: 2025-03-10T19:44:50Z
body: > Thanks for the updates here! Some feedback on the changes:
> 
> * It would be helpful to show a column with the bitcoin value of the UTXO
> * The "Transaction ID" pill in the left column should just copy the ID to the clipboard instead of opening a block explorer
> * The “open in preferred explorer” link at the right end seems to always open in mempool.space - is it possible to have that use whatever block explorer was selected by the user when loading the wallet?

Thanks for the reviews @jevidon I have pushed the required changes as per the reviews ... please let me know if they work for you :) 


author_association: CONTRIBUTOR
user: login: bucko13
created_at: 2025-03-14T21:12:22Z
body: ![Screenshot 2025-03-14 at 4 07 27 PM](https://github.com/user-attachments/assets/af78b312-33c1-40ee-a753-8031bb11bd53)

Unfortunately it doesn't look like the table is populating correctly now. This is a local regtest node so communicating via bitcoind rpc. The network calls are being made, but I am getting an error still:

```
Error fetching tx 8962df7edfe0e610af294ae1b6a738f325df96cebb9e3846dff420f4c404c34f: Error: Failed to get transaction: Cannot read properties of undefined (reading 'txid')
    at BlockchainClient.getTransaction (index-370fe92a.js:441:39165)
```

i believe this is with the latest deployment: https://caravan-coordinator-jh702nj0b-unchained.vercel.app/#/wallet
author_association: CONTRIBUTOR
user: login: Legend101Zz
created_at: 2025-03-17T17:04:49Z
body: > FYI: we'll need a changeset for the coordinator as well.

I've added all the required changesets, and I had to include an extra one for the `clients package` too. This extra changeset covers a `minor patch` for `BlockchainClient.getTransaction`—it now properly accesses the result property that was already being returned by `bitcoindRawTxData`.

Sorry for not catching this in #134; I'll be more careful next time `:)
author_association: CONTRIBUTOR
user: login: bucko13
created_at: 2025-03-18T16:47:26Z
body: I merged and released the fix #196 and merged the changes back into this PR. 
author_association: CONTRIBUTOR
user: login: jevidon
created_at: 2025-03-21T21:42:49Z
body: I checked the latest vercel preview, and it looks good to me as a starting point to build on. Happy to see this merged.
author_association: CONTRIBUTOR