Files
CurrenciCombo/src/hooks/useOnChainBalances.ts
Devin AI 007c79d7a9 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>
2026-04-19 00:33:46 +00:00

52 lines
1.8 KiB
TypeScript

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 };
}