import React, { useState, useEffect, useRef } from 'react' import { Box, Text } from 'ink' import type { Address } from 'viem' import { useSafes } from '../hooks/index.js' import { Header, List, KeyValue, Spinner } from '../components/index.js' import { theme } from '../theme.js' import { getConfigStore } from '../../storage/config-store.js' import { TransactionService } from '../../services/transaction-service.js' import { formatSafeAddress } from '../../utils/eip3770.js' import type { SafeAccount } from '../../types/safe.js' interface SafeLiveData { owners?: Address[] threshold?: number error?: boolean } export interface AccountListScreenProps { /** * Optional callback when the screen is ready to exit */ onExit?: () => void } /** * AccountListScreen displays all Safe accounts with live on-chain data. * This replaces the imperative console.log implementation in commands/account/list.ts * * Features: * - Shows all Safe accounts with chain info * - Displays deployment status * - Fetches live owner/threshold data for deployed Safes * - Real-time updates as data loads (reactive!) * - Empty state for no Safes */ export function AccountListScreen({ onExit }: AccountListScreenProps): React.ReactElement { const { safes, loading, error } = useSafes() const [liveData, setLiveData] = useState>(new Map()) const [fetchingLive, setFetchingLive] = useState(false) const completedCountRef = useRef(0) // Fetch live data for deployed Safes useEffect(() => { if (loading || !safes.length) return const deployedSafes = safes.filter((s) => s.deployed) if (deployedSafes.length === 0) return setFetchingLive(true) completedCountRef.current = 0 // Fetch live data independently for each safe deployedSafes.forEach(async (safe) => { const configStore = getConfigStore() const chain = configStore.getChain(safe.chainId) if (!chain) { setLiveData((prev) => new Map(prev).set(safe.address, { error: true })) completedCountRef.current++ if (completedCountRef.current === deployedSafes.length) { setFetchingLive(false) if (onExit) onExit() } return } try { const txService = new TransactionService(chain) const [owners, threshold] = await Promise.all([ txService.getOwners(safe.address as Address), txService.getThreshold(safe.address as Address), ]) // Update state independently as soon as data is available setLiveData((prev) => new Map(prev).set(safe.address, { owners, threshold })) } catch { setLiveData((prev) => new Map(prev).set(safe.address, { error: true })) } finally { completedCountRef.current++ if (completedCountRef.current === deployedSafes.length) { setFetchingLive(false) if (onExit) onExit() } } }) }, [loading, safes, onExit]) // Loading state if (loading) { return } // Error state if (error) { return ( Error: {error} ) } // Empty state if (safes.length === 0) { return (
No Safe accounts found Use "safe account create" or "safe account open" to add a Safe ) } const configStore = getConfigStore() const chains = configStore.getAllChains() // Safe list with live data return (
{fetchingLive && ( s.deployed).length} deployed Safe(s)...`} /> )} { const chain = configStore.getChain(safe.chainId) const eip3770 = formatSafeAddress(safe.address as Address, safe.chainId, chains) const status = safe.deployed ? 'deployed' : 'not deployed' const statusColor = safe.deployed ? theme.colors.success : theme.colors.warning // Get owner info let ownersValue: string if (safe.deployed) { const data = liveData.get(safe.address) if (fetchingLive && !data) { ownersValue = 'Loading...' } else if (data?.error) { ownersValue = 'Error fetching' } else if (data) { ownersValue = `${data.threshold} / ${data.owners?.length || 0}` } else { ownersValue = 'Loading...' } } else if (safe.predictedConfig) { ownersValue = `${safe.predictedConfig.threshold} / ${safe.predictedConfig.owners.length}` } else { ownersValue = 'Unknown' } return ( {safe.name} ) }} /> {/* Footer */} Total: {safes.length} Safe(s) ) }