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
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:
@@ -30,6 +30,9 @@
|
||||
"glM0": "1050",
|
||||
"glM1": "2100",
|
||||
"glM2": "2200",
|
||||
"glM3": "2300",
|
||||
"glM4NearMoney": "1000",
|
||||
"glM4Contingent": "1410",
|
||||
"glSettlement": "1000"
|
||||
},
|
||||
"production": {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
M0–M4 balances below are read from Fineract in real time.
|
||||
|
||||
{asOf ? ` Last updated ${asOf}.` : ''}
|
||||
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
14
packages/settlement-core/dist/money-supply.d.ts
vendored
14
packages/settlement-core/dist/money-supply.d.ts
vendored
@@ -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 M0–M4 */
|
||||
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 M0–M4 for production routing. */
|
||||
export declare const ALL_MONEY_LAYERS: MoneyLayer[];
|
||||
|
||||
47
packages/settlement-core/dist/money-supply.js
vendored
47
packages/settlement-core/dist/money-supply.js
vendored
@@ -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 M0–M4 */
|
||||
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 M0–M4 for production routing. */
|
||||
exports.ALL_MONEY_LAYERS = ['M0', 'M1', 'M2', 'M3', 'M4'];
|
||||
|
||||
13
packages/settlement-core/dist/types.d.ts
vendored
13
packages/settlement-core/dist/types.d.ts
vendored
@@ -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;
|
||||
|
||||
@@ -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 M0–M4 */
|
||||
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 M0–M4 for production routing. */
|
||||
export const ALL_MONEY_LAYERS: MoneyLayer[] = ['M0', 'M1', 'M2', 'M3', 'M4'];
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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 }],
|
||||
});
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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 }],
|
||||
});
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user