import { useCallback, useEffect, useRef, useState } from 'react'; import { getChainHealth, getLatestBlock, type ChainHealth, type LatestBlock } from '../services/chain138'; import { getExplorerStats, type ExplorerStats } from '../services/explorer'; export interface LiveChainState { health: ChainHealth | null; latestBlock: LatestBlock | null; stats: ExplorerStats | null; loading: boolean; error: string | null; lastUpdated: Date | null; refresh: () => void; } /** * Polls chain-138 RPC + SolaceScan explorer every `pollMs` (default 12s). * Returns `null` values while loading the first time; never throws. */ export function useLiveChain(pollMs = 12_000): LiveChainState { const [health, setHealth] = useState(null); const [latestBlock, setLatestBlock] = useState(null); const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [lastUpdated, setLastUpdated] = useState(null); const mounted = useRef(true); const tick = useCallback(async () => { try { const [h, b, s] = await Promise.allSettled([getChainHealth(), getLatestBlock(), getExplorerStats()]); if (!mounted.current) return; if (h.status === 'fulfilled') setHealth(h.value); if (b.status === 'fulfilled') setLatestBlock(b.value); if (s.status === 'fulfilled') setStats(s.value); const anyError = [h, b, s].find(r => r.status === 'rejected') as PromiseRejectedResult | undefined; setError(anyError ? String(anyError.reason?.message ?? anyError.reason) : null); setLastUpdated(new Date()); } catch (e) { if (!mounted.current) return; setError(e instanceof Error ? e.message : String(e)); } finally { if (mounted.current) setLoading(false); } }, []); useEffect(() => { mounted.current = true; void tick(); const id = setInterval(tick, pollMs); return () => { mounted.current = false; clearInterval(id); }; }, [tick, pollMs]); return { health, latestBlock, stats, loading, error, lastUpdated, refresh: () => { void tick(); } }; }