feat(portal): wire DashboardPage to live Chain-138 RPC + SolaceScan Explorer
- Add services/{http,chain138,explorer,proxmox,dbisCore} + hooks/{useLiveChain,useOnChainBalances}
- Add BackendStatusBar + LiveNetworkPanel components on DashboardPage
- Overlay on-chain META balance on account rows carrying a walletAddress
- Normalize EIP-55 checksum in chain138.getNativeBalance so hand-typed
sample custody addresses (e.g. 0x742d35Cc...bD38) don't silently drop
out of the balance map
- Default RPC: https://rpc.d-bis.org (user-preferred gateway)
- proxmox.ts stays mocked (CF-Access, needs BFF); dbisCore.ts stays
mocked (no public deployment yet)
Co-Authored-By: Nakamoto, S <defi@defi-oracle.io>
This commit is contained in:
54
src/hooks/useLiveChain.ts
Normal file
54
src/hooks/useLiveChain.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
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<ChainHealth | null>(null);
|
||||
const [latestBlock, setLatestBlock] = useState<LatestBlock | null>(null);
|
||||
const [stats, setStats] = useState<ExplorerStats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [lastUpdated, setLastUpdated] = useState<Date | null>(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(); } };
|
||||
}
|
||||
51
src/hooks/useOnChainBalances.ts
Normal file
51
src/hooks/useOnChainBalances.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { getNativeBalances, type OnChainBalance } from '../services/chain138';
|
||||
|
||||
export interface OnChainBalancesState {
|
||||
balances: Record<string, OnChainBalance>;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
lastUpdated: Date | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches native Chain-138 balances for the given addresses and re-polls
|
||||
* every `pollMs` (default 30s). Addresses array must be stable — pass a
|
||||
* memoized list, or the hook will re-fetch on every render.
|
||||
*/
|
||||
export function useOnChainBalances(addresses: string[], pollMs = 30_000): OnChainBalancesState {
|
||||
const [balances, setBalances] = useState<Record<string, OnChainBalance>>({});
|
||||
const [loading, setLoading] = useState(addresses.length > 0);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
|
||||
const mounted = useRef(true);
|
||||
|
||||
const key = addresses.join(',');
|
||||
|
||||
useEffect(() => {
|
||||
mounted.current = true;
|
||||
if (addresses.length === 0) { setLoading(false); return; }
|
||||
let cancelled = false;
|
||||
|
||||
const tick = async () => {
|
||||
try {
|
||||
const result = await getNativeBalances(addresses);
|
||||
if (cancelled || !mounted.current) return;
|
||||
setBalances(result);
|
||||
setError(null);
|
||||
setLastUpdated(new Date());
|
||||
} catch (e) {
|
||||
if (cancelled || !mounted.current) return;
|
||||
setError(e instanceof Error ? e.message : String(e));
|
||||
} finally {
|
||||
if (!cancelled && mounted.current) setLoading(false);
|
||||
}
|
||||
};
|
||||
void tick();
|
||||
const id = setInterval(tick, pollMs);
|
||||
return () => { cancelled = true; mounted.current = false; clearInterval(id); };
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [key, pollMs]);
|
||||
|
||||
return { balances, loading, error, lastUpdated };
|
||||
}
|
||||
Reference in New Issue
Block a user