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:
@@ -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<AccountType, string> = {
|
||||
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<string, OnChainBalance>;
|
||||
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 }
|
||||
<div>
|
||||
<span className="account-name-text">{account.name}</span>
|
||||
<span className="account-type-label">{account.type.replace('_', ' ')}</span>
|
||||
{account.walletAddress && (
|
||||
<span style={{ display: 'block', marginTop: 2 }}>
|
||||
<OnChainBalanceTag
|
||||
address={account.walletAddress}
|
||||
balance={onChain}
|
||||
loading={balancesLoading}
|
||||
compact
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="account-table-cell currency">{account.currency}</div>
|
||||
@@ -49,7 +71,18 @@ function AccountRow({ account, level = 0 }: { account: Account; level?: number }
|
||||
</div>
|
||||
<div className="account-table-cell identifier">
|
||||
{account.iban && <span className="mono small">{account.iban}</span>}
|
||||
{account.walletAddress && <span className="mono small">{account.walletAddress.slice(0, 10)}...</span>}
|
||||
{account.walletAddress && (
|
||||
<a
|
||||
href={explorerAddressUrl(account.walletAddress)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="mono small"
|
||||
title="View on SolaceScan"
|
||||
style={{ color: 'inherit', textDecoration: 'underline dotted' }}
|
||||
>
|
||||
{account.walletAddress.slice(0, 10)}…
|
||||
</a>
|
||||
)}
|
||||
{account.swift && <span className="swift-badge">{account.swift}</span>}
|
||||
</div>
|
||||
<div className="account-table-cell">
|
||||
@@ -62,7 +95,7 @@ function AccountRow({ account, level = 0 }: { account: Account; level?: number }
|
||||
</div>
|
||||
</div>
|
||||
{expanded && hasChildren && account.subaccounts!.map(sub => (
|
||||
<AccountRow key={sub.id} account={sub} level={level + 1} />
|
||||
<AccountRow key={sub.id} account={sub} level={level + 1} onChainBalances={onChainBalances} balancesLoading={balancesLoading} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
@@ -73,6 +106,15 @@ export default function AccountsPage() {
|
||||
const [typeFilter, setTypeFilter] = useState<string>('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() {
|
||||
</div>
|
||||
<div className="account-table-body">
|
||||
{filtered.map(acc => (
|
||||
<AccountRow key={acc.id} account={acc} />
|
||||
<AccountRow
|
||||
key={acc.id}
|
||||
account={acc}
|
||||
onChainBalances={onChainBalances}
|
||||
balancesLoading={balancesLoading}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user