feat: wire live Fineract M1 to Central Bank UI and Office 24 journal seed
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m21s
CI/CD Pipeline / Security Scanning (push) Successful in 2m42s
CI/CD Pipeline / Terraform Validation (push) Has been cancelled
CI/CD Pipeline / Kubernetes Validation (push) Has been cancelled
CI/CD Pipeline / Lint and Format (push) Has been cancelled
Deploy ChainID 138 / Deploy ChainID 138 (push) Has been cancelled
Validation / validate-genesis (push) Has been cancelled
Validation / validate-terraform (push) Has been cancelled
Validation / validate-kubernetes (push) Has been cancelled
Validation / validate-smart-contracts (push) Has been cancelled
Validation / validate-security (push) Has been cancelled
Validation / validate-documentation (push) Has been cancelled
Verify Deployment / Verify Deployment (push) Has been cancelled

Add a read-only money-supply endpoint for Office 24 dashboards, pass Fineract env through deploy scripts, and provide a seed script for the opening Dr 1410 / Cr 2100 journal.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-28 15:41:23 -07:00
parent 37e280f372
commit 1fe57901e5
12 changed files with 237 additions and 25 deletions

View File

@@ -1,6 +1,7 @@
import { useCentralBank } from './useCentralBank';
import ChainMatrixPanel from './ChainMatrixPanel';
import { SETTLEMENT_MIDDLEWARE_URL, DBIS_EXCHANGE_URL } from '../../config/dex';
import { formatUsd } from '../../utils/formatMoney';
function StatCard({
label,
@@ -58,19 +59,19 @@ export default function CentralBankDashboard() {
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<StatCard
label="M0 Reserve (GL 1050)"
value={cb.moneySupply?.M0?.gl1050 ?? '—'}
value={formatUsd(cb.moneySupply?.M0?.gl1050)}
sub={`Backing ${cb.moneySupply?.M0?.backingRatio ?? 1.2}×`}
accent="blue"
/>
<StatCard
label="M1 Circulating (GL 2100)"
value={cb.moneySupply?.M1?.circulating ?? '—'}
value={formatUsd(cb.moneySupply?.M1?.circulating)}
sub="Narrow money · eMoney"
accent="green"
/>
<StatCard
label="M2 Broad (GL 2200)"
value={cb.moneySupply?.M2?.broadMoney ?? '—'}
value={formatUsd(cb.moneySupply?.M2?.broadMoney)}
sub="Token load source"
accent="gold"
/>
@@ -82,10 +83,27 @@ export default function CentralBankDashboard() {
/>
</div>
{cb.moneySupply?.fineractLive === false && (
<p className="text-xs text-[#f6465d] mb-6">
Fineract GL balances unavailable check OMNL_FINERACT_* on settlement middleware and seed
Office 24 opening journal (Dr 1410 / Cr 2100).
</p>
)}
{cb.moneySupply?.asOf && (
<p className="text-xs text-[#848e9c] mb-6">
Live Fineract · Office {cb.moneySupply.officeId ?? 24} · as of{' '}
{cb.moneySupply.asOf.slice(0, 19).replace('T', ' ')} UTC
{cb.moneySupply.interoffice?.dueFromHeadOffice
? ` · Due from HO (GL 1410): ${formatUsd(cb.moneySupply.interoffice.dueFromHeadOffice)}`
: ''}
</p>
)}
{!cb.moneySupply && (
<p className="text-xs text-[#848e9c] mb-6">
Set <code className="text-[#f0b90b]">VITE_OMNL_API_KEY</code> in .env.local to load live M0/M1/M2
balances from Fineract.
Money supply loads from{' '}
<code className="text-[#f0b90b]">/money-supply/public</code> when online bank mode is enabled.
</p>
)}

View File

@@ -4,11 +4,26 @@ import { DBIS_EXCHANGE_URL, SETTLEMENT_MIDDLEWARE_URL } from '../../config/dex';
export type MoneySupply = {
asOf?: string;
officeId?: number;
fineractLive?: boolean;
interoffice?: { dueFromHeadOffice?: string; gl1410?: string };
M0?: { gl1050?: string; backingRatio?: number };
M1?: { circulating?: string; gl2100?: string };
M2?: { broadMoney?: string; gl2200?: string; metaFiatPending?: string };
};
async function loadMoneySupply(settlement: string): Promise<MoneySupply | null> {
const publicRes = await fetch(`${settlement}/api/v1/settlement/money-supply/public`);
if (publicRes.ok) return publicRes.json();
const apiKey = import.meta.env.VITE_OMNL_API_KEY;
if (!apiKey) return null;
const authRes = await fetch(`${settlement}/api/v1/settlement/money-supply`, {
headers: { Authorization: `Bearer ${apiKey}` },
});
return authRes.ok ? authRes.json() : null;
}
export function useCentralBank() {
const [health, setHealth] = useState<Record<string, unknown> | null>(null);
const [office, setOffice] = useState<Record<string, unknown> | null>(null);
@@ -25,24 +40,18 @@ export function useCentralBank() {
setLoading(true);
setError(null);
try {
const [h, o, m2, mon] = await Promise.all([
const [h, o, m2, mon, ms] = await Promise.all([
fetch(`${settlement}/api/v1/settlement/health`).then((r) => r.json()),
fetch(`${settlement}/api/v1/settlement/office`).then((r) => r.json()),
fetch(`${settlement}/api/v1/settlement/tokens/m2`).then((r) => r.json()),
fetch(`${exchange}/api/v1/exchange/swap/monitor?limit=10`).then((r) => r.json()),
loadMoneySupply(settlement),
]);
setHealth(h);
setOffice(o);
setTokenCount(Array.isArray(m2.tokens) ? m2.tokens.length : 0);
setMonitor(mon);
const apiKey = import.meta.env.VITE_OMNL_API_KEY;
if (apiKey) {
const ms = await fetch(`${settlement}/api/v1/settlement/money-supply`, {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (ms.ok) setMoneySupply(await ms.json());
}
setMoneySupply(ms);
} catch (e) {
setError(e instanceof Error ? e.message : 'Failed to load central bank data');
} finally {

View File

@@ -1,8 +1,9 @@
import { Link } from 'react-router-dom';
import { useOffice24 } from './useOffice24';
import { formatUsd } from '../../utils/formatMoney';
export default function Office24Dashboard() {
const { office, tokens, health, loading, refresh } = useOffice24();
const { office, tokens, health, moneySupply, loading, refresh } = useOffice24();
const profile = office as {
officeId?: number;
externalId?: string;
@@ -34,14 +35,22 @@ export default function Office24Dashboard() {
</Link>
</header>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
<div className="o24-card">
<p className="text-xs text-[#848e9c] uppercase">Office ID</p>
<p className="text-2xl font-bold text-[#0ecb81]">{profile?.officeId ?? 24}</p>
</div>
<div className="o24-card">
<p className="text-xs text-[#848e9c] uppercase">Default currency</p>
<p className="text-2xl font-bold text-[#eaecef]">{profile?.settlement?.defaultCurrency ?? 'USD'}</p>
<p className="text-xs text-[#848e9c] uppercase">M1 circulating (GL 2100)</p>
<p className="text-2xl font-bold text-[#eaecef]">
{formatUsd(moneySupply?.M1?.circulating)}
</p>
</div>
<div className="o24-card">
<p className="text-xs text-[#848e9c] uppercase">Due from HO (GL 1410)</p>
<p className="text-2xl font-bold text-[#3861fb]">
{formatUsd(moneySupply?.interoffice?.dueFromHeadOffice)}
</p>
</div>
<div className="o24-card">
<p className="text-xs text-[#848e9c] uppercase">M2 tradable tokens</p>
@@ -49,6 +58,12 @@ export default function Office24Dashboard() {
</div>
</div>
{moneySupply?.fineractLive === false && (
<p className="text-sm text-[#f6465d] mb-6">
No Fineract journals for Office 24 yet. Post opening M1: Dr 1410 / Cr 2100 via{' '}
<code className="text-[#f0b90b]">scripts/deployment/seed-office-24-opening-journal.mjs</code>
</p>
)}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
<section className="o24-card lg:col-span-1">
<h2 className="font-semibold text-[#eaecef] mb-4">Settlement rails</h2>

View File

@@ -1,5 +1,6 @@
import { useCallback, useEffect, useState } from 'react';
import { DBIS_EXCHANGE_URL, SETTLEMENT_MIDDLEWARE_URL, type DexToken } from '../../config/dex';
import type { MoneySupply } from '../central-bank/useCentralBank';
export type M2Token = DexToken & {
moneyLayer?: string;
@@ -13,6 +14,7 @@ export function useOffice24() {
const [office, setOffice] = useState<Record<string, unknown> | null>(null);
const [tokens, setTokens] = useState<M2Token[]>([]);
const [health, setHealth] = useState<Record<string, unknown> | null>(null);
const [moneySupply, setMoneySupply] = useState<MoneySupply | null>(null);
const [loading, setLoading] = useState(true);
const settlement = SETTLEMENT_MIDDLEWARE_URL.replace(/\/$/, '');
@@ -21,14 +23,18 @@ export function useOffice24() {
const refresh = useCallback(async () => {
setLoading(true);
try {
const [o, t, h] = await Promise.all([
const [o, t, h, ms] = await Promise.all([
fetch(`${settlement}/api/v1/settlement/office`).then((r) => r.json()),
fetch(`${settlement}/api/v1/settlement/tokens/m2`).then((r) => r.json()),
fetch(`${exchange}/api/v1/exchange/health`).then((r) => r.json()),
fetch(`${settlement}/api/v1/settlement/money-supply/public`).then((r) =>
r.ok ? r.json() : null,
),
]);
setOffice(o);
setTokens(Array.isArray(t.tokens) ? t.tokens : []);
setHealth(h);
setMoneySupply(ms);
} finally {
setLoading(false);
}
@@ -38,5 +44,5 @@ export function useOffice24() {
refresh();
}, [refresh]);
return { office, tokens, health, loading, refresh };
return { office, tokens, health, moneySupply, loading, refresh };
}

View File

@@ -0,0 +1,11 @@
/** Format Fineract USD balance strings for dashboard display */
export function formatUsd(value: string | number | undefined | null): string {
if (value === undefined || value === null || value === '') return '—';
const n = typeof value === 'number' ? value : parseFloat(String(value));
if (!Number.isFinite(n)) return '—';
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
maximumFractionDigits: 2,
}).format(n);
}