import React, { useEffect, useState } from 'react' import { Box, Text } from 'ink' import { type Address } from 'viem' import { useSafe, useChain } from '../hooks/index.js' import { Header, KeyValue, Spinner } from '../components/index.js' import { theme } from '../theme.js' import { SafeService } from '../../services/safe-service.js' import { ABIService } from '../../services/abi-service.js' import { formatSafeAddress } from '../../utils/eip3770.js' import { getConfigStore } from '../../storage/config-store.js' export interface AccountInfoScreenProps { /** * Chain ID of the Safe */ chainId: string /** * Address of the Safe */ address: Address /** * Optional callback when the screen is ready to exit */ onExit?: () => void } interface LiveData { version: string nonce: bigint balance?: bigint owners: Address[] threshold: number modules?: Address[] guard?: Address | null fallbackHandler?: Address | null masterCopy?: Address | null } /** * AccountInfoScreen displays detailed information about a Safe account. * This replaces the imperative console.log implementation in commands/account/info.ts * * Features: * - Shows basic Safe information (name, address, chain, status) * - Fetches and displays live on-chain data for deployed Safes * - Shows predicted configuration for undeployed Safes * - Displays explorer link * - Animated loading states */ export function AccountInfoScreen({ chainId, address, onExit, }: AccountInfoScreenProps): React.ReactElement { const { safe, loading: safeLoading } = useSafe(chainId, address) const { chain, loading: chainLoading, error: chainError } = useChain(chainId) const [liveData, setLiveData] = useState(null) const [fetchingLive, setFetchingLive] = useState(false) const [liveError, setLiveError] = useState(null) const [contractNames, setContractNames] = useState>({}) // Fetch live on-chain data useEffect(() => { if (!chain) { if (!chainLoading && onExit) { onExit() } return } // If Safe is in storage and not deployed, skip fetching if (safe && !safe.deployed) { if (!safeLoading && !chainLoading && onExit) { onExit() } return } setFetchingLive(true) const safeService = new SafeService(chain) safeService .getSafeInfo(address) .then((info) => { setLiveData({ version: info.version, nonce: info.nonce, balance: info.balance, owners: info.owners, threshold: info.threshold, modules: info.modules, guard: info.guard, fallbackHandler: info.fallbackHandler, masterCopy: info.masterCopy, }) setFetchingLive(false) if (onExit) onExit() }) .catch((error) => { setLiveError(error instanceof Error ? error.message : 'Failed to fetch on-chain data') setFetchingLive(false) if (onExit) onExit() }) }, [safe, chain, safeLoading, chainLoading, address, onExit]) // Fetch contract names from Etherscan for modules, guard, fallback handler, and mastercopy useEffect(() => { if (!liveData || !chain) return const addressesToFetch: Address[] = [] // Collect all addresses to fetch if (liveData.masterCopy) addressesToFetch.push(liveData.masterCopy) if (liveData.modules) addressesToFetch.push(...liveData.modules) if (liveData.guard) addressesToFetch.push(liveData.guard) if (liveData.fallbackHandler) addressesToFetch.push(liveData.fallbackHandler) if (addressesToFetch.length === 0) return // Get Etherscan API key from config const configStore = getConfigStore() const preferences = configStore.getPreferences() const etherscanApiKey = preferences?.etherscanApiKey const abiService = new ABIService(chain, etherscanApiKey) // Fetch contract info for each address const names: Record = {} Promise.all( addressesToFetch.map(async (addr) => { try { const info = await abiService.fetchContractInfo(addr) if (info.name) { names[addr] = info.name } } catch { // Ignore errors - contract might not be verified } }) ).then(() => { setContractNames(names) }) }, [liveData, chain]) // Loading state if (safeLoading || chainLoading) { return } // Error state if (chainError || !chain) { return ( Error: {chainError || 'Chain not found'} ) } // Note: safe can be null if the Safe is not in storage (ad-hoc query) // This is fine - we'll fetch live data directly from the blockchain const configStore = getConfigStore() const chains = configStore.getAllChains() const eip3770 = formatSafeAddress(address, chainId, chains) return (
{/* Safe name (if available) */} {safe?.name && ( {safe.name} )} {/* Basic information */} {/* Loading on-chain data */} {fetchingLive && } {/* Live on-chain data for deployed Safes */} {liveData && !fetchingLive && ( <> Owners: {liveData.owners.map((owner, i) => ( {i + 1}. {owner} ))} Threshold: {liveData.threshold} / {liveData.owners.length} {/* Advanced Safe Information */} {(liveData.masterCopy || liveData.modules || liveData.guard || liveData.fallbackHandler) && ( Advanced Configuration: {/* Master Copy / Implementation */} {liveData.masterCopy && ( Master Copy (Implementation): {liveData.masterCopy} {contractNames[liveData.masterCopy] && ( → {contractNames[liveData.masterCopy]} )} {chain.explorer && ( {chain.explorer}/address/{liveData.masterCopy} )} )} {/* Modules */} {liveData.modules && liveData.modules.length > 0 && ( Enabled Modules ({liveData.modules.length}): {liveData.modules.map((module, i) => ( {i + 1}. {module} {contractNames[module] && ( → {contractNames[module]} )} {chain.explorer && ( {chain.explorer}/address/{module} )} ))} )} {/* Guard */} {liveData.guard && ( Transaction Guard: {liveData.guard} {contractNames[liveData.guard] && ( → {contractNames[liveData.guard]} )} {chain.explorer && ( {chain.explorer}/address/{liveData.guard} )} )} {/* Fallback Handler */} {liveData.fallbackHandler && ( Fallback Handler: {liveData.fallbackHandler} {contractNames[liveData.fallbackHandler] && ( → {contractNames[liveData.fallbackHandler]} )} {chain.explorer && ( {chain.explorer}/address/{liveData.fallbackHandler} )} )} )} )} {/* Live data fetch error */} {liveError && !fetchingLive && ( ⚠ Could not fetch on-chain data {liveError} )} {/* Predicted configuration for undeployed Safes */} {safe && !safe.deployed && safe.predictedConfig && ( Predicted Configuration: {safe.predictedConfig.owners.map((owner, i) => ( {i + 1}. {owner} ))} Threshold: {safe.predictedConfig.threshold} / {safe.predictedConfig.owners.length} )} {/* Explorer link */} {chain.explorer && ( Explorer: {chain.explorer}/address/{address} )} {/* Success message */} Safe information displayed successfully ) }