feat(settlement): add M3 and M4 money layers for full M0-M4 production
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m11s
CI/CD Pipeline / Lint and Format (push) Has been cancelled
CI/CD Pipeline / Terraform Validation (push) Has been cancelled
CI/CD Pipeline / Kubernetes Validation (push) Has been cancelled
CI/CD Pipeline / Security Scanning (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
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 26s

Extend settlement-core with GL 2300 token liabilities (M3) and near-money aggregate (M4), wire middleware workflows and dashboards to expose all five layers end-to-end.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-29 06:27:23 -07:00
parent a5c16330ca
commit fce988dceb
23 changed files with 407 additions and 169 deletions

View File

@@ -30,6 +30,9 @@
"glM0": "1050",
"glM1": "2100",
"glM2": "2200",
"glM3": "2300",
"glM4NearMoney": "1000",
"glM4Contingent": "1410",
"glSettlement": "1000"
},
"production": {

View File

@@ -32,7 +32,15 @@ export default function CentralBankDashboard() {
externalId?: string;
settlement?: { moneyLayers?: string[]; glM0?: string; glM1?: string; glM2?: string };
settlement?: {
moneyLayers?: string[];
glM0?: string;
glM1?: string;
glM2?: string;
glM3?: string;
glM4NearMoney?: string;
glM4Contingent?: string;
};
} | null;
@@ -118,7 +126,7 @@ export default function CentralBankDashboard() {
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mb-8">
<DashboardStatCard label="M0 reserve" value={formatUsd(cb.moneySupply?.M0?.gl1050)} hint={`GL 1050 · ${cb.moneySupply?.M0?.backingRatio ?? 1.2}× backing policy`} accent="blue" icon="ledger" />
@@ -126,6 +134,10 @@ export default function CentralBankDashboard() {
<DashboardStatCard label="M2 broad money" value={formatUsd(cb.moneySupply?.M2?.broadMoney)} hint="GL 2200 · token-load source" accent="gold" icon="rocket" />
<DashboardStatCard label="M3 token liabilities" value={formatUsd(cb.moneySupply?.M3?.tokenLiabilities)} hint="GL 2300 · on-chain mint liability" accent="gold" icon="rocket" />
<DashboardStatCard label="M4 near money" value={formatUsd(cb.moneySupply?.M4?.nearMoney)} hint="GL 1000 + 1410 + in-flight SWIFT" accent="blue" icon="rail" />
<DashboardStatCard label="Due from head office" value={formatUsd(cb.moneySupply?.interoffice?.dueFromHeadOffice)} hint="GL 1410 · inter-office receivable" accent="blue" icon="rail" />
</div>
@@ -208,7 +220,7 @@ export default function CentralBankDashboard() {
<div className="flex flex-wrap gap-2 mb-4">
{(office?.settlement?.moneyLayers ?? ['M0', 'M1', 'M2']).map((layer) => (
{(office?.settlement?.moneyLayers ?? ['M0', 'M1', 'M2', 'M3', 'M4']).map((layer) => (
<span key={layer} className="nasa-badge nasa-badge--caution">{layer}</span>
@@ -224,6 +236,10 @@ export default function CentralBankDashboard() {
<div className="dash-kv"><dt>M2 broad</dt><dd>GL {office?.settlement?.glM2 ?? '2200'}</dd></div>
<div className="dash-kv"><dt>M3 token liability</dt><dd>GL {office?.settlement?.glM3 ?? '2300'}</dd></div>
<div className="dash-kv"><dt>M4 near money</dt><dd>GL {office?.settlement?.glM4NearMoney ?? '1000'} + {office?.settlement?.glM4Contingent ?? '1410'}</dd></div>
<div className="dash-kv"><dt>Inter-office (HO)</dt><dd>GL 1410</dd></div>
</dl>

View File

@@ -10,6 +10,8 @@ export type MoneySupply = {
M0?: { gl1050?: string; backingRatio?: number };
M1?: { circulating?: string; gl2100?: string };
M2?: { broadMoney?: string; gl2200?: string; metaFiatPending?: string };
M3?: { tokenLiabilities?: string; gl2300?: string };
M4?: { nearMoney?: string; gl1000?: string; glContingent?: string; inFlightSwift?: string };
};
async function fetchJson<T>(url: string): Promise<T | null> {

View File

@@ -116,15 +116,19 @@ export default function Office24Dashboard() {
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mb-8">
<DashboardStatCard label="Office ID" value={String(profile?.officeId ?? 24)} hint="Fineract office · HOSPITALLERS Ali" accent="green" icon="office" />
<DashboardStatCard label="M1 circulating" value={formatUsd(moneySupply?.M1?.circulating)} hint="GL 2100 · spendable M1 liability" accent="green" icon="money" />
<DashboardStatCard label="Due from head office" value={formatUsd(moneySupply?.interoffice?.dueFromHeadOffice)} hint="GL 1410 · HO allocation receivable" accent="blue" icon="rail" />
<DashboardStatCard label="M2 broad money" value={formatUsd(moneySupply?.M2?.broadMoney)} hint="GL 2200 · fiat backing for tokens" accent="gold" icon="money" />
<DashboardStatCard label="M2 tokens" value={String(tokens.length)} hint="Registered · swap · convert · transfer" accent="gold" icon="rocket" />
<DashboardStatCard label="M3 token liabilities" value={formatUsd(moneySupply?.M3?.tokenLiabilities)} hint="GL 2300 · on-chain mint liability" accent="gold" icon="rocket" />
<DashboardStatCard label="M4 near money" value={formatUsd(moneySupply?.M4?.nearMoney)} hint="GL 1000 + 1410 + in-flight SWIFT" accent="blue" icon="rail" />
<DashboardStatCard label="M2 tokens registered" value={String(tokens.length)} hint="Registered · swap · convert · transfer" accent="gold" icon="rocket" />
</div>

View File

@@ -1,99 +1,198 @@
import type { MoneySupply } from '../central-bank/useCentralBank';
import NasaIcon from '../../components/icons/NasaIcon';
type Props = {
moneySupply: MoneySupply | null;
loading?: boolean;
officeName?: string;
};
function formatAsOf(asOf?: string) {
if (!asOf) return null;
return asOf.slice(0, 19).replace('T', ' ') + ' UTC';
}
function BannerIcon({ variant }: { variant: 'ok' | 'warn' | 'error' | 'loading' }) {
const map = {
ok: { name: 'check' as const, className: 'nasa-icon--nominal' },
warn: { name: 'alert' as const, className: 'nasa-icon--caution' },
error: { name: 'x' as const, className: 'nasa-icon--critical' },
loading: { name: 'loading' as const, className: 'nasa-icon--telemetry nasa-icon--spin' },
};
const { name, className } = map[variant];
return <NasaIcon name={name} size={18} className={`shrink-0 mt-0.5 ${className}`} />;
}
export default function FineractLedgerBanner({ moneySupply, loading, officeName }: Props) {
if (loading && !moneySupply) {
return (
<div className="dash-banner dash-banner--loading mb-6">
<BannerIcon variant="loading" />
<div>
<p className="dash-banner__title">Loading live ledger balances</p>
<p className="dash-banner__detail">Fetching Fineract data for Office 24</p>
</div>
</div>
);
}
if (!moneySupply) {
return (
<div className="dash-banner dash-banner--warn mb-6">
<BannerIcon variant="warn" />
<div>
<p className="dash-banner__title">Ledger data not available</p>
<p className="dash-banner__detail">
The money-supply API did not respond. Check settlement middleware on port 3011.
</p>
</div>
</div>
);
import type { MoneySupply } from '../central-bank/useCentralBank';
import NasaIcon from '../../components/icons/NasaIcon';
type Props = {
moneySupply: MoneySupply | null;
loading?: boolean;
officeName?: string;
};
function formatAsOf(asOf?: string) {
if (!asOf) return null;
return asOf.slice(0, 19).replace('T', ' ') + ' UTC';
}
function BannerIcon({ variant }: { variant: 'ok' | 'warn' | 'error' | 'loading' }) {
const map = {
ok: { name: 'check' as const, className: 'nasa-icon--nominal' },
warn: { name: 'alert' as const, className: 'nasa-icon--caution' },
error: { name: 'x' as const, className: 'nasa-icon--critical' },
loading: { name: 'loading' as const, className: 'nasa-icon--telemetry nasa-icon--spin' },
};
const { name, className } = map[variant];
return <NasaIcon name={name} size={18} className={`shrink-0 mt-0.5 ${className}`} />;
}
export default function FineractLedgerBanner({ moneySupply, loading, officeName }: Props) {
if (loading && !moneySupply) {
return (
<div className="dash-banner dash-banner--loading mb-6">
<BannerIcon variant="loading" />
<div>
<p className="dash-banner__title">Loading live ledger balances</p>
<p className="dash-banner__detail">Fetching Fineract data for Office 24</p>
</div>
</div>
);
}
if (!moneySupply) {
return (
<div className="dash-banner dash-banner--warn mb-6">
<BannerIcon variant="warn" />
<div>
<p className="dash-banner__title">Ledger data not available</p>
<p className="dash-banner__detail">
The money-supply API did not respond. Check settlement middleware on port 3011.
</p>
</div>
</div>
);
}
const asOf = formatAsOf(moneySupply.asOf);
const officeLabel = officeName ?? `Office ${moneySupply.officeId ?? 24}`;
if (moneySupply.fineractConnected === false) {
return (
<div className="dash-banner dash-banner--error mb-6">
<BannerIcon variant="error" />
<div>
<p className="dash-banner__title">Fineract offline</p>
<p className="dash-banner__detail">
Cannot reach Fineract for {officeLabel}. Verify OMNL_FINERACT_* credentials on the
settlement service.
</p>
</div>
</div>
);
}
if (moneySupply.fineractConnected && !moneySupply.fineractLive) {
return (
<div className="dash-banner dash-banner--warn mb-6">
<BannerIcon variant="warn" />
<div>
<p className="dash-banner__title">Fineract connected opening journal pending</p>
<p className="dash-banner__detail">
{officeLabel} has no M1 allocation yet. Post the opening entry: Debit GL 1410 (Due from
Head Office) · Credit GL 2100 (M1 liabilities).
{asOf ? ` · Checked ${asOf}` : ''}
</p>
</div>
</div>
);
}
return (
<div className="dash-banner dash-banner--ok mb-6">
<BannerIcon variant="ok" />
<div>
<p className="dash-banner__title">Live Fineract ledger · {officeLabel}</p>
<p className="dash-banner__detail">
M0M4 balances below are read from Fineract in real time.
{asOf ? ` Last updated ${asOf}.` : ''}
</p>
</div>
</div>
);
}

View File

@@ -191,6 +191,8 @@ export default function DBISSwapPanel() {
<div className="text-xs space-y-2 font-mono">
<p>M1: {(swap.ledger as { fundLedger?: { M1?: { circulating?: string } } }).fundLedger?.M1?.circulating ?? '—'}</p>
<p>M2: {(swap.ledger as { fundLedger?: { M2?: { broadMoney?: string } } }).fundLedger?.M2?.broadMoney ?? '—'}</p>
<p>M3: {(swap.ledger as { fundLedger?: { M3?: { tokenLiabilities?: string } } }).fundLedger?.M3?.tokenLiabilities ?? '—'}</p>
<p>M4: {(swap.ledger as { fundLedger?: { M4?: { nearMoney?: string } } }).fundLedger?.M4?.nearMoney ?? '—'}</p>
{(swap.ledger as { cryptoLedger?: { balances?: Record<string, string> } }).cryptoLedger?.balances && (
<div className="mt-2 pt-2 border-t border-[#1e4a8a]">
<p className="text-[#7a9bb8] mb-1">Wallet balances</p>
@@ -209,8 +211,8 @@ export default function DBISSwapPanel() {
)}
</SidePanel>
<SidePanel title="M2 token load" icon="rocket">
<p className="text-xs text-[#7a9bb8] mb-3">GL 2200 on-chain M2</p>
<SidePanel title="M2→M3 token load" icon="rocket">
<p className="text-xs text-[#7a9bb8] mb-3">GL 2200 GL 2300 · on-chain M3</p>
<input type="text" inputMode="decimal" placeholder="Fiat amount" value={swap.loadAmount} onChange={(e) => swap.setLoadAmount(e.target.value)} className="nasa-input mb-2 text-sm" />
<button type="button" className="nasa-btn nasa-btn--primary w-full text-xs" disabled={swap.loading || !swap.isConnected || !swap.loadAmount} onClick={() => swap.loadM2Token()}>
Load {swap.tokenOut.symbol}

View File

@@ -1,19 +1,22 @@
import type { MoneyLayer, MoneySupplySnapshot } from './types';
/** IPSAS GL mapping for OMNL central-bank money aggregates */
/** IPSAS GL mapping for OMNL central-bank money aggregates M0M4 */
export declare const GL_CODES: {
readonly M0_RESERVE: "1050";
readonly M1_LIABILITY: "2000";
readonly M1_CIRCULATING: "2100";
readonly M2_BROAD: "2200";
readonly M3_TOKEN_LIABILITY: "2300";
readonly SETTLEMENT_SUSPENSE: "1000";
readonly TRADE_FINANCE_CONTINGENT: "1410";
readonly CRYPTO_TOKEN_LIABILITY: "2300";
};
/** Policy caps from hybx-omnl-policy (1.2× M0 reserve → M1 capacity; 5× M1 → M2 broad) */
/** Policy caps: M0→M1 (1.2×), M1→M2 (5×), M2→M3 token mint, M4 near-money rail */
export declare const POLICY: {
readonly M0_BACKING_MULTIPLIER: 1.2;
readonly M1_CAP_MULTIPLIER: 5;
readonly M2_META_FIAT_RAIL_CONVERSION: true;
readonly M3_MINT_FROM_M2: true;
readonly M4_NEAR_MONEY_RAIL: true;
};
export declare function aggregateMetaFiatLayers(layers: Partial<Record<MoneyLayer, string>>): {
metaFiatTotal: string;
@@ -24,3 +27,10 @@ export declare function m2FiatToTokenLoadAmount(fiatAmount: string, m2Available:
loadAmount: string;
fullyBacked: boolean;
};
/** M3 on-chain token mint must not exceed outstanding M2 broad money backing. */
export declare function m3TokenLoadAmount(requested: string, m2Broad: string, m3Outstanding: string): {
loadAmount: string;
fullyBacked: boolean;
};
/** All settlement layers M0M4 for production routing. */
export declare const ALL_MONEY_LAYERS: MoneyLayer[];

View File

@@ -1,33 +1,39 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.POLICY = exports.GL_CODES = void 0;
exports.ALL_MONEY_LAYERS = exports.POLICY = exports.GL_CODES = void 0;
exports.aggregateMetaFiatLayers = aggregateMetaFiatLayers;
exports.buildMoneySupplySnapshot = buildMoneySupplySnapshot;
exports.m2FiatToTokenLoadAmount = m2FiatToTokenLoadAmount;
/** IPSAS GL mapping for OMNL central-bank money aggregates */
exports.m3TokenLoadAmount = m3TokenLoadAmount;
/** IPSAS GL mapping for OMNL central-bank money aggregates M0M4 */
exports.GL_CODES = {
M0_RESERVE: '1050',
M1_LIABILITY: '2000',
M1_CIRCULATING: '2100',
M2_BROAD: '2200',
M3_TOKEN_LIABILITY: '2300',
SETTLEMENT_SUSPENSE: '1000',
TRADE_FINANCE_CONTINGENT: '1410',
CRYPTO_TOKEN_LIABILITY: '2300',
};
/** Policy caps from hybx-omnl-policy (1.2× M0 reserve → M1 capacity; 5× M1 → M2 broad) */
/** Policy caps: M0→M1 (1.2×), M1→M2 (5×), M2→M3 token mint, M4 near-money rail */
exports.POLICY = {
M0_BACKING_MULTIPLIER: 1.2,
M1_CAP_MULTIPLIER: 5,
M2_META_FIAT_RAIL_CONVERSION: true,
M3_MINT_FROM_M2: true,
M4_NEAR_MONEY_RAIL: true,
};
function aggregateMetaFiatLayers(layers) {
const breakdown = {
M0: layers.M0 ?? '0',
M1: layers.M1 ?? '0',
M2: layers.M2 ?? '0',
M3: layers.M3 ?? '0',
M4: layers.M4 ?? '0',
M00: layers.M00 ?? '0',
};
const sum = [breakdown.M0, breakdown.M1, breakdown.M2]
const sum = [breakdown.M0, breakdown.M1, breakdown.M2, breakdown.M3, breakdown.M4]
.map((v) => parseFloat(v) || 0)
.reduce((a, b) => a + b, 0);
return { metaFiatTotal: sum.toFixed(2), breakdown };
@@ -36,9 +42,11 @@ function buildMoneySupplySnapshot(officeId, balances) {
const m0 = balances[exports.GL_CODES.M0_RESERVE] ?? '0';
const m1 = balances[exports.GL_CODES.M1_CIRCULATING] ?? '0';
const m2 = balances[exports.GL_CODES.M2_BROAD] ?? '0';
const m3 = balances[exports.GL_CODES.M3_TOKEN_LIABILITY] ?? '0';
const pending = balances[exports.GL_CODES.SETTLEMENT_SUSPENSE] ?? '0';
const m0Num = parseFloat(m0) || 0;
const m1Num = parseFloat(m1) || 0;
const contingent = balances[exports.GL_CODES.TRADE_FINANCE_CONTINGENT] ?? '0';
const inFlight = balances.inFlightSwift ?? '0';
const m4Num = (parseFloat(pending) || 0) + (parseFloat(contingent) || 0) + (parseFloat(inFlight) || 0);
return {
asOf: new Date().toISOString(),
officeId,
@@ -55,11 +63,20 @@ function buildMoneySupplySnapshot(officeId, balances) {
M2: {
broadMoney: m2,
gl2200: m2,
metaFiatPending: pending,
},
M3: {
tokenLiabilities: m3,
gl2300: m3,
},
M4: {
nearMoney: m4Num.toFixed(2),
gl1000: pending,
glContingent: contingent,
inFlightSwift: inFlight,
},
metaFiatRealRail: {
pendingSettlement: pending,
inFlightSwift: balances['inFlightSwift'] ?? '0',
inFlightSwift: inFlight,
},
};
}
@@ -72,3 +89,17 @@ function m2FiatToTokenLoadAmount(fiatAmount, m2Available) {
fullyBacked: load >= fiat && fiat > 0,
};
}
/** M3 on-chain token mint must not exceed outstanding M2 broad money backing. */
function m3TokenLoadAmount(requested, m2Broad, m3Outstanding) {
const req = parseFloat(requested) || 0;
const m2 = parseFloat(m2Broad) || 0;
const m3 = parseFloat(m3Outstanding) || 0;
const headroom = Math.max(0, m2 - m3);
const load = Math.min(req, headroom);
return {
loadAmount: load.toFixed(2),
fullyBacked: load >= req && req > 0,
};
}
/** All settlement layers M0M4 for production routing. */
exports.ALL_MONEY_LAYERS = ['M0', 'M1', 'M2', 'M3', 'M4'];

View File

@@ -1,5 +1,5 @@
/** OMNL central-bank settlement domain types */
export type MoneyLayer = 'M0' | 'M1' | 'M2' | 'M00';
export type MoneyLayer = 'M0' | 'M1' | 'M2' | 'M3' | 'M4' | 'M00';
export type PaymentRail = 'SWIFT' | 'SEPA' | 'TARGET2' | 'RTGS' | 'ACH' | 'FEDWIRE' | 'INTERNAL' | 'CHAIN138';
export type TradeFinanceInstrument = 'SBLC' | 'DLC' | 'BG' | 'LS';
export type SwiftMessageKind = 'MT103' | 'MT102' | 'MT202' | 'MT910';
@@ -93,7 +93,16 @@ export type MoneySupplySnapshot = {
M2: {
broadMoney: string;
gl2200: string;
metaFiatPending: string;
};
M3: {
tokenLiabilities: string;
gl2300: string;
};
M4: {
nearMoney: string;
gl1000: string;
glContingent: string;
inFlightSwift: string;
};
metaFiatRealRail: {
pendingSettlement: string;

View File

@@ -1,21 +1,24 @@
import type { MoneyLayer, MoneySupplySnapshot } from './types';
/** IPSAS GL mapping for OMNL central-bank money aggregates */
/** IPSAS GL mapping for OMNL central-bank money aggregates M0M4 */
export const GL_CODES = {
M0_RESERVE: '1050',
M1_LIABILITY: '2000',
M1_CIRCULATING: '2100',
M2_BROAD: '2200',
M3_TOKEN_LIABILITY: '2300',
SETTLEMENT_SUSPENSE: '1000',
TRADE_FINANCE_CONTINGENT: '1410',
CRYPTO_TOKEN_LIABILITY: '2300',
} as const;
/** Policy caps from hybx-omnl-policy (1.2× M0 reserve → M1 capacity; 5× M1 → M2 broad) */
/** Policy caps: M0→M1 (1.2×), M1→M2 (5×), M2→M3 token mint, M4 near-money rail */
export const POLICY = {
M0_BACKING_MULTIPLIER: 1.2,
M1_CAP_MULTIPLIER: 5,
M2_META_FIAT_RAIL_CONVERSION: true,
M3_MINT_FROM_M2: true,
M4_NEAR_MONEY_RAIL: true,
} as const;
export function aggregateMetaFiatLayers(
@@ -25,9 +28,11 @@ export function aggregateMetaFiatLayers(
M0: layers.M0 ?? '0',
M1: layers.M1 ?? '0',
M2: layers.M2 ?? '0',
M3: layers.M3 ?? '0',
M4: layers.M4 ?? '0',
M00: layers.M00 ?? '0',
};
const sum = [breakdown.M0, breakdown.M1, breakdown.M2]
const sum = [breakdown.M0, breakdown.M1, breakdown.M2, breakdown.M3, breakdown.M4]
.map((v) => parseFloat(v) || 0)
.reduce((a, b) => a + b, 0);
return { metaFiatTotal: sum.toFixed(2), breakdown };
@@ -40,9 +45,12 @@ export function buildMoneySupplySnapshot(
const m0 = balances[GL_CODES.M0_RESERVE] ?? '0';
const m1 = balances[GL_CODES.M1_CIRCULATING] ?? '0';
const m2 = balances[GL_CODES.M2_BROAD] ?? '0';
const m3 = balances[GL_CODES.M3_TOKEN_LIABILITY] ?? '0';
const pending = balances[GL_CODES.SETTLEMENT_SUSPENSE] ?? '0';
const m0Num = parseFloat(m0) || 0;
const m1Num = parseFloat(m1) || 0;
const contingent = balances[GL_CODES.TRADE_FINANCE_CONTINGENT] ?? '0';
const inFlight = balances.inFlightSwift ?? '0';
const m4Num =
(parseFloat(pending) || 0) + (parseFloat(contingent) || 0) + (parseFloat(inFlight) || 0);
return {
asOf: new Date().toISOString(),
officeId,
@@ -59,11 +67,20 @@ export function buildMoneySupplySnapshot(
M2: {
broadMoney: m2,
gl2200: m2,
metaFiatPending: pending,
},
M3: {
tokenLiabilities: m3,
gl2300: m3,
},
M4: {
nearMoney: m4Num.toFixed(2),
gl1000: pending,
glContingent: contingent,
inFlightSwift: inFlight,
},
metaFiatRealRail: {
pendingSettlement: pending,
inFlightSwift: balances['inFlightSwift'] ?? '0',
inFlightSwift: inFlight,
},
};
}
@@ -80,3 +97,22 @@ export function m2FiatToTokenLoadAmount(fiatAmount: string, m2Available: string)
fullyBacked: load >= fiat && fiat > 0,
};
}
/** M3 on-chain token mint must not exceed outstanding M2 broad money backing. */
export function m3TokenLoadAmount(requested: string, m2Broad: string, m3Outstanding: string): {
loadAmount: string;
fullyBacked: boolean;
} {
const req = parseFloat(requested) || 0;
const m2 = parseFloat(m2Broad) || 0;
const m3 = parseFloat(m3Outstanding) || 0;
const headroom = Math.max(0, m2 - m3);
const load = Math.min(req, headroom);
return {
loadAmount: load.toFixed(2),
fullyBacked: load >= req && req > 0,
};
}
/** All settlement layers M0M4 for production routing. */
export const ALL_MONEY_LAYERS: MoneyLayer[] = ['M0', 'M1', 'M2', 'M3', 'M4'];

View File

@@ -1,6 +1,6 @@
/** OMNL central-bank settlement domain types */
export type MoneyLayer = 'M0' | 'M1' | 'M2' | 'M00';
export type MoneyLayer = 'M0' | 'M1' | 'M2' | 'M3' | 'M4' | 'M00';
export type PaymentRail =
| 'SWIFT'
@@ -108,6 +108,8 @@ export type MoneySupplySnapshot = {
officeId: number;
M0: { reserveOz?: string; gl1050: string; backingRatio: number };
M1: { circulating: string; gl2000: string; gl2100: string; capMultiple: number };
M2: { broadMoney: string; gl2200: string; metaFiatPending: string };
M2: { broadMoney: string; gl2200: string };
M3: { tokenLiabilities: string; gl2300: string };
M4: { nearMoney: string; gl1000: string; glContingent: string; inFlightSwift: string };
metaFiatRealRail: { pendingSettlement: string; inFlightSwift: string };
};

View File

@@ -198,7 +198,7 @@ function createSettlementRouter() {
expiryDate: body.tradeFinance?.expiryDate,
},
rail: 'SWIFT',
moneyLayers: body.moneyLayers ?? ['M1', 'M2'],
moneyLayers: body.moneyLayers ?? ['M0', 'M1', 'M2', 'M3', 'M4'],
});
const record = await (0, external_transfer_1.processExternalTransfer)(merged);
res.status(record.phase === 'FAILED' ? 422 : 200).json(record);
@@ -221,7 +221,7 @@ function createSettlementRouter() {
tokenLineId: body.convertToCrypto?.tokenLineId ?? `${body.currency ?? 'USD'}-M2`,
recipientAddress: body.convertToCrypto?.recipientAddress ?? '',
},
moneyLayers: ['M2'],
moneyLayers: ['M2', 'M3'],
});
const record = await (0, external_transfer_1.processExternalTransfer)(merged);
res.status(record.phase === 'FAILED' ? 422 : 200).json(record);
@@ -262,7 +262,7 @@ function createSettlementRouter() {
...body,
officeId: (0, config_1.settlementOfficeId)(body.officeId),
rail: 'INTERNAL',
moneyLayers: body.moneyLayers ?? ['M2'],
moneyLayers: body.moneyLayers ?? ['M2', 'M3'],
});
res.status(record.phase === 'FAILED' ? 422 : 200).json(record);
}
@@ -283,7 +283,7 @@ function createSettlementRouter() {
...body,
officeId: (0, config_1.settlementOfficeId)(body.officeId),
rail: body.rail === 'RTGS' ? 'RTGS' : 'SWIFT',
moneyLayers: body.moneyLayers ?? ['M2'],
moneyLayers: body.moneyLayers ?? ['M2', 'M4'],
});
res.status(record.phase === 'FAILED' ? 422 : 200).json(record);
}

View File

@@ -117,12 +117,13 @@ async function processExternalTransfer(input) {
const balances = await (0, fineract_1.fetchGlBalances)(req.officeId);
const snap = (0, settlement_core_1.buildMoneySupplySnapshot)(req.officeId, balances);
const { loadAmount, fullyBacked } = (0, settlement_core_1.m2FiatToTokenLoadAmount)(req.amount, snap.M2.broadMoney);
if (!fullyBacked) {
record.errors.push(`M2 backing insufficient for token load (${loadAmount}/${req.amount})`);
const m3Check = (0, settlement_core_1.m3TokenLoadAmount)(loadAmount, snap.M2.broadMoney, snap.M3.tokenLiabilities);
if (!fullyBacked || !m3Check.fullyBacked) {
record.errors.push(`M2/M3 backing insufficient for token load (${loadAmount}/${req.amount}, M3 headroom ${m3Check.loadAmount})`);
}
const mint = await (0, omnl_1.requestTokenMint)({
lineId: req.convertToCrypto.tokenLineId,
amount: loadAmount,
amount: m3Check.loadAmount,
recipient: req.convertToCrypto.recipientAddress,
settlementRef: record.settlementId,
dryRun: !cfg.production.allowChainMintExecute,

View File

@@ -24,7 +24,7 @@ async function processSwiftInbound(raw) {
creditorIban: ibanMatch?.[1] ?? '',
beneficiaryName: bnfMatch?.[1]?.trim() ?? 'Inbound beneficiary',
remittanceInfo: remMatch?.[1]?.trim() ?? 'Inbound SWIFT settlement',
moneyLayers: isMt102 ? ['M1', 'M2'] : ['M0', 'M1'],
moneyLayers: isMt102 ? ['M1', 'M2', 'M3', 'M4'] : ['M0', 'M1', 'M4'],
rail: 'SWIFT',
swiftMessageKind: isMt102 ? 'MT102' : 'MT103',
rwaVerbiage: { assetClass: 'RWA', tokenLineId: 'USD-M1', externalTransferRoll: true },

View File

@@ -28,8 +28,8 @@ async function processTokenLoad(input) {
amount: req.amount,
creditorIban: 'INTERNAL-M2-TOKEN-LOAD',
beneficiaryName: req.recipientAddress,
remittanceInfo: `M2 token load ${req.lineId}${req.symbol ?? req.tokenAddress ?? 'token'}`,
moneyLayers: ['M2'],
remittanceInfo: `M2→M3 token load ${req.lineId}${req.symbol ?? req.tokenAddress ?? 'token'}`,
moneyLayers: ['M2', 'M3'],
rail: 'INTERNAL',
convertToCrypto: {
enabled: true,
@@ -50,17 +50,18 @@ async function processTokenLoad(input) {
advance('VALIDATED');
advance('VERBIAGE_ROLLED', {
verbiageDocument: [
`OMNL M2 fiat-backed token load`,
`OMNL M2→M3 fiat-backed token load`,
`Office ${officeId} · line ${req.lineId}`,
`Amount ${req.amount} · recipient ${req.recipientAddress}`,
`GL ${settlement_core_1.GL_CODES.M2_BROAD}${settlement_core_1.GL_CODES.CRYPTO_TOKEN_LIABILITY}`,
`GL ${settlement_core_1.GL_CODES.M2_BROAD}${settlement_core_1.GL_CODES.M3_TOKEN_LIABILITY}`,
].join('\n'),
});
const balances = await (0, fineract_1.fetchGlBalances)(officeId);
const snap = (0, settlement_core_1.buildMoneySupplySnapshot)(officeId, balances);
const { loadAmount, fullyBacked } = (0, settlement_core_1.m2FiatToTokenLoadAmount)(req.amount, snap.M2.broadMoney);
if (!fullyBacked) {
record.errors.push(`M2 backing insufficient (${loadAmount}/${req.amount})`);
const m3Check = (0, settlement_core_1.m3TokenLoadAmount)(loadAmount, snap.M2.broadMoney, snap.M3.tokenLiabilities);
if (!fullyBacked || !m3Check.fullyBacked) {
record.errors.push(`M2/M3 backing insufficient (M2 load ${loadAmount}/${req.amount}, M3 headroom ${m3Check.loadAmount})`);
advance('FAILED');
return record;
}
@@ -68,13 +69,13 @@ async function processTokenLoad(input) {
if (amount > 0) {
const [debitGlId, creditGlId] = await Promise.all([
(0, fineract_1.resolveGlAccountId)(profile.settlement.glM2 ?? settlement_core_1.GL_CODES.M2_BROAD),
(0, fineract_1.resolveGlAccountId)(settlement_core_1.GL_CODES.CRYPTO_TOKEN_LIABILITY),
(0, fineract_1.resolveGlAccountId)(profile.settlement.glM3 ?? settlement_core_1.GL_CODES.M3_TOKEN_LIABILITY),
]);
const je = await (0, fineract_1.postJournalEntry)({
officeId,
transactionDate: now.slice(0, 10),
referenceNumber: req.idempotencyKey,
comments: `M2 token load ${req.lineId}`,
comments: `M2→M3 token load ${req.lineId}`,
debits: [{ glAccountId: debitGlId, amount }],
credits: [{ glAccountId: creditGlId, amount }],
});

View File

@@ -32,7 +32,11 @@ async function processTransfer(input) {
creditorIban: req.creditorIban ?? (isExternal ? '' : 'INTERNAL-CHAIN138'),
beneficiaryName: req.beneficiaryName ?? req.recipientAddress,
remittanceInfo: req.remittanceInfo ?? `Transfer ${req.tokenSymbol}${req.recipientAddress}`,
moneyLayers: req.moneyLayers.length ? req.moneyLayers : ['M2'],
moneyLayers: req.moneyLayers.length
? req.moneyLayers
: isExternal
? ['M2', 'M4']
: ['M2', 'M3'],
rail: req.rail === 'CHAIN138' ? 'CHAIN138' : req.rail === 'INTERNAL' ? 'INTERNAL' : 'SWIFT',
},
errors: [],
@@ -51,7 +55,7 @@ async function processTransfer(input) {
advance('VERBIAGE_ROLLED', {
verbiageDocument: [
`OMNL ${req.rail} transfer · ${req.tokenSymbol}`,
`M2 layer · ${req.amount}`,
`Layers ${record.request.moneyLayers.join('/')} · ${req.amount}`,
`To ${req.recipientAddress}`,
isExternal ? `IBAN ${req.creditorIban}` : 'Internal / Chain 138',
].join('\n'),

View File

@@ -203,7 +203,7 @@ export function createSettlementRouter(): Router {
expiryDate: body.tradeFinance?.expiryDate,
},
rail: 'SWIFT',
moneyLayers: body.moneyLayers ?? ['M1', 'M2'],
moneyLayers: body.moneyLayers ?? ['M0', 'M1', 'M2', 'M3', 'M4'],
});
const record = await processExternalTransfer(merged);
res.status(record.phase === 'FAILED' ? 422 : 200).json(record);
@@ -225,7 +225,7 @@ export function createSettlementRouter(): Router {
tokenLineId: body.convertToCrypto?.tokenLineId ?? `${body.currency ?? 'USD'}-M2`,
recipientAddress: body.convertToCrypto?.recipientAddress ?? '',
},
moneyLayers: ['M2'],
moneyLayers: ['M2', 'M3'],
});
const record = await processExternalTransfer(merged);
res.status(record.phase === 'FAILED' ? 422 : 200).json(record);
@@ -264,7 +264,7 @@ export function createSettlementRouter(): Router {
...body,
officeId: settlementOfficeId(body.officeId),
rail: 'INTERNAL',
moneyLayers: body.moneyLayers ?? ['M2'],
moneyLayers: body.moneyLayers ?? ['M2', 'M3'],
});
res.status(record.phase === 'FAILED' ? 422 : 200).json(record);
} catch (e) {
@@ -284,7 +284,7 @@ export function createSettlementRouter(): Router {
...body,
officeId: settlementOfficeId(body.officeId),
rail: body.rail === 'RTGS' ? 'RTGS' : 'SWIFT',
moneyLayers: body.moneyLayers ?? ['M2'],
moneyLayers: body.moneyLayers ?? ['M2', 'M4'],
});
res.status(record.phase === 'FAILED' ? 422 : 200).json(record);
} catch (e) {

View File

@@ -17,6 +17,9 @@ export type OfficeSettlementProfile = {
glM0: string;
glM1: string;
glM2: string;
glM3: string;
glM4NearMoney: string;
glM4Contingent: string;
glSettlement: string;
glTradeFinanceContingent: string;
};
@@ -51,6 +54,9 @@ export type MiddlewareConfig = {
glM0: string;
glM1: string;
glM2: string;
glM3: string;
glM4NearMoney: string;
glM4Contingent: string;
glSettlement: string;
};
production: {

View File

@@ -3,6 +3,7 @@ import {
buildMoneySupplySnapshot,
isIbanValid,
m2FiatToTokenLoadAmount,
m3TokenLoadAmount,
resolveInstrumentGl,
rollExternalTransferVerbiage,
swiftKindForRequest,
@@ -136,12 +137,15 @@ export async function processExternalTransfer(
req.amount,
snap.M2.broadMoney,
);
if (!fullyBacked) {
record.errors.push(`M2 backing insufficient for token load (${loadAmount}/${req.amount})`);
const m3Check = m3TokenLoadAmount(loadAmount, snap.M2.broadMoney, snap.M3.tokenLiabilities);
if (!fullyBacked || !m3Check.fullyBacked) {
record.errors.push(
`M2/M3 backing insufficient for token load (${loadAmount}/${req.amount}, M3 headroom ${m3Check.loadAmount})`,
);
}
const mint = await requestTokenMint({
lineId: req.convertToCrypto.tokenLineId,
amount: loadAmount,
amount: m3Check.loadAmount,
recipient: req.convertToCrypto.recipientAddress,
settlementRef: record.settlementId,
dryRun: !cfg.production.allowChainMintExecute,

View File

@@ -25,7 +25,7 @@ export async function processSwiftInbound(raw: string): Promise<SettlementRecord
creditorIban: ibanMatch?.[1] ?? '',
beneficiaryName: bnfMatch?.[1]?.trim() ?? 'Inbound beneficiary',
remittanceInfo: remMatch?.[1]?.trim() ?? 'Inbound SWIFT settlement',
moneyLayers: isMt102 ? ['M1', 'M2'] : ['M0', 'M1'],
moneyLayers: isMt102 ? ['M1', 'M2', 'M3', 'M4'] : ['M0', 'M1', 'M4'],
rail: 'SWIFT',
swiftMessageKind: isMt102 ? 'MT102' : 'MT103',
rwaVerbiage: { assetClass: 'RWA', tokenLineId: 'USD-M1', externalTransferRoll: true },

View File

@@ -3,6 +3,7 @@ import {
buildMoneySupplySnapshot,
GL_CODES,
m2FiatToTokenLoadAmount,
m3TokenLoadAmount,
type TokenLoadRequest,
type SettlementRecord,
} from '@dbis/settlement-core';
@@ -33,8 +34,8 @@ export async function processTokenLoad(input: TokenLoadRequest): Promise<Settlem
amount: req.amount,
creditorIban: 'INTERNAL-M2-TOKEN-LOAD',
beneficiaryName: req.recipientAddress,
remittanceInfo: `M2 token load ${req.lineId}${req.symbol ?? req.tokenAddress ?? 'token'}`,
moneyLayers: ['M2'],
remittanceInfo: `M2→M3 token load ${req.lineId}${req.symbol ?? req.tokenAddress ?? 'token'}`,
moneyLayers: ['M2', 'M3'],
rail: 'INTERNAL',
convertToCrypto: {
enabled: true,
@@ -57,18 +58,21 @@ export async function processTokenLoad(input: TokenLoadRequest): Promise<Settlem
advance('VALIDATED');
advance('VERBIAGE_ROLLED', {
verbiageDocument: [
`OMNL M2 fiat-backed token load`,
`OMNL M2→M3 fiat-backed token load`,
`Office ${officeId} · line ${req.lineId}`,
`Amount ${req.amount} · recipient ${req.recipientAddress}`,
`GL ${GL_CODES.M2_BROAD}${GL_CODES.CRYPTO_TOKEN_LIABILITY}`,
`GL ${GL_CODES.M2_BROAD}${GL_CODES.M3_TOKEN_LIABILITY}`,
].join('\n'),
});
const balances = await fetchGlBalances(officeId);
const snap = buildMoneySupplySnapshot(officeId, balances);
const { loadAmount, fullyBacked } = m2FiatToTokenLoadAmount(req.amount, snap.M2.broadMoney);
if (!fullyBacked) {
record.errors.push(`M2 backing insufficient (${loadAmount}/${req.amount})`);
const m3Check = m3TokenLoadAmount(loadAmount, snap.M2.broadMoney, snap.M3.tokenLiabilities);
if (!fullyBacked || !m3Check.fullyBacked) {
record.errors.push(
`M2/M3 backing insufficient (M2 load ${loadAmount}/${req.amount}, M3 headroom ${m3Check.loadAmount})`,
);
advance('FAILED');
return record;
}
@@ -77,13 +81,13 @@ export async function processTokenLoad(input: TokenLoadRequest): Promise<Settlem
if (amount > 0) {
const [debitGlId, creditGlId] = await Promise.all([
resolveGlAccountId(profile.settlement.glM2 ?? GL_CODES.M2_BROAD),
resolveGlAccountId(GL_CODES.CRYPTO_TOKEN_LIABILITY),
resolveGlAccountId(profile.settlement.glM3 ?? GL_CODES.M3_TOKEN_LIABILITY),
]);
const je = await postJournalEntry({
officeId,
transactionDate: now.slice(0, 10),
referenceNumber: req.idempotencyKey,
comments: `M2 token load ${req.lineId}`,
comments: `M2→M3 token load ${req.lineId}`,
debits: [{ glAccountId: debitGlId, amount }],
credits: [{ glAccountId: creditGlId, amount }],
});

View File

@@ -31,7 +31,11 @@ export async function processTransfer(input: TransferRequest): Promise<Settlemen
creditorIban: req.creditorIban ?? (isExternal ? '' : 'INTERNAL-CHAIN138'),
beneficiaryName: req.beneficiaryName ?? req.recipientAddress,
remittanceInfo: req.remittanceInfo ?? `Transfer ${req.tokenSymbol}${req.recipientAddress}`,
moneyLayers: req.moneyLayers.length ? req.moneyLayers : ['M2'],
moneyLayers: req.moneyLayers.length
? req.moneyLayers
: isExternal
? ['M2', 'M4']
: ['M2', 'M3'],
rail: req.rail === 'CHAIN138' ? 'CHAIN138' : req.rail === 'INTERNAL' ? 'INTERNAL' : 'SWIFT',
},
errors: [],
@@ -52,7 +56,7 @@ export async function processTransfer(input: TransferRequest): Promise<Settlemen
advance('VERBIAGE_ROLLED', {
verbiageDocument: [
`OMNL ${req.rail} transfer · ${req.tokenSymbol}`,
`M2 layer · ${req.amount}`,
`Layers ${record.request.moneyLayers.join('/')} · ${req.amount}`,
`To ${req.recipientAddress}`,
isExternal ? `IBAN ${req.creditorIban}` : 'Internal / Chain 138',
].join('\n'),

View File

@@ -332,7 +332,7 @@ router.get('/omnl/health', async (req: Request, res: Response) => {
});
/**
* POST /omnl/settlement/token-load — M2 fiat-backed token mint (Office 24 settlement).
* POST /omnl/settlement/token-load — M2→M3 fiat-backed token mint (Office 24 settlement).
*/
router.post('/omnl/settlement/token-load', async (req: Request, res: Response) => {
const { lineId, amount, recipient, settlementRef, dryRun, symbol, tokenAddress } = req.body as {
@@ -370,13 +370,13 @@ router.post('/omnl/settlement/token-load', async (req: Request, res: Response) =
symbol: symbol ?? null,
tokenAddress: tokenAddress ?? null,
settlementRef: settlementRef ?? null,
moneyLayer: 'M2',
moneyLayer: 'M3',
loadFromGl: '2200',
creditGl: '2300',
capabilities,
txHash: null,
chainRailConfigured: chainRailConfigured(),
message: 'M2 token load validated — set SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE=1 to mint on-chain',
message: 'M2→M3 token load validated — set SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE=1 to mint on-chain',
});
return;
}
@@ -406,10 +406,10 @@ router.post('/omnl/settlement/token-load', async (req: Request, res: Response) =
symbol: symbol ?? null,
tokenAddress,
settlementRef: settlementRef ?? null,
moneyLayer: 'M2',
moneyLayer: 'M3',
capabilities,
txHash,
message: 'M2 token minted on Chain 138 — tradable, swappable, transferable',
message: 'M3 token minted on Chain 138 — tradable, swappable, transferable',
});
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);