fix: separate M4 near-money from interoffice GL 1410
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m13s
CI/CD Pipeline / Security Scanning (push) Successful in 2m22s
CI/CD Pipeline / Lint and Format (push) Failing after 41s
CI/CD Pipeline / Terraform Validation (push) Failing after 24s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 28s
Validation / validate-genesis (push) Successful in 28s
Validation / validate-terraform (push) Failing after 29s
Validation / validate-kubernetes (push) Failing after 10s
Validation / validate-smart-contracts (push) Failing after 9s
Validation / validate-security (push) Failing after 1m18s
Validation / validate-documentation (push) Failing after 17s

M4 aggregate now sums GL 1000 suspense plus in-flight SWIFT only; GL 1410 stays under interoffice HO receivable. SWIFT/RTGS outbound journals route M2/M1 into M4 suspense via resolveM4OutboundJournal.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-29 07:26:34 -07:00
parent 7aaf5ca086
commit 771697563e
9 changed files with 104 additions and 19 deletions

View File

@@ -136,7 +136,7 @@ export default function CentralBankDashboard() {
<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="M4 near money" value={formatUsd(cb.moneySupply?.M4?.nearMoney)} hint="GL 1000 suspense + 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" />
@@ -238,7 +238,7 @@ export default function CentralBankDashboard() {
<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>M4 near money</dt><dd>GL {office?.settlement?.glM4NearMoney ?? '1000'} + in-flight SWIFT</dd></div>
<div className="dash-kv"><dt>Inter-office (HO)</dt><dd>GL 1410</dd></div>

View File

@@ -126,7 +126,7 @@ export default function Office24Dashboard() {
<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="M4 near money" value={formatUsd(moneySupply?.M4?.nearMoney)} hint="GL 1000 suspense + 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" />

View File

@@ -34,3 +34,12 @@ export declare function m3TokenLoadAmount(requested: string, m2Broad: string, m3
};
/** All settlement layers M0M4 for production routing. */
export declare const ALL_MONEY_LAYERS: MoneyLayer[];
/** M4 outbound SWIFT/RTGS: move M1/M2 into GL 1000 settlement suspense while in flight. */
export declare function resolveM4OutboundJournal(moneyLayers: MoneyLayer[], gl: {
glM1: string;
glM2: string;
glM4NearMoney: string;
}): {
debitGl: string;
creditGl: string;
} | null;

View File

@@ -5,6 +5,7 @@ exports.aggregateMetaFiatLayers = aggregateMetaFiatLayers;
exports.buildMoneySupplySnapshot = buildMoneySupplySnapshot;
exports.m2FiatToTokenLoadAmount = m2FiatToTokenLoadAmount;
exports.m3TokenLoadAmount = m3TokenLoadAmount;
exports.resolveM4OutboundJournal = resolveM4OutboundJournal;
/** IPSAS GL mapping for OMNL central-bank money aggregates M0M4 */
exports.GL_CODES = {
M0_RESERVE: '1050',
@@ -46,7 +47,8 @@ function buildMoneySupplySnapshot(officeId, balances) {
const pending = balances[exports.GL_CODES.SETTLEMENT_SUSPENSE] ?? '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);
// M4 near money = GL 1000 settlement suspense + in-flight SWIFT (GL 1410 is interoffice HO receivable)
const m4Num = (parseFloat(pending) || 0) + (parseFloat(inFlight) || 0);
return {
asOf: new Date().toISOString(),
officeId,
@@ -103,3 +105,16 @@ function m3TokenLoadAmount(requested, m2Broad, m3Outstanding) {
}
/** All settlement layers M0M4 for production routing. */
exports.ALL_MONEY_LAYERS = ['M0', 'M1', 'M2', 'M3', 'M4'];
/** M4 outbound SWIFT/RTGS: move M1/M2 into GL 1000 settlement suspense while in flight. */
function resolveM4OutboundJournal(moneyLayers, gl) {
if (!moneyLayers.includes('M4'))
return null;
const suspense = gl.glM4NearMoney || exports.GL_CODES.SETTLEMENT_SUSPENSE;
if (moneyLayers.includes('M2')) {
return { debitGl: gl.glM2 || exports.GL_CODES.M2_BROAD, creditGl: suspense };
}
if (moneyLayers.includes('M1')) {
return { debitGl: gl.glM1 || exports.GL_CODES.M1_CIRCULATING, creditGl: suspense };
}
return { debitGl: exports.GL_CODES.M2_BROAD, creditGl: suspense };
}

View File

@@ -49,8 +49,8 @@ export function buildMoneySupplySnapshot(
const pending = balances[GL_CODES.SETTLEMENT_SUSPENSE] ?? '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);
// M4 near money = GL 1000 settlement suspense + in-flight SWIFT (GL 1410 is interoffice HO receivable)
const m4Num = (parseFloat(pending) || 0) + (parseFloat(inFlight) || 0);
return {
asOf: new Date().toISOString(),
officeId,
@@ -116,3 +116,20 @@ export function m3TokenLoadAmount(requested: string, m2Broad: string, m3Outstand
/** All settlement layers M0M4 for production routing. */
export const ALL_MONEY_LAYERS: MoneyLayer[] = ['M0', 'M1', 'M2', 'M3', 'M4'];
/** M4 outbound SWIFT/RTGS: move M1/M2 into GL 1000 settlement suspense while in flight. */
export function resolveM4OutboundJournal(moneyLayers: MoneyLayer[], gl: {
glM1: string;
glM2: string;
glM4NearMoney: string;
}): { debitGl: string; creditGl: string } | null {
if (!moneyLayers.includes('M4')) return null;
const suspense = gl.glM4NearMoney || GL_CODES.SETTLEMENT_SUSPENSE;
if (moneyLayers.includes('M2')) {
return { debitGl: gl.glM2 || GL_CODES.M2_BROAD, creditGl: suspense };
}
if (moneyLayers.includes('M1')) {
return { debitGl: gl.glM1 || GL_CODES.M1_CIRCULATING, creditGl: suspense };
}
return { debitGl: GL_CODES.M2_BROAD, creditGl: suspense };
}

View File

@@ -43,8 +43,15 @@ async function processExternalTransfer(input) {
const amount = parseFloat(req.amount) || 0;
const tf = req.tradeFinance;
const gl = tf ? (0, settlement_core_1.resolveInstrumentGl)(tf.instrument) : null;
const debitGl = gl?.debitGl ?? profile.settlement.glSettlement ?? cfg.moneySupply.glSettlement;
const creditGl = gl?.creditGl ?? profile.settlement.glM1 ?? cfg.moneySupply.glM1;
const m4Journal = !gl
? (0, settlement_core_1.resolveM4OutboundJournal)(req.moneyLayers, {
glM1: profile.settlement.glM1 ?? cfg.moneySupply.glM1,
glM2: profile.settlement.glM2 ?? cfg.moneySupply.glM2,
glM4NearMoney: profile.settlement.glM4NearMoney ?? cfg.moneySupply.glM4NearMoney ?? cfg.moneySupply.glSettlement,
})
: null;
const debitGl = gl?.debitGl ?? m4Journal?.debitGl ?? profile.settlement.glM2 ?? cfg.moneySupply.glM2;
const creditGl = gl?.creditGl ?? m4Journal?.creditGl ?? profile.settlement.glM4NearMoney ?? cfg.moneySupply.glSettlement;
if (amount > 0) {
const [debitGlId, creditGlId] = await Promise.all([
(0, fineract_1.resolveGlAccountId)(debitGl),

View File

@@ -56,15 +56,29 @@ async function processTransfer(input) {
verbiageDocument: [
`OMNL ${req.rail} transfer · ${req.tokenSymbol}`,
`Layers ${record.request.moneyLayers.join('/')} · ${req.amount}`,
`To ${req.recipientAddress}`,
isExternal ? `IBAN ${req.creditorIban}` : 'Internal / Chain 138',
isExternal
? `IBAN ${req.creditorIban} · M4 GL ${profile.settlement.glM4NearMoney ?? '1000'} suspense`
: `To ${req.recipientAddress} · internal chain`,
].join('\n'),
});
const amount = parseFloat(req.amount) || 0;
if (amount > 0) {
const layers = record.request.moneyLayers;
const m4Journal = isExternal
? (0, settlement_core_1.resolveM4OutboundJournal)(layers, {
glM1: profile.settlement.glM1 ?? settlement_core_1.GL_CODES.M1_CIRCULATING,
glM2: profile.settlement.glM2 ?? settlement_core_1.GL_CODES.M2_BROAD,
glM4NearMoney: profile.settlement.glM4NearMoney ?? profile.settlement.glSettlement ?? settlement_core_1.GL_CODES.SETTLEMENT_SUSPENSE,
})
: null;
const debitGl = m4Journal?.debitGl ?? profile.settlement.glM2 ?? settlement_core_1.GL_CODES.M2_BROAD;
const creditGl = m4Journal?.creditGl
?? (isExternal
? (profile.settlement.glSettlement ?? settlement_core_1.GL_CODES.SETTLEMENT_SUSPENSE)
: (profile.settlement.glM3 ?? settlement_core_1.GL_CODES.M3_TOKEN_LIABILITY));
const [debitGlId, creditGlId] = await Promise.all([
(0, fineract_1.resolveGlAccountId)(profile.settlement.glM2 ?? settlement_core_1.GL_CODES.M2_BROAD),
(0, fineract_1.resolveGlAccountId)(profile.settlement.glSettlement ?? settlement_core_1.GL_CODES.SETTLEMENT_SUSPENSE),
(0, fineract_1.resolveGlAccountId)(debitGl),
(0, fineract_1.resolveGlAccountId)(creditGl),
]);
const je = await (0, fineract_1.postJournalEntry)({
officeId,

View File

@@ -1,10 +1,12 @@
import { v4 as uuidv4 } from 'uuid';
import {
buildMoneySupplySnapshot,
GL_CODES,
isIbanValid,
m2FiatToTokenLoadAmount,
m3TokenLoadAmount,
resolveInstrumentGl,
resolveM4OutboundJournal,
rollExternalTransferVerbiage,
swiftKindForRequest,
type ExternalTransferRequest,
@@ -56,8 +58,15 @@ export async function processExternalTransfer(
const amount = parseFloat(req.amount) || 0;
const tf = req.tradeFinance;
const gl = tf ? resolveInstrumentGl(tf.instrument) : null;
const debitGl = gl?.debitGl ?? profile.settlement.glSettlement ?? cfg.moneySupply.glSettlement;
const creditGl = gl?.creditGl ?? profile.settlement.glM1 ?? cfg.moneySupply.glM1;
const m4Journal = !gl
? resolveM4OutboundJournal(req.moneyLayers, {
glM1: profile.settlement.glM1 ?? cfg.moneySupply.glM1,
glM2: profile.settlement.glM2 ?? cfg.moneySupply.glM2,
glM4NearMoney: profile.settlement.glM4NearMoney ?? cfg.moneySupply.glM4NearMoney ?? cfg.moneySupply.glSettlement,
})
: null;
const debitGl = gl?.debitGl ?? m4Journal?.debitGl ?? profile.settlement.glM2 ?? cfg.moneySupply.glM2;
const creditGl = gl?.creditGl ?? m4Journal?.creditGl ?? profile.settlement.glM4NearMoney ?? cfg.moneySupply.glSettlement;
if (amount > 0) {
const [debitGlId, creditGlId] = await Promise.all([

View File

@@ -1,5 +1,5 @@
import { v4 as uuidv4 } from 'uuid';
import { GL_CODES, type TransferRequest, type SettlementRecord } from '@dbis/settlement-core';
import { GL_CODES, resolveM4OutboundJournal, type TransferRequest, type SettlementRecord } from '@dbis/settlement-core';
import { loadConfig, loadOfficeProfile, settlementOfficeId } from '../config';
import { postJournalEntry, resolveGlAccountId } from '../adapters/fineract';
import { HybxProductionRail } from '../adapters/hybx-production';
@@ -57,16 +57,30 @@ export async function processTransfer(input: TransferRequest): Promise<Settlemen
verbiageDocument: [
`OMNL ${req.rail} transfer · ${req.tokenSymbol}`,
`Layers ${record.request.moneyLayers.join('/')} · ${req.amount}`,
`To ${req.recipientAddress}`,
isExternal ? `IBAN ${req.creditorIban}` : 'Internal / Chain 138',
isExternal
? `IBAN ${req.creditorIban} · M4 GL ${profile.settlement.glM4NearMoney ?? '1000'} suspense`
: `To ${req.recipientAddress} · internal chain`,
].join('\n'),
});
const amount = parseFloat(req.amount) || 0;
if (amount > 0) {
const layers = record.request.moneyLayers;
const m4Journal = isExternal
? resolveM4OutboundJournal(layers, {
glM1: profile.settlement.glM1 ?? GL_CODES.M1_CIRCULATING,
glM2: profile.settlement.glM2 ?? GL_CODES.M2_BROAD,
glM4NearMoney: profile.settlement.glM4NearMoney ?? profile.settlement.glSettlement ?? GL_CODES.SETTLEMENT_SUSPENSE,
})
: null;
const debitGl = m4Journal?.debitGl ?? profile.settlement.glM2 ?? GL_CODES.M2_BROAD;
const creditGl = m4Journal?.creditGl
?? (isExternal
? (profile.settlement.glSettlement ?? GL_CODES.SETTLEMENT_SUSPENSE)
: (profile.settlement.glM3 ?? GL_CODES.M3_TOKEN_LIABILITY));
const [debitGlId, creditGlId] = await Promise.all([
resolveGlAccountId(profile.settlement.glM2 ?? GL_CODES.M2_BROAD),
resolveGlAccountId(profile.settlement.glSettlement ?? GL_CODES.SETTLEMENT_SUSPENSE),
resolveGlAccountId(debitGl),
resolveGlAccountId(creditGl),
]);
const je = await postJournalEntry({
officeId,