import React, { useEffect, useMemo, useState } from 'react' import { Box, Text } from 'ink' import type { Address } from 'viem' import { useTransactions, useTransactionsBySafe } from '../hooks/index.js' import { Header, KeyValue, Spinner } from '../components/index.js' import { theme } from '../theme.js' import type { StoredTransaction, TransactionStatus } from '../../types/transaction.js' import { getConfigStore } from '../../storage/config-store.js' import { getSafeStorage } from '../../storage/safe-store.js' import { formatSafeAddress } from '../../utils/eip3770.js' import { TransactionService } from '../../services/transaction-service.js' export interface TransactionListScreenProps { /** * Optional Safe address to filter by */ safeAddress?: string /** * Optional chain ID to filter by (requires safeAddress) */ chainId?: string /** * Optional status filter */ statusFilter?: TransactionStatus /** * Optional callback when the screen is ready to exit */ onExit?: () => void } interface TransactionItemProps { transaction: StoredTransaction } /** * Helper function to get status badge display */ function getStatusBadge(status: TransactionStatus): { emoji: string; text: string; color: string } { switch (status) { case 'pending': return { emoji: '⏳', text: 'PENDING', color: theme.colors.warning } case 'signed': return { emoji: '✍️', text: 'SIGNED', color: theme.colors.info } case 'executed': return { emoji: '✅', text: 'EXECUTED', color: theme.colors.success } case 'rejected': return { emoji: '❌', text: 'REJECTED', color: theme.colors.error } default: return { emoji: '❓', text: 'UNKNOWN', color: theme.colors.dim } } } /** * Individual transaction item component */ function TransactionItem({ transaction }: TransactionItemProps): React.ReactElement { const configStore = getConfigStore() const safeStorage = getSafeStorage() const chains = configStore.getAllChains() const [threshold, setThreshold] = useState(undefined) const safe = safeStorage.getSafe(transaction.chainId, transaction.safeAddress) const safeName = safe?.name || 'Unknown' const eip3770 = formatSafeAddress(transaction.safeAddress as Address, transaction.chainId, chains) const chain = configStore.getChain(transaction.chainId) const statusBadge = getStatusBadge(transaction.status) // Fetch live threshold from blockchain useEffect(() => { if (!safe?.deployed || !chain) return const fetchThreshold = async () => { try { const txService = new TransactionService(chain) const liveThreshold = await txService.getThreshold(transaction.safeAddress as Address) setThreshold(liveThreshold) } catch { // Silently fail - threshold will remain undefined } } fetchThreshold() }, [safe?.deployed, chain, transaction.safeAddress]) return ( {/* Status badge */} {statusBadge.emoji} {statusBadge.text} {/* Transaction details */} ) } /** * TransactionListScreen displays a list of Safe transactions. * This replaces the imperative console.log implementation in commands/tx/list.ts * * Features: * - Displays all transactions or filtered by Safe and status * - Shows detailed transaction information * - Displays summary statistics by status * - Empty state handling * - Sorted by creation date (newest first) */ export function TransactionListScreen({ safeAddress, chainId, statusFilter, onExit, }: TransactionListScreenProps): React.ReactElement { // Use appropriate hook based on filtering const allTransactionsResult = useTransactions() const filteredTransactionsResult = useTransactionsBySafe( (safeAddress || '') as Address, chainId ) const { transactions: rawTransactions, loading, error } = safeAddress ? filteredTransactionsResult : allTransactionsResult // Auto-exit after rendering useEffect(() => { if (!loading && onExit) { onExit() } }, [loading, onExit]) // Apply status filter and sort const transactions = useMemo(() => { let result = [...rawTransactions] // Filter by status if provided if (statusFilter) { result = result.filter((tx) => tx.status === statusFilter) } // Sort by creation date (newest first) result.sort((a, b) => { const dateA = new Date(a.createdAt).getTime() const dateB = new Date(b.createdAt).getTime() return dateB - dateA }) return result }, [rawTransactions, statusFilter]) // Calculate summary statistics const summary = useMemo(() => { return { pending: transactions.filter((tx) => tx.status === 'pending').length, signed: transactions.filter((tx) => tx.status === 'signed').length, executed: transactions.filter((tx) => tx.status === 'executed').length, rejected: transactions.filter((tx) => tx.status === 'rejected').length, } }, [transactions]) // Loading state if (loading) { return } // Error state if (error) { return ( Error: {error} ) } // Empty state if (transactions.length === 0) { return (
No transactions found {safeAddress ? 'This Safe has no transactions yet' : 'No transactions found. Create one with "safe tx create"'} ) } return (
{/* Transaction count */} Found {transactions.length} transaction(s) {/* Transaction list */} {transactions.map((tx) => ( ))} {/* Summary statistics */} {/* Success message */} Transactions displayed successfully ) }