feat(portal): wire Accounts/Treasury/Reporting/Compliance/Settlements/TransactionBuilder to live Chain-138 + SolaceScan
Extends the POC from #2 beyond the Dashboard so every portal page that can benefit from on-chain signal now pulls from live backends while preserving its existing UX. Pages without an on-chain analogue (the IFRS/GAAP/IPSAS report rows, the dbis_core compliance alerts) stay on sample data with an explicit 'mocked' note. New shared primitives --------------------- src/hooks/useLatestTransactions.ts — polls SolaceScan /transactions every 15s src/hooks/useAddressTransactions.ts — per-address tx feed, 60s polling src/components/portal/LiveTransactionsPanel.tsx — reusable live-tx card src/components/portal/LiveChainBanner.tsx — slim status banner src/components/portal/OnChainBalanceTag.tsx — shared live/off-chain pill Per-page wiring --------------- AccountsPage — on-chain pill + META balance + SolaceScan link on each account row that carries a walletAddress; overlay renders only on wallet rows (negative check). SettlementsPage — replaces the static 'Settlement Rate' tile with a live Chain-138 block + tx-today tile; adds a LiveTransactionsPanel above the CSD queue so the page no longer renders identical output when RPC is dead. ReportingPage — new On-Chain Reporting Snapshot row (Blockscout /stats: block depth, total tx, total addrs, utilisation, avg block time). Clear note that the IFRS/GAAP/IPSAS rows come from dbis_core and are still mocked. TreasuryPage — two new summary tiles: live Chain-138 gas + aggregated on-chain custody (META) from sample wallet addresses. Uses the same useOnChainBalances hook as Accounts. CompliancePage — AML monitor strip with wallet selector; dedicated 'On-Chain Tx Feed' card shows IN/OUT per tracked wallet via SolaceScan. dbis_core alerts still mocked (no public deploy). TransactionBuilder — LiveChainBanner inserted above the composer so users know chain health + gas + latency before composing; transaction-builder-module made a flex column so the banner doesn't cover the canvas. Assertions baked into every live widget --------------------------------------- - RPC failure flips colour + text to 'degraded'/'—' (no silent freeze). - Loading state is distinct from both live and degraded. - Each overlay is only rendered where real data differs from sample data (walletAddress rows for balances, tracked custody for AML, etc.) so a page without live overlays is proof-of-scope, not proof-of-brokenness. Verified locally ---------------- - tsc --noEmit: clean - npm run build: clean (2066 modules, 565 ms) Still intentionally mocked -------------------------- - proxmox.ts — CF-Access protected; a BFF route is now open in orchestrator PR (see companion PR for /api/proxmox/*). - dbisCore.ts — no public deployment exists yet.
This commit is contained in:
54
src/hooks/useAddressTransactions.ts
Normal file
54
src/hooks/useAddressTransactions.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { getAddressTransactions, type ExplorerTx } from '../services/explorer';
|
||||
|
||||
export interface AddressTransactionsState {
|
||||
transactions: ExplorerTx[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
lastUpdated: Date | null;
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches recent transactions for a single address from SolaceScan.
|
||||
* Re-fetches on address change; also re-polls every `pollMs` (default 30s).
|
||||
* Empty address short-circuits — hook returns an idle state with no error.
|
||||
*/
|
||||
export function useAddressTransactions(address: string | null | undefined, limit = 10, pollMs = 30_000): AddressTransactionsState {
|
||||
const [transactions, setTransactions] = useState<ExplorerTx[]>([]);
|
||||
const [loading, setLoading] = useState(!!address);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
|
||||
const mounted = useRef(true);
|
||||
|
||||
const tick = useCallback(async () => {
|
||||
if (!address) {
|
||||
setTransactions([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const txs = await getAddressTransactions(address, limit);
|
||||
if (!mounted.current) return;
|
||||
setTransactions(txs);
|
||||
setError(null);
|
||||
setLastUpdated(new Date());
|
||||
} catch (e) {
|
||||
if (!mounted.current) return;
|
||||
setError(e instanceof Error ? e.message : String(e));
|
||||
setTransactions([]);
|
||||
} finally {
|
||||
if (mounted.current) setLoading(false);
|
||||
}
|
||||
}, [address, limit]);
|
||||
|
||||
useEffect(() => {
|
||||
mounted.current = true;
|
||||
void tick();
|
||||
if (!address) return () => { mounted.current = false; };
|
||||
const id = setInterval(tick, pollMs);
|
||||
return () => { mounted.current = false; clearInterval(id); };
|
||||
}, [tick, address, pollMs]);
|
||||
|
||||
return { transactions, loading, error, lastUpdated, refresh: () => { void tick(); } };
|
||||
}
|
||||
46
src/hooks/useLatestTransactions.ts
Normal file
46
src/hooks/useLatestTransactions.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { getLatestTransactions, type ExplorerTx } from '../services/explorer';
|
||||
|
||||
export interface LatestTransactionsState {
|
||||
transactions: ExplorerTx[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
lastUpdated: Date | null;
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Polls SolaceScan (Blockscout v2) `/transactions` every `pollMs` and
|
||||
* returns the top `limit` rows. Never throws — error surfaces in state.
|
||||
*/
|
||||
export function useLatestTransactions(limit = 20, pollMs = 15_000): LatestTransactionsState {
|
||||
const [transactions, setTransactions] = useState<ExplorerTx[]>([]);
|
||||
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 txs = await getLatestTransactions(limit);
|
||||
if (!mounted.current) return;
|
||||
setTransactions(txs);
|
||||
setError(null);
|
||||
setLastUpdated(new Date());
|
||||
} catch (e) {
|
||||
if (!mounted.current) return;
|
||||
setError(e instanceof Error ? e.message : String(e));
|
||||
} finally {
|
||||
if (mounted.current) setLoading(false);
|
||||
}
|
||||
}, [limit]);
|
||||
|
||||
useEffect(() => {
|
||||
mounted.current = true;
|
||||
void tick();
|
||||
const id = setInterval(tick, pollMs);
|
||||
return () => { mounted.current = false; clearInterval(id); };
|
||||
}, [tick, pollMs]);
|
||||
|
||||
return { transactions, loading, error, lastUpdated, refresh: () => { void tick(); } };
|
||||
}
|
||||
Reference in New Issue
Block a user