From 7253ad1974fe4e5527f10f4e6d0ef1cf0d0d238f Mon Sep 17 00:00:00 2001 From: "Nakamoto, S" Date: Sun, 19 Apr 2026 08:31:04 +0000 Subject: [PATCH] feat(portal): wire Accounts/Treasury/Reporting/Compliance/Settlements/TransactionBuilder to live Chain-138 + SolaceScan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/Portal.tsx | 8 +- src/components/portal/LiveChainBanner.tsx | 50 ++++++++ .../portal/LiveTransactionsPanel.tsx | 92 ++++++++++++++ src/components/portal/OnChainBalanceTag.tsx | 42 +++++++ src/hooks/useAddressTransactions.ts | 54 ++++++++ src/hooks/useLatestTransactions.ts | 46 +++++++ src/pages/AccountsPage.tsx | 57 ++++++++- src/pages/CompliancePage.tsx | 119 +++++++++++++++++- src/pages/ReportingPage.tsx | 62 ++++++++- src/pages/SettlementsPage.tsx | 20 ++- src/pages/TreasuryPage.tsx | 39 +++++- 11 files changed, 570 insertions(+), 19 deletions(-) create mode 100644 src/components/portal/LiveChainBanner.tsx create mode 100644 src/components/portal/LiveTransactionsPanel.tsx create mode 100644 src/components/portal/OnChainBalanceTag.tsx create mode 100644 src/hooks/useAddressTransactions.ts create mode 100644 src/hooks/useLatestTransactions.ts diff --git a/src/Portal.tsx b/src/Portal.tsx index 4c614f4..8f37d5b 100644 --- a/src/Portal.tsx +++ b/src/Portal.tsx @@ -8,6 +8,7 @@ import ReportingPage from './pages/ReportingPage'; import CompliancePage from './pages/CompliancePage'; import SettlementsPage from './pages/SettlementsPage'; import PortalLayout from './components/portal/PortalLayout'; +import LiveChainBanner from './components/portal/LiveChainBanner'; import App from './App'; function ProtectedRoute({ children }: { children: React.ReactNode }) { @@ -64,8 +65,11 @@ export default function Portal() { element={ -
- +
+ +
+ +
diff --git a/src/components/portal/LiveChainBanner.tsx b/src/components/portal/LiveChainBanner.tsx new file mode 100644 index 0000000..ea81c46 --- /dev/null +++ b/src/components/portal/LiveChainBanner.tsx @@ -0,0 +1,50 @@ +import { useLiveChain } from '../../hooks/useLiveChain'; +import { endpoints } from '../../config/endpoints'; +import { Zap } from 'lucide-react'; + +/** + * Slim one-line banner suitable for embedding above dense UIs (e.g. the + * transaction-builder canvas). Shows chain health + block + gas. + * Flips to a red "RPC degraded" state on polling failure so you don't + * accidentally compose a tx against a dead endpoint. + */ +export default function LiveChainBanner() { + const { health, error, lastUpdated } = useLiveChain(); + const ok = !error && !!health; + return ( +
+ + + Chain {endpoints.chain138.chainId} ({endpoints.chain138.name}) + + + {error ? `● RPC degraded · ${error}` : ok ? '● LIVE' : '○ connecting…'} + + {ok && ( + <> + block + {health.blockNumber.toLocaleString()} + gas + {health.gasPriceGwei.toFixed(4)} gwei + rpc + {health.latencyMs}ms + + )} + + rpc: {endpoints.chain138.rpcUrl} + {lastUpdated && · {lastUpdated.toLocaleTimeString()}} + +
+ ); +} diff --git a/src/components/portal/LiveTransactionsPanel.tsx b/src/components/portal/LiveTransactionsPanel.tsx new file mode 100644 index 0000000..f64cd63 --- /dev/null +++ b/src/components/portal/LiveTransactionsPanel.tsx @@ -0,0 +1,92 @@ +import { useLatestTransactions } from '../../hooks/useLatestTransactions'; +import { explorerTxUrl, explorerAddressUrl, type ExplorerTx } from '../../services/explorer'; +import { formatEther } from 'ethers'; +import { Activity } from 'lucide-react'; + +const shortHash = (h: string) => `${h.slice(0, 10)}…${h.slice(-6)}`; +const shortAddr = (a: string) => `${a.slice(0, 6)}…${a.slice(-4)}`; +const formatMETA = (wei: string) => { + try { return `${Number(formatEther(BigInt(wei))).toFixed(4)} META`; } catch { return `${wei} wei`; } +}; +const relativeTime = (iso: string) => { + const then = new Date(iso).getTime(); + const dt = Date.now() - then; + if (dt < 60_000) return `${Math.max(1, Math.round(dt / 1000))}s ago`; + if (dt < 3_600_000) return `${Math.round(dt / 60_000)}m ago`; + return `${Math.round(dt / 3_600_000)}h ago`; +}; + +interface Props { + /** Max rows to show (default 10). */ + limit?: number; + /** Custom card header label — defaults to "Live Chain-138 Transactions". */ + title?: string; +} + +/** + * Renders the most recent on-chain transactions from SolaceScan. + * Degraded state shows the error message; empty state shows a one-liner. + * Links every hash/address to the explorer. + */ +export default function LiveTransactionsPanel({ limit = 10, title = 'Live Chain-138 Transactions' }: Props) { + const { transactions, loading, error, lastUpdated } = useLatestTransactions(limit); + + return ( +
+
+

{title}

+ + {error + ? RPC degraded · {error} + : loading + ? 'loading…' + : `${transactions.length} tx · ${lastUpdated ? lastUpdated.toLocaleTimeString() : '—'}`} + +
+
+ {!loading && transactions.length === 0 && !error && ( +
+ No transactions returned yet — SolaceScan may be indexing. +
+ )} + {transactions.map((tx: ExplorerTx) => ( +
+ + {shortHash(tx.hash)} + + + {shortAddr(tx.from.hash)} + + + {tx.to ? shortAddr(tx.to.hash) : '— contract create —'} + + {formatMETA(tx.value)} + {relativeTime(tx.timestamp)} + + {tx.status ?? 'pending'} + +
+ ))} +
+
+ Source: SolaceScan Explorer + {' · polls every 15s · Blockscout v2 /transactions'} +
+
+ ); +} diff --git a/src/components/portal/OnChainBalanceTag.tsx b/src/components/portal/OnChainBalanceTag.tsx new file mode 100644 index 0000000..4248b77 --- /dev/null +++ b/src/components/portal/OnChainBalanceTag.tsx @@ -0,0 +1,42 @@ +import type { OnChainBalance } from '../../services/chain138'; +import { endpoints } from '../../config/endpoints'; +import { explorerAddressUrl } from '../../services/explorer'; + +interface Props { + address: string; + balance: OnChainBalance | undefined; + loading: boolean; + compact?: boolean; +} + +/** + * Renders a small pill that flips between three states: + * ● live · chain 138 — on-chain read succeeded + * ○ fetching… — initial RPC call in flight + * ○ off-chain — RPC call failed / empty state + * + * When `balance` is present, the pill becomes a link to the address page on + * SolaceScan. We never hide this tag once a walletAddress is present — + * otherwise there's no way to tell "not wired" apart from "backend is down". + */ +export default function OnChainBalanceTag({ address, balance, loading, compact }: Props) { + const color = balance ? '#22c55e' : loading ? '#eab308' : '#6b7280'; + const label = balance ? `● live · chain ${endpoints.chain138.chainId}` : loading ? '○ fetching…' : '○ off-chain'; + const href = explorerAddressUrl(address); + const style: React.CSSProperties = { + fontSize: compact ? 9 : 10, + color, + textDecoration: 'none', + letterSpacing: 0.2, + }; + return ( + + {label} + {balance && ( + + {Number(balance.balanceEth).toFixed(4)} META + + )} + + ); +} diff --git a/src/hooks/useAddressTransactions.ts b/src/hooks/useAddressTransactions.ts new file mode 100644 index 0000000..71ff024 --- /dev/null +++ b/src/hooks/useAddressTransactions.ts @@ -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([]); + const [loading, setLoading] = useState(!!address); + const [error, setError] = useState(null); + const [lastUpdated, setLastUpdated] = useState(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(); } }; +} diff --git a/src/hooks/useLatestTransactions.ts b/src/hooks/useLatestTransactions.ts new file mode 100644 index 0000000..7e3e2f0 --- /dev/null +++ b/src/hooks/useLatestTransactions.ts @@ -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([]); + 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 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(); } }; +} diff --git a/src/pages/AccountsPage.tsx b/src/pages/AccountsPage.tsx index b5e5458..6bd446d 100644 --- a/src/pages/AccountsPage.tsx +++ b/src/pages/AccountsPage.tsx @@ -1,10 +1,14 @@ -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import { Building2, ChevronRight, ChevronDown, Search, Filter, Plus, Download, ExternalLink, Copy, MoreHorizontal } from 'lucide-react'; import { sampleAccounts } from '../data/portalData'; import type { Account, AccountType } from '../types/portal'; +import { useOnChainBalances } from '../hooks/useOnChainBalances'; +import type { OnChainBalance } from '../services/chain138'; +import OnChainBalanceTag from '../components/portal/OnChainBalanceTag'; +import { explorerAddressUrl } from '../services/explorer'; const typeColors: Record = { operating: '#3b82f6', reserve: '#22c55e', custody: '#a855f7', escrow: '#f97316', @@ -20,9 +24,17 @@ const formatBalance = (amount: number, currency: string) => { return `${sym}${amount.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; }; -function AccountRow({ account, level = 0 }: { account: Account; level?: number }) { +interface AccountRowProps { + account: Account; + level?: number; + onChainBalances: Record; + balancesLoading: boolean; +} + +function AccountRow({ account, level = 0, onChainBalances, balancesLoading }: AccountRowProps) { const [expanded, setExpanded] = useState(false); const hasChildren = account.subaccounts && account.subaccounts.length > 0; + const onChain = account.walletAddress ? onChainBalances[account.walletAddress] : undefined; return ( <> @@ -39,6 +51,16 @@ function AccountRow({ account, level = 0 }: { account: Account; level?: number }
{account.name} {account.type.replace('_', ' ')} + {account.walletAddress && ( + + + + )}
{account.currency}
@@ -49,7 +71,18 @@ function AccountRow({ account, level = 0 }: { account: Account; level?: number }
{account.iban && {account.iban}} - {account.walletAddress && {account.walletAddress.slice(0, 10)}...} + {account.walletAddress && ( + + {account.walletAddress.slice(0, 10)}… + + )} {account.swift && {account.swift}}
@@ -62,7 +95,7 @@ function AccountRow({ account, level = 0 }: { account: Account; level?: number }
{expanded && hasChildren && account.subaccounts!.map(sub => ( - + ))} ); @@ -73,6 +106,15 @@ export default function AccountsPage() { const [typeFilter, setTypeFilter] = useState('all'); const [view, setView] = useState<'tree' | 'flat'>('tree'); + const onChainAddresses = useMemo( + () => sampleAccounts + .flatMap(a => [a, ...(a.subaccounts || [])]) + .filter(a => !!a.walletAddress) + .map(a => a.walletAddress as string), + [], + ); + const { balances: onChainBalances, loading: balancesLoading } = useOnChainBalances(onChainAddresses); + const allAccounts = view === 'flat' ? sampleAccounts.flatMap(a => [a, ...(a.subaccounts || [])]) : sampleAccounts; @@ -175,7 +217,12 @@ export default function AccountsPage() {
{filtered.map(acc => ( - + ))}
diff --git a/src/pages/CompliancePage.tsx b/src/pages/CompliancePage.tsx index 37637e0..09a138d 100644 --- a/src/pages/CompliancePage.tsx +++ b/src/pages/CompliancePage.tsx @@ -1,6 +1,10 @@ -import { useState } from 'react'; -import { Shield, AlertTriangle, CheckCircle2, Clock, Filter, Download, Eye, UserCheck } from 'lucide-react'; -import { complianceAlerts } from '../data/portalData'; +import { useMemo, useState } from 'react'; +import { Shield, AlertTriangle, CheckCircle2, Clock, Filter, Download, Eye, UserCheck, Activity } from 'lucide-react'; +import { complianceAlerts, sampleAccounts } from '../data/portalData'; +import { useLiveChain } from '../hooks/useLiveChain'; +import { useAddressTransactions } from '../hooks/useAddressTransactions'; +import { explorerAddressUrl } from '../services/explorer'; +import { endpoints } from '../config/endpoints'; const severityColors: Record = { critical: '#ef4444', high: '#f97316', medium: '#eab308', low: '#3b82f6', @@ -29,6 +33,21 @@ const regulatoryFrameworks = [ export default function CompliancePage() { const [severityFilter, setSeverityFilter] = useState('all'); const [statusFilter, setStatusFilter] = useState('all'); + const { health, error: liveErr } = useLiveChain(); + + const tracked = useMemo( + () => sampleAccounts + .flatMap(a => [a, ...(a.subaccounts || [])]) + .filter(a => !!a.walletAddress) + .map(a => ({ name: a.name, address: a.walletAddress as string, type: a.type })), + [], + ); + const [selectedWallet, setSelectedWallet] = useState(tracked[0]?.address ?? ''); + const { + transactions: walletTxs, + loading: walletLoading, + error: walletErr, + } = useAddressTransactions(selectedWallet, 10, 60_000); const filtered = complianceAlerts.filter(a => { const matchSev = severityFilter === 'all' || a.severity === severityFilter; @@ -39,6 +58,8 @@ export default function CompliancePage() { const openCount = complianceAlerts.filter(a => a.status === 'open').length; const criticalCount = complianceAlerts.filter(a => a.severity === 'critical' && a.status !== 'resolved').length; + const selectedWalletName = tracked.find(t => t.address === selectedWallet)?.name ?? ''; + return (
@@ -52,6 +73,44 @@ export default function CompliancePage() {
+ {/* On-Chain AML Monitoring strip */} +
+
+ + + On-chain AML monitor — Chain {endpoints.chain138.chainId} + + + {liveErr ? `● degraded · ${liveErr}` : health ? `● live · block ${health.blockNumber.toLocaleString()}` : '○ polling…'} + + + Tracked custody wallets: {tracked.length} + +
+ +
+ {/* Compliance Metrics */}
{complianceMetrics.map(m => ( @@ -116,6 +175,60 @@ export default function CompliancePage() {
+ {/* On-chain transactions for the selected tracked wallet */} +
+
+

On-Chain Tx Feed

+ + {selectedWalletName ? selectedWalletName : 'select a wallet above'} + {walletLoading ? ' · loading…' : walletErr ? ` · ${walletErr}` : ''} + +
+
+ {!walletLoading && walletTxs.length === 0 && !walletErr && ( +
+ No on-chain activity for this wallet yet. + {selectedWallet && ( + <> View on SolaceScan. + )} +
+ )} + {walletTxs.map(tx => ( +
+ + + {tx.hash.slice(0, 14)}… + + + + {tx.from.hash.toLowerCase() === selectedWallet.toLowerCase() ? 'OUT →' : 'IN ←'} + {' '} + {(tx.from.hash.toLowerCase() === selectedWallet.toLowerCase() ? tx.to?.hash : tx.from.hash)?.slice(0, 10) ?? '—'}… + + {new Date(tx.timestamp).toLocaleTimeString()} + + {tx.status ?? 'pending'} + +
+ ))} +
+
+ {/* Regulatory Frameworks */}
diff --git a/src/pages/ReportingPage.tsx b/src/pages/ReportingPage.tsx index 8d19d12..c36a1fa 100644 --- a/src/pages/ReportingPage.tsx +++ b/src/pages/ReportingPage.tsx @@ -1,7 +1,9 @@ import { useState } from 'react'; -import { FileText, Download, Filter, Plus, Eye, Clock, CheckCircle2, AlertTriangle, Send } from 'lucide-react'; +import { FileText, Download, Filter, Plus, Eye, Clock, CheckCircle2, AlertTriangle, Send, Database } from 'lucide-react'; import { reportConfigs } from '../data/portalData'; import type { ReportingStandard } from '../types/portal'; +import { useLiveChain } from '../hooks/useLiveChain'; +import { endpoints } from '../config/endpoints'; const standardColors: Record = { IPSAS: '#a855f7', @@ -27,6 +29,7 @@ export default function ReportingPage() { const [standardFilter, setStandardFilter] = useState('all'); const [typeFilter, setTypeFilter] = useState('all'); const [activeStandard, setActiveStandard] = useState('IFRS'); + const { health, stats, error: liveErr, lastUpdated: liveUpdatedAt } = useLiveChain(); const filtered = reportConfigs.filter(r => { const matchStandard = standardFilter === 'all' || r.standard === standardFilter; @@ -70,6 +73,63 @@ export default function ReportingPage() {
+ {/* On-Chain Reporting Snapshot — live data from Chain-138 + SolaceScan */} +
+
+ + On-Chain Reporting Snapshot — Chain {endpoints.chain138.chainId} + + + {liveErr ? `RPC degraded · ${liveErr}` : liveUpdatedAt ? `updated ${liveUpdatedAt.toLocaleTimeString()}` : 'polling…'} + +
+
+ Latest Block +
{liveErr ? '—' : (health?.blockNumber?.toLocaleString() ?? '…')}
+
+
+ Total Blocks (ledger depth) +
{liveErr ? '—' : (stats?.total_blocks?.toLocaleString() ?? '…')}
+
+
+ Total Transactions +
{liveErr ? '—' : (stats?.total_transactions?.toLocaleString() ?? '…')}
+
+
+ Total Addresses +
{liveErr ? '—' : (stats?.total_addresses?.toLocaleString() ?? '…')}
+
+
+ Network Utilisation +
+ {liveErr ? '—' : (stats ? `${stats.network_utilization_percentage.toFixed(1)}%` : '…')} +
+
+
+ Avg Block Time +
+ {liveErr ? '—' : (stats ? `${stats.average_block_time.toFixed(1)}s` : '…')} +
+
+
+ Sources: {endpoints.chain138.rpcUrl} + {' · '}{endpoints.explorer.apiBaseUrl}/api/v2/stats + {' · the IFRS / US GAAP / IPSAS reports below are generated by dbis_core (currently mocked — no public deployment).'} +
+
+ {/* Standards Overview */}
{(['IPSAS', 'US_GAAP', 'IFRS'] as ReportingStandard[]).map(std => ( diff --git a/src/pages/SettlementsPage.tsx b/src/pages/SettlementsPage.tsx index de63b9a..bce7168 100644 --- a/src/pages/SettlementsPage.tsx +++ b/src/pages/SettlementsPage.tsx @@ -1,6 +1,8 @@ import { useState } from 'react'; import { CheckSquare, Filter, Download, Clock, CheckCircle2, XCircle, ArrowUpDown } from 'lucide-react'; import { settlementRecords } from '../data/portalData'; +import LiveTransactionsPanel from '../components/portal/LiveTransactionsPanel'; +import { useLiveChain } from '../hooks/useLiveChain'; const statusColors: Record = { pending: '#eab308', matched: '#3b82f6', affirmed: '#a855f7', @@ -26,6 +28,7 @@ export default function SettlementsPage() { const [statusFilter, setStatusFilter] = useState('all'); const [typeFilter, setTypeFilter] = useState('all'); const [sortBy, setSortBy] = useState<'date' | 'amount'>('date'); + const { health, stats, error: liveErr } = useLiveChain(); const filtered = settlementRecords .filter(s => (statusFilter === 'all' || s.status === statusFilter) && (typeFilter === 'all' || s.type === typeFilter)) @@ -52,24 +55,31 @@ export default function SettlementsPage() {
- Pending + Pending (CSD) {pendingCount} {formatCurrency(totalPending, 'USD')} total
- Settled + Settled (CSD) {settledCount}
- Failed + Failed (CSD) {failedCount}
- Settlement Rate - {settledCount > 0 ? ((settledCount / (settledCount + failedCount)) * 100).toFixed(0) : 0}% + Chain-138 Block + + {liveErr ? '—' : health?.blockNumber?.toLocaleString() ?? '…'} + + + {liveErr ? 'RPC degraded' : stats ? `${stats.transactions_today.toLocaleString()} tx today` : 'polling…'} +
+ +

Settlement Queue

diff --git a/src/pages/TreasuryPage.tsx b/src/pages/TreasuryPage.tsx index 5c60e91..66f0a11 100644 --- a/src/pages/TreasuryPage.tsx +++ b/src/pages/TreasuryPage.tsx @@ -1,6 +1,9 @@ -import { useState } from 'react'; -import { Landmark, TrendingUp, TrendingDown, Download, Filter, ArrowUpDown, RefreshCw } from 'lucide-react'; -import { treasuryPositions, cashForecasts } from '../data/portalData'; +import { useMemo, useState } from 'react'; +import { Landmark, TrendingUp, TrendingDown, Download, Filter, ArrowUpDown, RefreshCw, Zap } from 'lucide-react'; +import { treasuryPositions, cashForecasts, sampleAccounts } from '../data/portalData'; +import { useLiveChain } from '../hooks/useLiveChain'; +import { useOnChainBalances } from '../hooks/useOnChainBalances'; +import { endpoints } from '../config/endpoints'; const formatCurrency = (amount: number) => { if (Math.abs(amount) >= 1_000_000) return `$${(amount / 1_000_000).toFixed(2)}M`; @@ -11,6 +14,18 @@ const formatCurrency = (amount: number) => { export default function TreasuryPage() { const [assetFilter, setAssetFilter] = useState('all'); const [sortBy, setSortBy] = useState<'value' | 'pnl' | 'name'>('value'); + const { health, error: liveErr } = useLiveChain(); + + const custodyAddresses = useMemo( + () => sampleAccounts + .flatMap(a => [a, ...(a.subaccounts || [])]) + .filter(a => !!a.walletAddress) + .map(a => a.walletAddress as string), + [], + ); + const { balances: onChainBalances } = useOnChainBalances(custodyAddresses); + const totalOnChainMETA = Object.values(onChainBalances) + .reduce((sum, b) => sum + Number(b.balanceEth || 0), 0); const assetClasses = [...new Set(treasuryPositions.map(p => p.assetClass))]; @@ -63,6 +78,24 @@ export default function TreasuryPage() { {totalPnL >= 0 ? '+' : ''}{((totalPnL / totalCostBasis) * 100).toFixed(2)}%
+
+ Chain-138 Gas + + {liveErr ? '—' : health ? `${health.gasPriceGwei.toFixed(3)} gwei` : '…'} + + + {liveErr ? 'RPC degraded' : health ? `block ${health.blockNumber.toLocaleString()}` : 'polling…'} + +
+
+ On-Chain Custody (META) + + {totalOnChainMETA.toFixed(4)} + + + {custodyAddresses.length} custody wallet{custodyAddresses.length === 1 ? '' : 's'} · chain {endpoints.chain138.chainId} + +