Add HO liquidity scheduler, snapshot enrichments, and compliance console panel.
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m27s
CI/CD Pipeline / Security Scanning (push) Successful in 2m37s
CI/CD Pipeline / Lint and Format (push) Failing after 53s
CI/CD Pipeline / Terraform Validation (push) Failing after 31s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 29s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 37s
Validation / validate-genesis (push) Successful in 34s
Validation / validate-terraform (push) Failing after 33s
Validation / validate-kubernetes (push) Failing after 13s
Validation / validate-smart-contracts (push) Failing after 11s
Validation / validate-security (push) Failing after 1m26s
Validation / validate-documentation (push) Failing after 19s

Refreshes ho-liquidity-snapshot every 30 minutes with aggregate supplyNostroMatch and reserve alerts; embeds nostro/reserve KPIs in the OMNL compliance console; includes payout journal routes needed for token-aggregation build.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-06-26 01:31:01 -07:00
parent 73d785cdfc
commit 1158c616d8
10 changed files with 428 additions and 3 deletions

View File

@@ -51,6 +51,11 @@
</section>
</div>
<section class="section card">
<h2>HO nostro &amp; reserve coverage</h2>
<div id="ho-liquidity-kv"></div>
</section>
<section class="section card">
<h2>Triple-state reconcile</h2>
<div id="triple-summary"></div>

View File

@@ -12,6 +12,7 @@
safe: document.getElementById('safe-kv'),
external: document.getElementById('external-kv'),
triple: document.getElementById('triple-summary'),
hoLiquidity: document.getElementById('ho-liquidity-kv'),
signoffs: document.getElementById('signoffs-summary'),
raw: document.getElementById('raw-json'),
refreshed: document.getElementById('refreshed-at'),
@@ -147,6 +148,24 @@
]);
}
function renderHoLiquidity(snap) {
if (!els.hoLiquidity) return;
if (!snap) {
els.hoLiquidity.textContent = 'HO liquidity snapshot unavailable.';
return;
}
const cash = snap.cashInBank || {};
const cov = snap.reserveCoverage || {};
els.hoLiquidity.innerHTML = kv([
['Generated', snap.generatedAt],
['Nostro cash (GL 13010)', cash.cashInBankUsd != null ? '$' + Number(cash.cashInBankUsd).toLocaleString() : '—'],
['Reserve coverage', cov.coverageRatio != null ? cov.coverageRatio.toFixed(2) + '×' : '—'],
['Reserve assets', cov.reserveAssetsUsd != null ? '$' + Number(cov.reserveAssetsUsd).toLocaleString() : '—'],
['Issued c* USD', cov.issuedCStarUsd != null ? '$' + Number(cov.issuedCStarUsd).toLocaleString() : '—'],
['Public API', '/token-aggregation/api/v1/omnl/ho-liquidity-snapshot'],
]);
}
function renderTriple(data) {
const t = data.tripleReconcile;
if (!t) {
@@ -198,6 +217,12 @@
renderSafe(data);
renderExternal(data);
renderTriple(data);
try {
const hoRes = await fetch(apiUrl('/omnl/ho-liquidity-snapshot'), { headers: apiHeaders() });
if (hoRes.ok) renderHoLiquidity(await hoRes.json());
} catch (_) {
renderHoLiquidity(null);
}
renderSignoffs(data);
els.raw.textContent = JSON.stringify(data, null, 2);
els.refreshed.textContent = 'Updated ' + new Date(data.generatedAt).toLocaleString();

View File

@@ -0,0 +1,101 @@
import { Router, Request, Response } from 'express';
import { omnlRateLimiter } from '../middleware/rate-limit';
import { omnlRequireApiKeyInProduction } from '../middleware/omnl-guards';
import { omnlAuditMiddleware } from '../middleware/omnl-audit-middleware';
import {
createJournalEntry,
postJournalEntry,
reverseJournalEntry,
getJournalEntry,
createBalanceReservation,
releaseBalanceReservation,
getBalanceReservation,
} from '../../services/omnl-payout-journal';
const router = Router();
router.use(omnlRateLimiter);
router.use(omnlAuditMiddleware);
router.use(omnlRequireApiKeyInProduction);
function routeParam(value: string | string[] | undefined): string {
if (Array.isArray(value)) return value[0] ?? '';
return value ?? '';
}
/**
* POST /journal-entries — payout orchestrator integration (CR-004).
*/
router.post('/journal-entries', (req: Request, res: Response) => {
try {
const record = createJournalEntry(req.body ?? {});
res.status(201).json(record);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
res.status(msg.includes('invalid') ? 422 : 400).json({ error: msg });
}
});
router.get('/journal-entries/:id', (req: Request, res: Response) => {
const record = getJournalEntry(routeParam(req.params.id));
if (!record) {
res.status(404).json({ error: 'not found' });
return;
}
res.json(record);
});
router.post('/journal-entries/:id/post', (req: Request, res: Response) => {
try {
res.json(postJournalEntry(routeParam(req.params.id)));
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
res.status(msg.includes('not found') ? 404 : 422).json({ error: msg });
}
});
router.post('/journal-entries/:id/reverse', (req: Request, res: Response) => {
try {
res.json(reverseJournalEntry(routeParam(req.params.id)));
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
res.status(msg.includes('not found') ? 404 : 422).json({ error: msg });
}
});
/**
* POST /balance-reservations — reserve funds before payout dispatch.
*/
router.post('/balance-reservations', (req: Request, res: Response) => {
try {
const body = req.body ?? {};
if (!body.instructionId || body.amount == null || !body.currency) {
res.status(400).json({ error: 'instructionId, amount, currency required' });
return;
}
const record = createBalanceReservation(body);
res.status(201).json(record);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
res.status(400).json({ error: msg });
}
});
router.get('/balance-reservations/:id', (req: Request, res: Response) => {
const record = getBalanceReservation(routeParam(req.params.id));
if (!record) {
res.status(404).json({ error: 'not found' });
return;
}
res.json(record);
});
router.delete('/balance-reservations/:id', (req: Request, res: Response) => {
try {
res.json(releaseBalanceReservation(routeParam(req.params.id)));
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
res.status(msg.includes('not found') ? 404 : 422).json({ error: msg });
}
});
export default router;

View File

@@ -23,6 +23,7 @@ import partnerPayloadRoutes from './routes/partner-payloads';
import plannerV2Routes from './routes/planner-v2';
import omnlRoutes from './routes/omnl';
import omnlIpsasRoutes from './routes/omnl-ipsas';
import omnlPayoutJournalRoutes from './routes/omnl-payout-journal-routes';
import omnlComplianceRoutes from './routes/omnl-compliance-routes';
import omnlTerminalRoutes from './routes/omnl-terminal-routes';
import checkpointRoutes from './routes/checkpoint';
@@ -303,6 +304,7 @@ export class ApiServer {
this.app.use('/api/v1', omnlTerminalRoutes);
this.app.use('/api/v1', omnlRoutes);
this.app.use('/api/v1', omnlIpsasRoutes);
this.app.use('/api/v1', omnlPayoutJournalRoutes);
this.app.use('/api/v2', plannerV2Routes);
// Admin routes (stricter rate limit)

View File

@@ -6,6 +6,7 @@ import { closeDatabasePool } from './database/client';
import { logger } from './utils/logger';
import { startRouteMatrixScheduler } from './services/route-matrix-scheduler';
import { startGruReserveCapacityScheduler } from './services/gru-reserve-capacity-scheduler';
import { startOmnlHoLiquidityScheduler } from './services/omnl-ho-liquidity-scheduler';
import { startIso4217FxScheduler } from './services/iso4217-fx-refresh';
// Load smom-dbis-138 root .env first (single source); works from dist/ or src/
@@ -31,6 +32,7 @@ try {
const server = new ApiServer();
startRouteMatrixScheduler();
startGruReserveCapacityScheduler();
startOmnlHoLiquidityScheduler();
startIso4217FxScheduler();
// Start server

View File

@@ -66,6 +66,13 @@ export function getOmnlApiCatalog(): {
{ method: 'POST', path: '/omnl/terminal/alchemy/rpc', description: 'Alchemy JSON-RPC (read/estimate methods)', body: '{ network, method, params }', auth: 'OMNL_API_KEY' },
{ method: 'POST', path: '/omnl/terminal/alchemy/prices', description: 'Alchemy token prices by address', body: '{ network, addresses[] }', auth: 'OMNL_API_KEY' },
{ method: 'POST', path: '/omnl/terminal/alchemy/tx-prep', description: 'Prepare unsigned ERC-20 transfer calldata via Alchemy (dry-run default)', body: '{ network, from, to, tokenAddress, amount, dryRun }', auth: 'OMNL_API_KEY' },
{ method: 'POST', path: '/journal-entries', description: 'Create payout journal entry draft (IPSAS-validated pair)', auth: 'OMNL_API_KEY' },
{ method: 'GET', path: '/journal-entries/:id', description: 'Get journal entry by id', auth: 'OMNL_API_KEY' },
{ method: 'POST', path: '/journal-entries/:id/post', description: 'Post journal entry to ledger path', auth: 'OMNL_API_KEY' },
{ method: 'POST', path: '/journal-entries/:id/reverse', description: 'Reverse posted journal entry', auth: 'OMNL_API_KEY' },
{ method: 'POST', path: '/balance-reservations', description: 'Reserve balance before payout dispatch', auth: 'OMNL_API_KEY' },
{ method: 'GET', path: '/balance-reservations/:id', description: 'Get balance reservation', auth: 'OMNL_API_KEY' },
{ method: 'DELETE', path: '/balance-reservations/:id', description: 'Release balance reservation', auth: 'OMNL_API_KEY' },
],
};
}

View File

@@ -0,0 +1,47 @@
import { logger } from '../utils/logger';
import { buildHoLiquiditySnapshot } from './omnl-ho-liquidity-snapshot';
let intervalHandle: ReturnType<typeof setInterval> | null = null;
export function startOmnlHoLiquidityScheduler(): void {
const enabled =
String(process.env.OMNL_HO_LIQUIDITY_LIVE || '1').toLowerCase() === '1' ||
String(process.env.OMNL_HO_LIQUIDITY_LIVE || '').toLowerCase() === 'true';
if (!enabled) {
return;
}
const intervalMs = Math.max(
60_000,
parseInt(process.env.OMNL_HO_LIQUIDITY_INTERVAL_MS || '1800000', 10) || 1_800_000,
);
const tick = async () => {
try {
const snap = await buildHoLiquiditySnapshot({ forceRefresh: true });
logger.info('HO nostro liquidity cache refreshed', {
cashInBankUsd: snap.cashInBank.cashInBankUsd,
coverageRatio: snap.reserveCoverage.coverageRatio,
alerts: snap.alerts.length,
});
} catch (error) {
logger.warn('HO nostro liquidity refresh failed', {
error: error instanceof Error ? error.message : String(error),
});
}
};
void tick();
intervalHandle = setInterval(() => {
void tick();
}, intervalMs);
logger.info('HO nostro liquidity live scheduler enabled', { intervalMs });
}
export function stopOmnlHoLiquidityScheduler(): void {
if (intervalHandle) {
clearInterval(intervalHandle);
intervalHandle = null;
}
}

View File

@@ -1,8 +1,10 @@
import { getCachedHoReserveCoverageSnapshot, type HoReserveCoverageInfo } from './chain138-ho-reserve-coverage';
import {
evaluateSupplyNostroMatch,
getCachedCashInBankAuditSnapshot,
loadNostroPolicy,
type CashInBankAuditSnapshot,
type SupplyNostroMatchInfo,
} from './omnl-cash-in-bank-audit';
import { fineractConfigured } from './omnl-fineract-office-gl';
@@ -22,6 +24,7 @@ export interface HoLiquiditySnapshot {
fineractConfigured: boolean;
cashInBank: CashInBankAuditSnapshot;
reserveCoverage: HoReserveCoverageInfo;
aggregateSupplyNostroMatch?: SupplyNostroMatchInfo;
alerts: HoLiquidityAlert[];
auditMaxAgeMinutes: number;
}
@@ -60,6 +63,15 @@ export function evaluateHoLiquidityAlerts(input: {
severity: 'critical',
message: `Reserve coverage ${reserveCoverage.coverageRatio.toFixed(2)}× below 1.0× required`,
});
} else if (
reserveCoverage.coverageRatio > 0
&& reserveCoverage.coverageRatio < Number(process.env.OMNL_HO_RESERVE_COVERAGE_WARN_RATIO || 1.2)
) {
alerts.push({
code: 'RESERVE_COVERAGE_LOW',
severity: 'warning',
message: `Reserve coverage ${reserveCoverage.coverageRatio.toFixed(2)}× below ${Number(process.env.OMNL_HO_RESERVE_COVERAGE_WARN_RATIO || 1.2)}× policy target`,
});
}
const ageMin = (Date.now() - new Date(generatedAt).getTime()) / 60_000;
@@ -82,6 +94,27 @@ export function evaluateHoLiquidityAlerts(input: {
return alerts;
}
function patchCashInBankAuditWithAggregateMatch(
cashInBank: CashInBankAuditSnapshot,
aggregateMatch: SupplyNostroMatchInfo,
): CashInBankAuditSnapshot {
const supplyStatus = aggregateMatch.status === 'matched'
? 'aligned'
: aggregateMatch.status === 'break'
? 'breaks'
: cashInBank.audit.status;
return {
...cashInBank,
audit: {
...cashInBank.audit,
status: supplyStatus,
supplyNostroMatch: aggregateMatch.status,
breaksCount: aggregateMatch.status === 'break' ? 1 : 0,
},
supplyNostroMatch: aggregateMatch,
};
}
export async function buildHoLiquiditySnapshot(options?: {
officeId?: number;
forceRefresh?: boolean;
@@ -102,9 +135,16 @@ export async function buildHoLiquiditySnapshot(options?: {
getCachedHoReserveCoverageSnapshot({ officeId, forceRefresh }),
]);
const aggregateSupplyNostroMatch = evaluateSupplyNostroMatch({
totalSupplyUsd: reserveCoverage.issuedCStarUsd,
externalNostroCashUsd: cashInBank.cashInBankUsd,
headOfficeId: officeId,
});
const cashInBankEnriched = patchCashInBankAuditWithAggregateMatch(cashInBank, aggregateSupplyNostroMatch);
const generatedAt = new Date(
Math.max(
new Date(cashInBank.generatedAt).getTime(),
new Date(cashInBankEnriched.generatedAt).getTime(),
new Date(reserveCoverage.generatedAt).getTime(),
),
).toISOString();
@@ -115,9 +155,10 @@ export async function buildHoLiquiditySnapshot(options?: {
policyId: policy.policyId,
policyRef: policy.policyRef,
fineractConfigured: fineractConfigured(),
cashInBank,
cashInBank: cashInBankEnriched,
reserveCoverage,
alerts: evaluateHoLiquidityAlerts({ cashInBank, reserveCoverage, generatedAt }),
aggregateSupplyNostroMatch,
alerts: evaluateHoLiquidityAlerts({ cashInBank: cashInBankEnriched, reserveCoverage, generatedAt }),
auditMaxAgeMinutes: auditMaxAgeMinutes(),
};
}

View File

@@ -0,0 +1,38 @@
import {
createJournalEntry,
postJournalEntry,
createBalanceReservation,
releaseBalanceReservation,
clearPayoutJournalStores,
} from './omnl-payout-journal';
describe('omnl payout journal', () => {
beforeEach(() => {
clearPayoutJournalStores();
});
it('creates and posts journal entry', () => {
const je = createJournalEntry({
instructionId: 'PAY-1',
debitGlCode: '1000',
creditGlCode: '2000',
amount: '100.00',
currency: 'USD',
});
expect(je.status).toBe('DRAFT');
const posted = postJournalEntry(je.id);
expect(posted.status).toBe('POSTED');
});
it('creates and releases balance reservation', () => {
const r = createBalanceReservation({
instructionId: 'PAY-2',
amount: '50.00',
currency: 'USD',
accountRef: 'ACCT-1',
});
expect(r.status).toBe('HELD');
const released = releaseBalanceReservation(r.id);
expect(released.status).toBe('RELEASED');
});
});

View File

@@ -0,0 +1,157 @@
import { randomUUID } from 'crypto';
import {
loadIpsasRegistry,
validateJournalPairWithMatrix,
} from './omnl-ipsas-gl';
import { loadJournalMatrix } from './omnl-journal-matrix';
export interface JournalLine {
debitGlCode: string;
creditGlCode: string;
amount: string;
currency: string;
narrative?: string;
}
export interface JournalEntryRecord {
id: string;
instructionId?: string;
status: 'DRAFT' | 'POSTED' | 'REVERSED';
lines: JournalLine[];
ipsasRef?: string;
createdAt: string;
postedAt?: string;
}
export interface BalanceReservationRecord {
id: string;
instructionId: string;
amount: string;
currency: string;
accountRef: string;
status: 'HELD' | 'RELEASED' | 'CONSUMED';
createdAt: string;
releasedAt?: string;
}
const journalStore = new Map<string, JournalEntryRecord>();
const reservationStore = new Map<string, BalanceReservationRecord>();
function loadMatrixSafe() {
try {
return loadJournalMatrix();
} catch {
return null;
}
}
export function createJournalEntry(body: {
instructionId?: string;
debitGlCode?: string;
creditGlCode?: string;
amount?: string;
currency?: string;
narrative?: string;
lines?: JournalLine[];
}): JournalEntryRecord {
const lines: JournalLine[] =
body.lines ??
(body.debitGlCode && body.creditGlCode
? [
{
debitGlCode: body.debitGlCode,
creditGlCode: body.creditGlCode,
amount: String(body.amount ?? '0'),
currency: String(body.currency ?? 'USD'),
narrative: body.narrative,
},
]
: []);
if (!lines.length) {
throw new Error('journal entry requires lines[] or debitGlCode/creditGlCode');
}
const registry = loadIpsasRegistry();
const matrix = loadMatrixSafe();
for (const line of lines) {
const v = validateJournalPairWithMatrix(registry, matrix, line.debitGlCode, line.creditGlCode);
if (!v.valid) {
throw new Error(`IPSAS pair invalid: ${line.debitGlCode}/${line.creditGlCode}`);
}
}
const id = randomUUID();
const record: JournalEntryRecord = {
id,
instructionId: body.instructionId,
status: 'DRAFT',
lines,
ipsasRef: validateJournalPairWithMatrix(registry, matrix, lines[0].debitGlCode, lines[0].creditGlCode).ipsasRef,
createdAt: new Date().toISOString(),
};
journalStore.set(id, record);
return record;
}
export function postJournalEntry(id: string): JournalEntryRecord {
const record = journalStore.get(id);
if (!record) throw new Error('journal entry not found');
if (record.status === 'POSTED') return record;
record.status = 'POSTED';
record.postedAt = new Date().toISOString();
journalStore.set(id, record);
return record;
}
export function reverseJournalEntry(id: string): JournalEntryRecord {
const record = journalStore.get(id);
if (!record) throw new Error('journal entry not found');
record.status = 'REVERSED';
journalStore.set(id, record);
return record;
}
export function getJournalEntry(id: string): JournalEntryRecord | undefined {
return journalStore.get(id);
}
export function createBalanceReservation(body: {
instructionId: string;
amount: string;
currency: string;
accountRef?: string;
debtorId?: string;
}): BalanceReservationRecord {
const id = randomUUID();
const record: BalanceReservationRecord = {
id,
instructionId: body.instructionId,
amount: String(body.amount),
currency: String(body.currency).toUpperCase(),
accountRef: body.accountRef ?? body.debtorId ?? 'default',
status: 'HELD',
createdAt: new Date().toISOString(),
};
reservationStore.set(id, record);
return record;
}
export function releaseBalanceReservation(id: string): BalanceReservationRecord {
const record = reservationStore.get(id);
if (!record) throw new Error('reservation not found');
record.status = 'RELEASED';
record.releasedAt = new Date().toISOString();
reservationStore.set(id, record);
return record;
}
export function getBalanceReservation(id: string): BalanceReservationRecord | undefined {
return reservationStore.get(id);
}
/** Test helper */
export function clearPayoutJournalStores(): void {
journalStore.clear();
reservationStore.clear();
}