feat: add hybx omnl stack and gas pmm tooling
Some checks failed
CI/CD Pipeline / Lint and Format (push) Failing after 46s
CI/CD Pipeline / Terraform Validation (push) Failing after 35s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 37s
Deploy ChainID 138 / Deploy ChainID 138 (push) Failing after 1m50s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 2m19s
Validation / validate-genesis (push) Successful in 51s
Validation / validate-terraform (push) Failing after 39s
Validation / validate-kubernetes (push) Failing after 10s
CI/CD Pipeline / Solidity Contracts (push) Failing after 12m56s
Validation / validate-smart-contracts (push) Failing after 12s
CI/CD Pipeline / Security Scanning (push) Failing after 15m52s
Validation / validate-security (push) Failing after 10m59s
Validation / validate-documentation (push) Failing after 17s
Validate Token List / validate (push) Failing after 30s
OMNL reconcile anchor / Run omnl:reconcile and upload artifacts (push) Failing after 26s
Verify Deployment / Verify Deployment (push) Failing after 56s

This commit is contained in:
defiQUG
2026-04-24 12:56:40 -07:00
parent c3d4c786fa
commit f3d2961b97
80 changed files with 7192 additions and 2 deletions

View File

@@ -0,0 +1,37 @@
import type { Request, Response } from 'express';
import { omnlSensitiveRouteGuard } from './omnl-guards';
describe('omnlSensitiveRouteGuard', () => {
const oldKey = process.env.OMNL_API_KEY;
afterEach(() => {
if (oldKey === undefined) delete process.env.OMNL_API_KEY;
else process.env.OMNL_API_KEY = oldKey;
});
it('passes when OMNL_API_KEY unset', () => {
delete process.env.OMNL_API_KEY;
const next = jest.fn();
omnlSensitiveRouteGuard({ headers: {}, query: {} } as unknown as Request, {} as Response, next);
expect(next).toHaveBeenCalled();
});
it('401 when key set and no auth', () => {
process.env.OMNL_API_KEY = 'abc';
const json = jest.fn();
const status = jest.fn().mockReturnValue({ json });
omnlSensitiveRouteGuard({ headers: {}, query: {} } as unknown as Request, { status } as unknown as Response, jest.fn());
expect(status).toHaveBeenCalledWith(401);
});
it('passes with Bearer token', () => {
process.env.OMNL_API_KEY = 'abc';
const next = jest.fn();
omnlSensitiveRouteGuard(
{ headers: { authorization: 'Bearer abc' }, query: {} } as unknown as Request,
{} as Response,
next
);
expect(next).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,21 @@
import type { Request, Response, NextFunction } from 'express';
/**
* When `OMNL_API_KEY` is set, require `Authorization: Bearer <key>` or `?access_token=<key>`.
* If unset, all requests pass (backwards compatible).
*/
export function omnlSensitiveRouteGuard(req: Request, res: Response, next: NextFunction): void {
const key = process.env.OMNL_API_KEY?.trim();
if (!key) {
next();
return;
}
const auth = String(req.headers.authorization || '');
const bearer = auth.startsWith('Bearer ') ? auth.slice(7).trim() : '';
const q = String(req.query.access_token ?? '').trim();
if (bearer === key || q === key) {
next();
return;
}
res.status(401).json({ error: 'Unauthorized', hint: 'Set Authorization: Bearer or access_token for OMNL_API_KEY' });
}

View File

@@ -0,0 +1,222 @@
import { Router, Request, Response } from 'express';
import { omnlRateLimiter } from '../middleware/rate-limit';
import { omnlSensitiveRouteGuard } from '../middleware/omnl-guards';
import {
loadIpsasRegistry,
validateJournalPairWithMatrix,
fetchFineractGlAccounts,
compareRegistryToFineract,
checkFineractConnectivity,
} from '../../services/omnl-ipsas-gl';
import { loadJournalMatrix } from '../../services/omnl-journal-matrix';
import { fetchOmnlCompliance, fetchOmnlComplianceAggregated } from '../../services/omnl-compliance';
const router = Router();
router.use(omnlRateLimiter);
/**
* GET /omnl/ipsas/registry — full IPSAS GL registry (codes, pairs, monetary layer hints).
*/
router.get('/omnl/ipsas/registry', (_req: Request, res: Response) => {
try {
res.json(loadIpsasRegistry());
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
res.status(500).json({ error: msg });
}
});
/**
* GET /omnl/ipsas/matrix — journal matrix (T-001…T-008) for Fineract posting alignment.
*/
router.get('/omnl/ipsas/matrix', (_req: Request, res: Response) => {
try {
res.json(loadJournalMatrix());
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
res.status(500).json({ error: msg });
}
});
/**
* GET /omnl/ipsas/validate-pair?debitGlCode=&creditGlCode=
* Ensures Debit/Credit GL codes appear in IPSAS registry or approved journal matrix.
*/
/**
* POST /omnl/ipsas/validate-pairs — body `{ "pairs": [{ "debitGlCode", "creditGlCode" }, ...] }`
*/
router.post('/omnl/ipsas/validate-pairs', (req: Request, res: Response) => {
try {
const raw = (req.body as { pairs?: unknown })?.pairs;
if (!Array.isArray(raw) || raw.length === 0) {
res.status(400).json({ error: 'JSON body must include non-empty pairs: [{ debitGlCode, creditGlCode }, ...]' });
return;
}
const registry = loadIpsasRegistry();
let matrix = null;
try {
matrix = loadJournalMatrix();
} catch {
matrix = null;
}
const results = raw.map((p: unknown) => {
const row = p as { debitGlCode?: string; creditGlCode?: string };
const debitGlCode = String(row?.debitGlCode ?? '').trim();
const creditGlCode = String(row?.creditGlCode ?? '').trim();
if (!debitGlCode || !creditGlCode) {
return {
debitGlCode,
creditGlCode,
error: 'debitGlCode and creditGlCode required per pair',
};
}
const v = validateJournalPairWithMatrix(registry, matrix, debitGlCode, creditGlCode);
return {
debitGlCode,
creditGlCode,
ipsasCompliantPair: v.valid,
ipsasRef: v.ipsasRef,
memo: v.memo,
narrative: v.narrative,
source: v.source,
};
});
res.json({ generatedAt: new Date().toISOString(), count: results.length, results });
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
res.status(500).json({ error: msg });
}
});
router.get('/omnl/ipsas/validate-pair', (req: Request, res: Response) => {
try {
const debitGlCode = String(req.query.debitGlCode ?? '').trim();
const creditGlCode = String(req.query.creditGlCode ?? '').trim();
if (!debitGlCode || !creditGlCode) {
res.status(400).json({ error: 'debitGlCode and creditGlCode query params required' });
return;
}
const registry = loadIpsasRegistry();
let matrix = null;
try {
matrix = loadJournalMatrix();
} catch {
matrix = null;
}
const v = validateJournalPairWithMatrix(registry, matrix, debitGlCode, creditGlCode);
const debitKnown = registry.accounts.some((a) => a.glCode === debitGlCode);
const creditKnown = registry.accounts.some((a) => a.glCode === creditGlCode);
res.json({
debitGlCode,
creditGlCode,
ipsasCompliantPair: v.valid,
ipsasRef: v.ipsasRef,
memo: v.memo,
narrative: v.narrative,
source: v.source,
glCodesKnownInRegistry: { debit: debitKnown, credit: creditKnown },
});
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
res.status(500).json({ error: msg });
}
});
/**
* GET /omnl/ipsas/fineract-health — probe Fineract `/glaccounts?limit=1` (credentials from OMNL_FINERACT_*).
*/
router.get('/omnl/ipsas/fineract-health', async (_req: Request, res: Response) => {
try {
const r = await checkFineractConnectivity();
const configured = Boolean(
(process.env.OMNL_FINERACT_BASE_URL || '').trim() && (process.env.OMNL_FINERACT_PASSWORD || '').trim()
);
res.json({
generatedAt: new Date().toISOString(),
configured,
...r,
});
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
res.status(500).json({ error: msg });
}
});
/**
* GET /omnl/ipsas/fineract-compare — live Fineract /glaccounts vs IPSAS registry codes (requires OMNL_FINERACT_*).
*/
router.get('/omnl/ipsas/fineract-compare', omnlSensitiveRouteGuard, async (_req: Request, res: Response) => {
try {
const registry = loadIpsasRegistry();
const rows = await fetchFineractGlAccounts();
const cmp = compareRegistryToFineract(registry, rows);
res.json({
...cmp,
fineractAccountCount: rows.length,
ipsasCompliant: cmp.aligned,
});
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
res.status(503).json({ error: msg });
}
});
/**
* GET /omnl/ipsas/layer/:layer — monetary layer hint (m0_reserve | m1_liability | settlement | equity).
*/
router.get('/omnl/ipsas/layer/:layer', (req: Request, res: Response) => {
try {
const layer = req.params.layer;
const registry = loadIpsasRegistry();
const hint = registry.monetaryLayerHints[layer];
if (!hint) {
res.status(404).json({
error: 'Unknown layer',
known: Object.keys(registry.monetaryLayerHints),
});
return;
}
res.json({ layer, ...hint });
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
res.status(500).json({ error: msg });
}
});
/**
* GET /omnl/ipsas/compliance-context/:lineId?aggregated=1
* On-chain compliance snapshot + IPSAS GL hints for OMNL / Fineract alignment.
*/
router.get('/omnl/ipsas/compliance-context/:lineId', omnlSensitiveRouteGuard, async (req: Request, res: Response) => {
try {
const lineId = req.params.lineId as string;
const aggregated =
req.query.aggregated === '1' || String(req.query.aggregated || '').toLowerCase() === 'true';
const registry = loadIpsasRegistry();
let compliance: unknown;
if (aggregated) {
compliance = await fetchOmnlComplianceAggregated(lineId);
} else {
const addr = process.env.OMNL_COMPLIANCE_CORE_138;
if (!addr) {
res.status(503).json({ error: 'OMNL_COMPLIANCE_CORE_138 not set' });
return;
}
compliance = await fetchOmnlCompliance(138, lineId, addr);
}
res.json({
compliance,
ipsas: {
monetaryLayerHints: registry.monetaryLayerHints,
postingGuidance:
'Reserve movements (M0) map to GL 1050; M1 liabilities to 2000/2100; settlement to 1000. Validate every journal with GET /api/v1/omnl/ipsas/validate-pair before POST /journalentries.',
fineractDocs: 'https://omnl.hybxfinance.io/fineract-provider/swagger-ui/index.html',
},
});
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
res.status(500).json({ error: msg });
}
});
export default router;

View File

@@ -0,0 +1,324 @@
import { Router, Request, Response } from 'express';
import { Contract, JsonRpcProvider, type InterfaceAbi } from 'ethers';
import { omnlRateLimiter } from '../middleware/rate-limit';
import {
fetchOmnlCompliance,
fetchOmnlComplianceAggregated,
fetchLatestAttestation,
fetchBreakerStatus,
loadCrossChainLines,
loadCrossChainConfigPath,
} from '../../services/omnl-compliance';
import { computeOmnlReconcileAnchor } from '../../services/omnl-reconcile-anchor';
import { getOmnlIntegrationStatus } from '../../services/omnl-integration-status';
import { getOmnlApiCatalog } from '../../services/omnl-api-catalog';
import omnlOpenApi from '../../resources/omnl-openapi.json';
const router = Router();
router.use(omnlRateLimiter);
const REGISTRY_ABI: InterfaceAbi = [
'function allLineIds() view returns (bytes32[])',
'function getLine(bytes32 lineId) view returns (tuple(address tokenM0,address tokenM1,uint8 decimals,uint16 iso4217Numeric,bool isXAU,bool active))',
];
const COORDINATOR_ABI: InterfaceAbi = [
'function mirrorChainSelector() view returns (uint64)',
'function mirrorReceiver() view returns (address)',
'function feeToken() view returns (address)',
];
/**
* GET /omnl/openapi.json — OpenAPI 3.0 document for Swagger UI / codegen.
*/
router.get('/omnl/openapi.json', (_req: Request, res: Response) => {
res.type('application/json').json(omnlOpenApi as Record<string, unknown>);
});
/**
* GET /omnl/catalog — machine-readable list of OMNL HTTP integrations.
*/
router.get('/omnl/catalog', (_req: Request, res: Response) => {
res.json(getOmnlApiCatalog());
});
/**
* GET /omnl/integration-status — which env-backed integrations are present (no secrets).
*/
router.get('/omnl/integration-status', (_req: Request, res: Response) => {
res.json({
generatedAt: new Date().toISOString(),
...getOmnlIntegrationStatus(),
});
});
/**
* GET /omnl/reconcile-anchor — same SHA-256 as omnl-reconcile-report.mjs (IPSAS + matrix files).
*/
router.get('/omnl/reconcile-anchor', (_req: Request, res: Response) => {
const out = computeOmnlReconcileAnchor();
if (!out.ok) {
res.status(503).json(out);
return;
}
res.json(out);
});
/**
* GET /omnl/cross-chain-lines — logical lines from hybx-omnl-cross-chain-lines.json.
*/
router.get('/omnl/cross-chain-lines', (_req: Request, res: Response) => {
try {
res.json({
configPath: loadCrossChainConfigPath(),
lines: loadCrossChainLines(),
});
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
res.status(500).json({ error: msg });
}
});
/**
* GET /omnl/mirror-coordinator?chainId=138 — on-chain mirror destination (CCIP).
*/
/**
* GET /omnl/zk-verifier — env-configured verifier address (placeholder ok).
*/
router.get('/omnl/zk-verifier', (_req: Request, res: Response) => {
const a = process.env.OMNL_ZK_VERIFIER?.trim() || '';
res.json({
configured: Boolean(a),
address: a || null,
note: 'See docs/hybx-omnl/ZK_INTEGRATION.md for wiring a real verifier.',
});
});
router.get('/omnl/mirror-coordinator', async (req: Request, res: Response) => {
try {
const chainId = parseInt(String(req.query.chainId || '138'), 10);
const coord =
process.env[`OMNL_MIRROR_COORDINATOR_${chainId}`]?.trim() ||
process.env.OMNL_MIRROR_COORDINATOR?.trim();
if (!coord) {
res.status(503).json({
error: 'OMNL_MIRROR_COORDINATOR or OMNL_MIRROR_COORDINATOR_<chainId> not set',
chainId,
});
return;
}
const rpc = rpcUrl(chainId);
if (!rpc) {
res.status(503).json({ error: 'RPC not configured for chain', chainId });
return;
}
const c = new Contract(coord, COORDINATOR_ABI, new JsonRpcProvider(rpc, chainId));
const [mirrorChainSelector, mirrorReceiver, feeToken] = await Promise.all([
c.mirrorChainSelector(),
c.mirrorReceiver(),
c.feeToken(),
]);
res.json({
chainId,
coordinator: coord,
mirrorChainSelector: String(mirrorChainSelector),
mirrorReceiver,
feeToken,
});
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
res.status(500).json({ error: msg });
}
});
function addrForChain(chainId: number): string | undefined {
if (chainId === 138) {
return process.env.OMNL_COMPLIANCE_CORE_138;
}
if (chainId === 651940) {
return process.env.OMNL_COMPLIANCE_CORE_651940;
}
return process.env[`OMNL_COMPLIANCE_CORE_${chainId}`];
}
function rpcUrl(chainId: number): string | undefined {
if (chainId === 138) return process.env.RPC_URL_138 || process.env.RPC_URL;
if (chainId === 651940) return process.env.CHAIN_651940_RPC_URL || 'https://mainnet-rpc.alltra.global';
return process.env[`CHAIN_${chainId}_RPC_URL`];
}
/**
* GET /omnl/compliance/:lineId?chainId=138|651940
*/
router.get('/omnl/compliance/:lineId', async (req: Request, res: Response) => {
try {
const chainId = parseInt(String(req.query.chainId || '138'), 10);
const lineId = req.params.lineId as string;
const addr = addrForChain(chainId);
if (!addr) {
res.status(503).json({
error: 'OMNL_COMPLIANCE_CORE not configured for this chain',
chainId,
});
return;
}
const snap = await fetchOmnlCompliance(chainId, lineId, addr);
res.json(snap);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
res.status(500).json({ error: msg });
}
});
/**
* GET /omnl/compliance-aggregated/:lineId — summed supply across chains (see hybx-omnl-cross-chain-lines.json).
*/
router.get('/omnl/compliance-aggregated/:lineId', async (req: Request, res: Response) => {
try {
const lineId = req.params.lineId as string;
const snap = await fetchOmnlComplianceAggregated(lineId);
res.json(snap);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
res.status(500).json({ error: msg });
}
});
/**
* GET /omnl/instruments?chainId=138 — InstrumentRegistry.allLineIds + getLine
*/
router.get('/omnl/instruments', async (req: Request, res: Response) => {
try {
const chainId = parseInt(String(req.query.chainId || '138'), 10);
const reg = process.env[`OMNL_INSTRUMENT_REGISTRY_${chainId}`] || process.env.OMNL_INSTRUMENT_REGISTRY_138;
const rpc = rpcUrl(chainId);
if (!reg || !rpc) {
res.status(503).json({ error: 'OMNL_INSTRUMENT_REGISTRY_* and RPC required', chainId });
return;
}
const c = new Contract(reg, REGISTRY_ABI, new JsonRpcProvider(rpc, chainId));
const ids = (await c.allLineIds()) as string[];
const lines = await Promise.all(
ids.map(async (id: string) => {
const line = await c.getLine(id);
return { lineId: id, line };
})
);
res.json({ chainId, lines, crossChainConfigLines: loadCrossChainLines().length });
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
res.status(500).json({ error: msg });
}
});
/**
* GET /omnl/attestations/:lineId?chainId=138 — ReserveCommitmentStore.getCommitment
*/
router.get('/omnl/attestations/:lineId', async (req: Request, res: Response) => {
try {
const chainId = parseInt(String(req.query.chainId || '138'), 10);
const lineId = req.params.lineId as string;
const out = await fetchLatestAttestation(chainId, lineId);
res.json({ chainId, lineId, ...out });
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
res.status(500).json({ error: msg });
}
});
/**
* GET /omnl/breaker?chainId=138&lineId=0x...
*/
router.get('/omnl/breaker', async (req: Request, res: Response) => {
try {
const chainId = parseInt(String(req.query.chainId || '138'), 10);
const lineId = String(req.query.lineId || '');
if (!lineId || lineId === 'undefined') {
res.status(400).json({ error: 'lineId query parameter required' });
return;
}
const s = await fetchBreakerStatus(chainId, lineId);
res.json({ chainId, lineId, ...s });
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
res.status(500).json({ error: msg });
}
});
/**
* GET /omnl/mirror-status/:lineId — compare reserve version (and R) across 138 and 651940.
*/
router.get('/omnl/mirror-status/:lineId', async (req: Request, res: Response) => {
try {
const lineId = req.params.lineId as string;
const a138 = process.env.OMNL_RESERVE_STORE_138;
const a651940 = process.env.OMNL_RESERVE_STORE_651940;
const out: Record<string, unknown> = {
lineId,
configured138: Boolean(a138),
configured651940: Boolean(a651940),
};
if (!a138 || !a651940) {
res.status(503).json({
...out,
error: 'Set OMNL_RESERVE_STORE_138 and OMNL_RESERVE_STORE_651940 for mirror comparison',
});
return;
}
const [x138, x651] = await Promise.all([
fetchLatestAttestation(138, lineId),
fetchLatestAttestation(651940, lineId),
]);
out.attestation138 = x138;
out.attestation651940 = x651;
out.rSynced = x138.r === x651.r;
out.versionSynced = x138.version === x651.version;
out.merkleSynced = x138.merkleRoot === x651.merkleRoot;
out.inSync = out.rSynced && out.versionSynced;
res.json(out);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
res.status(500).json({ error: msg });
}
});
/**
* GET /omnl/health — requires OMNL_HEALTH_LINE_ID when comparing chains (no default zero line).
*/
router.get('/omnl/health', async (req: Request, res: Response) => {
const line = process.env.OMNL_HEALTH_LINE_ID;
if (!line) {
res.status(400).json({
error: 'Set OMNL_HEALTH_LINE_ID to a registered bytes32 line id (0x-prefixed)',
});
return;
}
const a138 = process.env.OMNL_COMPLIANCE_CORE_138;
const a651940 = process.env.OMNL_COMPLIANCE_CORE_651940;
const out: Record<string, unknown> = {
lineId: line,
configured138: Boolean(a138),
configured651940: Boolean(a651940),
};
try {
if (a138) {
out.compliance138 = await fetchOmnlCompliance(138, line, a138);
}
if (a651940) {
out.compliance651940 = await fetchOmnlCompliance(651940, line, a651940);
}
if (a138 && a651940 && out.compliance138 && out.compliance651940) {
const x = out.compliance138 as { r: string; reportingCompliant: boolean };
const y = out.compliance651940 as { r: string; reportingCompliant: boolean };
out.rSynced = x.r === y.r;
out.reportingSynced =
x.reportingCompliant === y.reportingCompliant && x.r === y.r;
}
res.json(out);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
res.status(500).json({ ...out, error: msg });
}
});
export default router;

View File

@@ -0,0 +1,86 @@
import { Contract, JsonRpcProvider, type InterfaceAbi } from 'ethers';
import { logger } from '../utils/logger';
import { emitOmnlWebhook } from '../services/omnl-webhooks';
import { loadLastProcessedBlock, saveLastProcessedBlock } from './omnl-poller-state';
const RESERVE_ABI: InterfaceAbi = [
'event ReserveCommitted(bytes32 indexed lineId,uint256 R,uint256 validUntil,bytes32 evidenceHash,bytes32 merkleRoot,uint256 version,address indexed by)',
];
/**
* Polls ReserveCommitted on Chain 138 / 651940 for ops visibility.
* Persists last processed block per chain (`OMNL_POLLER_STATE_PATH` or `.omnl-poller-state.json`).
*/
export class OmnlEventPoller {
private timers: NodeJS.Timeout[] = [];
start(): void {
const interval = parseInt(process.env.OMNL_EVENT_POLL_INTERVAL_MS || '60000', 10);
const chains = (process.env.OMNL_EVENT_POLL_CHAINS || '138,651940').split(',').map((s) => parseInt(s.trim(), 10));
for (const chainId of chains) {
const addr =
chainId === 138
? process.env.OMNL_RESERVE_STORE_138
: chainId === 651940
? process.env.OMNL_RESERVE_STORE_651940
: process.env[`OMNL_RESERVE_STORE_${chainId}`];
const rpc =
chainId === 138
? process.env.RPC_URL_138 || process.env.RPC_URL
: process.env.CHAIN_651940_RPC_URL || 'https://mainnet-rpc.alltra.global';
if (!addr || !rpc) {
logger.warn(`OMNL event poll skipped chain ${chainId}: missing store or RPC`);
continue;
}
const persisted = loadLastProcessedBlock(chainId);
let lastBlock = persisted ?? 0;
const provider = new JsonRpcProvider(rpc, chainId);
const c = new Contract(addr, RESERVE_ABI, provider);
const tick = async () => {
try {
const head = await provider.getBlockNumber();
const from = lastBlock === 0 ? Math.max(0, head - 99) : lastBlock + 1;
if (from > head) return;
const evs = await c.queryFilter('ReserveCommitted', from, head);
for (const ev of evs) {
const deliveryId = `${chainId}-${ev.blockNumber}-${ev.index}`;
logger.info('OMNL ReserveCommitted', { chainId, logIndex: ev.index, deliveryId });
const args = (ev as { args?: { lineId?: string; version?: bigint; R?: bigint } }).args;
void emitOmnlWebhook({
event: 'ReserveCommitted',
chainId,
lineId: args?.lineId,
deliveryId,
payload: {
blockNumber: ev.blockNumber,
transactionHash: ev.transactionHash,
version: args?.version?.toString(),
r: args?.R?.toString(),
},
ts: new Date().toISOString(),
});
}
lastBlock = head;
saveLastProcessedBlock(chainId, head);
} catch (e) {
logger.error(`OMNL poll error chain ${chainId}`, e);
}
};
void tick();
const t = setInterval(() => void tick(), interval);
this.timers.push(t);
}
}
stop(): void {
for (const t of this.timers) {
clearInterval(t);
}
this.timers = [];
}
}

View File

@@ -0,0 +1,41 @@
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
import { dirname, join } from 'path';
export type OmnlPollerStateFile = {
chains: Record<string, number>;
};
function statePath(): string {
return process.env.OMNL_POLLER_STATE_PATH?.trim() || join(process.cwd(), '.omnl-poller-state.json');
}
export function loadLastProcessedBlock(chainId: number): number | null {
const p = statePath();
if (!existsSync(p)) return null;
try {
const j = JSON.parse(readFileSync(p, 'utf8')) as OmnlPollerStateFile;
const v = j.chains?.[String(chainId)];
return typeof v === 'number' && v >= 0 ? v : null;
} catch {
return null;
}
}
export function saveLastProcessedBlock(chainId: number, block: number): void {
const p = statePath();
let j: OmnlPollerStateFile = { chains: {} };
if (existsSync(p)) {
try {
const parsed = JSON.parse(readFileSync(p, 'utf8')) as OmnlPollerStateFile;
j = { chains: { ...parsed.chains } };
} catch {
j = { chains: {} };
}
}
j.chains[String(chainId)] = block;
const dir = dirname(p);
if (dir && dir !== '.') {
mkdirSync(dir, { recursive: true });
}
writeFileSync(p, JSON.stringify(j, null, 2) + '\n', 'utf8');
}

View File

@@ -0,0 +1,276 @@
{
"openapi": "3.0.3",
"info": {
"title": "HYBX OMNL API",
"description": "HYBX OMNL endpoints on the token-aggregation service (Chain 138 / ALL Mainnet 651940, IPSAS, Fineract alignment). Optional auth: when `OMNL_API_KEY` is set, use `Authorization: Bearer <key>` or query `access_token`.",
"version": "1.2.0"
},
"servers": [{ "url": "/api/v1", "description": "Token aggregation v1" }],
"tags": [
{ "name": "omnl-discovery", "description": "Catalog and probes" },
{ "name": "omnl-chain", "description": "On-chain compliance and instruments" },
{ "name": "omnl-ipsas", "description": "IPSAS GL and Fineract" }
],
"paths": {
"/omnl/openapi.json": {
"get": {
"tags": ["omnl-discovery"],
"summary": "OpenAPI 3 document (this file)",
"operationId": "getOmnlOpenApi",
"responses": { "200": { "description": "OpenAPI 3 JSON" } }
}
},
"/omnl/catalog": {
"get": {
"tags": ["omnl-discovery"],
"summary": "Human/agent catalog of OMNL routes",
"operationId": "getOmnlCatalog",
"responses": { "200": { "description": "Catalog object" } }
}
},
"/omnl/integration-status": {
"get": {
"tags": ["omnl-discovery"],
"summary": "Which env-backed integrations are configured",
"operationId": "getOmnlIntegrationStatus",
"responses": { "200": { "description": "Status flags" } }
}
},
"/omnl/reconcile-anchor": {
"get": {
"tags": ["omnl-discovery"],
"summary": "SHA-256 anchor for IPSAS registry + journal matrix files",
"operationId": "getOmnlReconcileAnchor",
"responses": { "200": { "description": "Anchor payload" }, "503": { "description": "Config files missing" } }
}
},
"/omnl/cross-chain-lines": {
"get": {
"tags": ["omnl-discovery"],
"summary": "hybx-omnl-cross-chain-lines.json",
"operationId": "getOmnlCrossChainLines",
"responses": { "200": { "description": "lines + configPath" } }
}
},
"/omnl/zk-verifier": {
"get": {
"tags": ["omnl-discovery"],
"summary": "ZK verifier address from env",
"operationId": "getOmnlZkVerifier",
"responses": { "200": { "description": "address or null" } }
}
},
"/omnl/mirror-coordinator": {
"get": {
"tags": ["omnl-chain"],
"summary": "Read OMNLMirrorCoordinator mirror destination",
"operationId": "getOmnlMirrorCoordinator",
"parameters": [
{
"name": "chainId",
"in": "query",
"schema": { "type": "integer", "default": 138 },
"description": "Chain where coordinator is deployed"
}
],
"responses": { "200": { "description": "selector, receiver, feeToken" }, "503": { "description": "Not configured" } }
}
},
"/omnl/compliance/{lineId}": {
"get": {
"tags": ["omnl-chain"],
"summary": "ComplianceCore snapshot",
"operationId": "getOmnlCompliance",
"parameters": [
{ "name": "lineId", "in": "path", "required": true, "schema": { "type": "string" } },
{
"name": "chainId",
"in": "query",
"schema": { "type": "integer", "default": 138 }
}
],
"responses": { "200": { "description": "Compliance snapshot" } }
}
},
"/omnl/compliance-aggregated/{lineId}": {
"get": {
"tags": ["omnl-chain"],
"summary": "Aggregated cross-chain compliance",
"operationId": "getOmnlComplianceAggregated",
"parameters": [{ "name": "lineId", "in": "path", "required": true, "schema": { "type": "string" } }],
"responses": { "200": { "description": "Aggregated snapshot" } }
}
},
"/omnl/instruments": {
"get": {
"tags": ["omnl-chain"],
"summary": "InstrumentRegistry lines",
"operationId": "getOmnlInstruments",
"parameters": [
{ "name": "chainId", "in": "query", "schema": { "type": "integer", "default": 138 } }
],
"responses": { "200": { "description": "lines" } }
}
},
"/omnl/attestations/{lineId}": {
"get": {
"tags": ["omnl-chain"],
"summary": "Reserve commitment",
"operationId": "getOmnlAttestations",
"parameters": [
{ "name": "lineId", "in": "path", "required": true, "schema": { "type": "string" } },
{ "name": "chainId", "in": "query", "schema": { "type": "integer", "default": 138 } }
],
"responses": { "200": { "description": "Commitment tuple" } }
}
},
"/omnl/breaker": {
"get": {
"tags": ["omnl-chain"],
"summary": "Circuit breaker status",
"operationId": "getOmnlBreaker",
"parameters": [
{ "name": "chainId", "in": "query", "schema": { "type": "integer", "default": 138 } },
{ "name": "lineId", "in": "query", "required": true, "schema": { "type": "string" } }
],
"responses": { "200": { "description": "Breaker state" } }
}
},
"/omnl/mirror-status/{lineId}": {
"get": {
"tags": ["omnl-chain"],
"summary": "Compare reserve attestation across chains",
"operationId": "getOmnlMirrorStatus",
"parameters": [{ "name": "lineId", "in": "path", "required": true, "schema": { "type": "string" } }],
"responses": { "200": { "description": "Comparison" } }
}
},
"/omnl/health": {
"get": {
"tags": ["omnl-chain"],
"summary": "Multi-chain compliance health (requires OMNL_HEALTH_LINE_ID)",
"operationId": "getOmnlHealth",
"responses": { "200": { "description": "Health payload" }, "400": { "description": "Missing line id env" } }
}
},
"/omnl/ipsas/registry": {
"get": {
"tags": ["omnl-ipsas"],
"summary": "IPSAS GL registry JSON",
"operationId": "getOmnlIpsasRegistry",
"responses": { "200": { "description": "Registry" } }
}
},
"/omnl/ipsas/matrix": {
"get": {
"tags": ["omnl-ipsas"],
"summary": "Journal matrix JSON",
"operationId": "getOmnlIpsasMatrix",
"responses": { "200": { "description": "Matrix" } }
}
},
"/omnl/ipsas/validate-pair": {
"get": {
"tags": ["omnl-ipsas"],
"summary": "Validate one GL pair",
"operationId": "getOmnlIpsasValidatePair",
"parameters": [
{ "name": "debitGlCode", "in": "query", "required": true, "schema": { "type": "string" } },
{ "name": "creditGlCode", "in": "query", "required": true, "schema": { "type": "string" } }
],
"responses": { "200": { "description": "Validation result" } }
}
},
"/omnl/ipsas/validate-pairs": {
"post": {
"tags": ["omnl-ipsas"],
"summary": "Batch validate GL pairs",
"operationId": "postOmnlIpsasValidatePairs",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["pairs"],
"properties": {
"pairs": {
"type": "array",
"items": {
"type": "object",
"required": ["debitGlCode", "creditGlCode"],
"properties": {
"debitGlCode": { "type": "string" },
"creditGlCode": { "type": "string" }
}
}
}
}
}
}
}
},
"responses": { "200": { "description": "Batch results" }, "400": { "description": "Invalid body" } }
}
},
"/omnl/ipsas/fineract-health": {
"get": {
"tags": ["omnl-ipsas"],
"summary": "Fineract API reachability probe",
"operationId": "getOmnlIpsasFineractHealth",
"responses": { "200": { "description": "Health payload" } }
}
},
"/omnl/ipsas/fineract-compare": {
"get": {
"tags": ["omnl-ipsas"],
"summary": "Registry vs live Fineract GL accounts",
"operationId": "getOmnlIpsasFineractCompare",
"security": [{ "OmnlApiKey": [] }, { "AccessTokenQuery": [] }],
"responses": { "200": { "description": "Compare result" }, "401": { "description": "When OMNL_API_KEY set" } }
}
},
"/omnl/ipsas/layer/{layer}": {
"get": {
"tags": ["omnl-ipsas"],
"summary": "Monetary layer hint",
"operationId": "getOmnlIpsasLayer",
"parameters": [{ "name": "layer", "in": "path", "required": true, "schema": { "type": "string" } }],
"responses": { "200": { "description": "Layer hint" }, "404": { "description": "Unknown layer" } }
}
},
"/omnl/ipsas/compliance-context/{lineId}": {
"get": {
"tags": ["omnl-ipsas"],
"summary": "Compliance + IPSAS posting guidance",
"operationId": "getOmnlIpsasComplianceContext",
"parameters": [
{ "name": "lineId", "in": "path", "required": true, "schema": { "type": "string" } },
{
"name": "aggregated",
"in": "query",
"schema": { "type": "string", "enum": ["1", "true"] },
"description": "Set to 1 for aggregated view"
}
],
"security": [{ "OmnlApiKey": [] }, { "AccessTokenQuery": [] }],
"responses": { "200": { "description": "Context" }, "401": { "description": "When OMNL_API_KEY set" } }
}
}
},
"components": {
"securitySchemes": {
"OmnlApiKey": {
"type": "http",
"scheme": "bearer",
"description": "When OMNL_API_KEY is set: value equals that secret (Bearer)."
},
"AccessTokenQuery": {
"type": "apiKey",
"in": "query",
"name": "access_token",
"description": "Alternate to Bearer when OMNL_API_KEY is set."
}
}
}
}

View File

@@ -0,0 +1,50 @@
/** Static catalog for integration tools (OpenAPI-style discovery without Swagger). */
export function getOmnlApiCatalog(): {
service: string;
basePath: string;
version: string;
endpoints: Array<{
method: string;
path: string;
description: string;
query?: string[];
auth?: string;
body?: string;
}>;
} {
return {
service: 'HYBX OMNL',
basePath: '/api/v1',
version: '1.2.0',
endpoints: [
{ method: 'GET', path: '/omnl/openapi.json', description: 'OpenAPI 3.0 JSON (Swagger-compatible)', auth: 'none' },
{ method: 'GET', path: '/omnl/catalog', description: 'This catalog (machine-readable)', auth: 'none' },
{ method: 'GET', path: '/omnl/integration-status', description: 'Which env-backed integrations are configured', auth: 'none' },
{ method: 'GET', path: '/omnl/reconcile-anchor', description: 'SHA-256 of canonical IPSAS registry + journal matrix JSON', auth: 'none' },
{ method: 'GET', path: '/omnl/cross-chain-lines', description: 'hybx-omnl-cross-chain-lines.json lines[]', auth: 'none' },
{ method: 'GET', path: '/omnl/zk-verifier', description: 'ZK verifier address from OMNL_ZK_VERIFIER', auth: 'none' },
{ method: 'GET', path: '/omnl/mirror-coordinator', description: 'Read on-chain mirror destination', query: ['chainId'], auth: 'none' },
{ method: 'GET', path: '/omnl/compliance/:lineId', description: 'ComplianceCore snapshot', query: ['chainId'], auth: 'none' },
{ method: 'GET', path: '/omnl/compliance-aggregated/:lineId', description: 'Aggregated supply + policy', auth: 'none' },
{ method: 'GET', path: '/omnl/instruments', description: 'InstrumentRegistry lines', query: ['chainId'], auth: 'none' },
{ method: 'GET', path: '/omnl/attestations/:lineId', description: 'ReserveCommitmentStore commitment', query: ['chainId'], auth: 'none' },
{ method: 'GET', path: '/omnl/breaker', description: 'Circuit breaker status', query: ['chainId', 'lineId'], auth: 'none' },
{ method: 'GET', path: '/omnl/mirror-status/:lineId', description: 'Compare reserve attestation 138 vs 651940', auth: 'none' },
{ method: 'GET', path: '/omnl/health', description: 'Multi-chain compliance probe', query: [], auth: 'none' },
{ method: 'GET', path: '/omnl/ipsas/registry', description: 'IPSAS GL registry JSON', auth: 'none' },
{ method: 'GET', path: '/omnl/ipsas/matrix', description: 'Journal matrix JSON', auth: 'none' },
{ method: 'GET', path: '/omnl/ipsas/validate-pair', description: 'Validate one debit/credit pair', query: ['debitGlCode', 'creditGlCode'], auth: 'none' },
{
method: 'POST',
path: '/omnl/ipsas/validate-pairs',
description: 'Batch validate journal pairs',
body: '{ pairs: [{ debitGlCode, creditGlCode }] }',
auth: 'none',
},
{ method: 'GET', path: '/omnl/ipsas/fineract-health', description: 'Fineract API reachability (glaccounts probe)', auth: 'none' },
{ method: 'GET', path: '/omnl/ipsas/fineract-compare', description: 'Registry vs live GL accounts', auth: 'OMNL_API_KEY if set' },
{ method: 'GET', path: '/omnl/ipsas/layer/:layer', description: 'Monetary layer hint', auth: 'none' },
{ method: 'GET', path: '/omnl/ipsas/compliance-context/:lineId', description: 'Compliance + IPSAS guidance', query: ['aggregated'], auth: 'OMNL_API_KEY if set' },
],
};
}

View File

@@ -0,0 +1,274 @@
import { Contract, JsonRpcProvider, type InterfaceAbi } from 'ethers';
import { readFileSync, existsSync } from 'fs';
import { resolve } from 'path';
import { isCompliant, maxM1ForM0, minReservesForM0 } from './omnl-policy-math';
const COMPLIANCE_ABI: InterfaceAbi = [
'function getCompliance(bytes32 lineId) view returns (uint256 s0, uint256 s1, uint256 r, uint256 validUntil, bytes32 evidenceHash, bytes32 merkleRoot, uint256 minR, uint256 maxS1, bool m0Ok, bool m1Ok, bool attestationStale, bool policyOk, bool operational, bool reportingCompliant)',
];
const BREAKER_ABI: InterfaceAbi = ['function isLineOperational(bytes32 lineId) view returns (bool)'];
const ERC20_ABI: InterfaceAbi = ['function totalSupply() view returns (uint256)'];
const RESERVE_ABI: InterfaceAbi = [
'function getCommitment(bytes32 lineId) view returns (tuple(uint256 R,uint256 validUntil,bytes32 evidenceHash,bytes32 merkleRoot,uint256 version))',
];
export interface OmnlComplianceSnapshot {
chainId: number;
lineId: string;
s0: string;
s1: string;
r: string;
validUntil: string;
evidenceHash: string;
merkleRoot: string;
minR: string;
maxS1: string;
m0Ok: boolean;
m1Ok: boolean;
attestationStale: boolean;
policyOk: boolean;
operational: boolean;
reportingCompliant: boolean;
}
export interface OmnlCrossChainLineEntry {
lineId: string;
chains: Record<string, { tokenM0: string; tokenM1: string }>;
}
export interface OmnlAggregatedSnapshot extends OmnlComplianceSnapshot {
aggregated: true;
s0ByChain: Record<string, string>;
s1ByChain: Record<string, string>;
}
function rpcForChain(chainId: number): string | undefined {
if (chainId === 138) {
return process.env.RPC_URL_138 || process.env.RPC_URL;
}
if (chainId === 651940) {
return process.env.CHAIN_651940_RPC_URL || 'https://mainnet-rpc.alltra.global';
}
return process.env[`CHAIN_${chainId}_RPC_URL`];
}
function parseTuple(res: unknown): OmnlComplianceSnapshot & { chainId: number; lineId: string } {
const t = res as unknown as {
s0: bigint;
s1: bigint;
r: bigint;
validUntil: bigint;
evidenceHash: string;
merkleRoot: string;
minR: bigint;
maxS1: bigint;
m0Ok: boolean;
m1Ok: boolean;
attestationStale: boolean;
policyOk: boolean;
operational: boolean;
reportingCompliant: boolean;
};
return {
chainId: 0,
lineId: '',
s0: t.s0.toString(),
s1: t.s1.toString(),
r: t.r.toString(),
validUntil: t.validUntil.toString(),
evidenceHash: t.evidenceHash,
merkleRoot: t.merkleRoot,
minR: t.minR.toString(),
maxS1: t.maxS1.toString(),
m0Ok: t.m0Ok,
m1Ok: t.m1Ok,
attestationStale: t.attestationStale,
policyOk: t.policyOk,
operational: t.operational,
reportingCompliant: t.reportingCompliant,
};
}
export async function fetchOmnlCompliance(
chainId: number,
lineIdHex: string,
complianceAddress: string
): Promise<OmnlComplianceSnapshot> {
const url = rpcForChain(chainId);
if (!url) {
throw new Error(`No RPC configured for chain ${chainId}`);
}
const provider = new JsonRpcProvider(url, chainId);
const c = new Contract(complianceAddress, COMPLIANCE_ABI, provider);
const lineId = lineIdHex.startsWith('0x') ? lineIdHex : `0x${lineIdHex}`;
const res = await c.getCompliance(lineId);
const out = parseTuple(res);
out.chainId = chainId;
out.lineId = lineId;
return out;
}
export function loadCrossChainConfigPath(): string {
return (
process.env.OMNL_CROSS_CHAIN_CONFIG ||
resolve(__dirname, '../../../../config/hybx-omnl-cross-chain-lines.json')
);
}
export function loadCrossChainLines(): OmnlCrossChainLineEntry[] {
const p = loadCrossChainConfigPath();
if (!existsSync(p)) {
return [];
}
const raw = JSON.parse(readFileSync(p, 'utf8')) as { lines?: OmnlCrossChainLineEntry[] };
return raw.lines ?? [];
}
async function readTotalSupply(rpcUrl: string, chainId: number, token: string): Promise<bigint> {
const provider = new JsonRpcProvider(rpcUrl, chainId);
const t = new Contract(token, ERC20_ABI, provider);
return (await t.totalSupply()) as bigint;
}
/**
* Sum M0/M1 supply across chains for a logical line; use R/TTL from primary (138) reserve or compliance.
*/
export async function fetchOmnlComplianceAggregated(lineIdHex: string): Promise<OmnlAggregatedSnapshot> {
const addr138 = process.env.OMNL_COMPLIANCE_CORE_138;
if (!addr138) {
throw new Error('OMNL_COMPLIANCE_CORE_138 required for aggregated view');
}
const lineId = lineIdHex.startsWith('0x') ? lineIdHex : `0x${lineIdHex}`;
const lines = loadCrossChainLines();
const entry = lines.find((l) => l.lineId.toLowerCase() === lineId.toLowerCase());
if (!entry) {
throw new Error(`lineId not found in cross-chain config: ${lineId}`);
}
const primary = await fetchOmnlCompliance(138, lineId, addr138);
const rpc138 = rpcForChain(138)!;
const rpc651 = rpcForChain(651940)!;
let s0Sum = 0n;
let s1Sum = 0n;
const s0ByChain: Record<string, string> = {};
const s1ByChain: Record<string, string> = {};
for (const [cid, pair] of Object.entries(entry.chains)) {
const chainNum = parseInt(cid, 10);
const rpc = chainNum === 138 ? rpc138 : chainNum === 651940 ? rpc651 : rpcForChain(chainNum);
if (!rpc) continue;
const a0 = await readTotalSupply(rpc, chainNum, pair.tokenM0);
const a1 = await readTotalSupply(rpc, chainNum, pair.tokenM1);
s0Sum += a0;
s1Sum += a1;
s0ByChain[cid] = a0.toString();
s1ByChain[cid] = a1.toString();
}
const r = BigInt(primary.r);
const validUntil = BigInt(primary.validUntil);
const now = BigInt(Math.floor(Date.now() / 1000));
const attestationStale = now > validUntil;
const minR = minReservesForM0(s0Sum);
const maxS1 = maxM1ForM0(s0Sum);
const m0Ok = r >= minR;
const m1Ok = s1Sum <= maxS1;
const policyOk = isCompliant(s0Sum, s1Sum, r);
let operational = primary.operational;
const br651 = process.env.OMNL_CIRCUIT_BREAKER_651940;
if (br651) {
const p = new JsonRpcProvider(rpc651, 651940);
const b = new Contract(br651, BREAKER_ABI, p);
operational = operational && (await b.isLineOperational(lineId));
}
const reportingCompliant = policyOk && !attestationStale && operational;
return {
aggregated: true,
chainId: 138,
lineId,
s0: s0Sum.toString(),
s1: s1Sum.toString(),
r: primary.r,
validUntil: primary.validUntil,
evidenceHash: primary.evidenceHash,
merkleRoot: primary.merkleRoot,
minR: minR.toString(),
maxS1: maxS1.toString(),
m0Ok,
m1Ok,
attestationStale,
policyOk,
operational,
reportingCompliant,
s0ByChain,
s1ByChain,
};
}
/** Latest commitment tuple from ReserveCommitmentStore (when OMNL_RESERVE_STORE_* set). */
export async function fetchLatestAttestation(
chainId: number,
lineIdHex: string
): Promise<{ r: string; validUntil: string; evidenceHash: string; merkleRoot: string; version: string }> {
const addr =
chainId === 138
? process.env.OMNL_RESERVE_STORE_138
: chainId === 651940
? process.env.OMNL_RESERVE_STORE_651940
: process.env[`OMNL_RESERVE_STORE_${chainId}`];
if (!addr) {
throw new Error(`OMNL_RESERVE_STORE_${chainId} not configured`);
}
const url = rpcForChain(chainId);
if (!url) throw new Error(`No RPC for ${chainId}`);
const lineId = lineIdHex.startsWith('0x') ? lineIdHex : `0x${lineIdHex}`;
const c = new Contract(addr, RESERVE_ABI, new JsonRpcProvider(url, chainId));
const t = await c.getCommitment(lineId);
const x = t as unknown as { R: bigint; validUntil: bigint; evidenceHash: string; merkleRoot: string; version: bigint };
return {
r: x.R.toString(),
validUntil: x.validUntil.toString(),
evidenceHash: x.evidenceHash,
merkleRoot: x.merkleRoot,
version: x.version.toString(),
};
}
export async function fetchBreakerStatus(chainId: number, lineIdHex: string): Promise<{
globalPaused: boolean;
linePaused: boolean;
operational: boolean;
}> {
const addr =
chainId === 138
? process.env.OMNL_CIRCUIT_BREAKER_138
: chainId === 651940
? process.env.OMNL_CIRCUIT_BREAKER_651940
: process.env[`OMNL_CIRCUIT_BREAKER_${chainId}`];
if (!addr) {
throw new Error(`OMNL_CIRCUIT_BREAKER_${chainId} not configured`);
}
const url = rpcForChain(chainId);
if (!url) throw new Error(`No RPC for ${chainId}`);
const lineId = lineIdHex.startsWith('0x') ? lineIdHex : `0x${lineIdHex}`;
const abi: InterfaceAbi = [
'function globalPaused() view returns (bool)',
'function linePaused(bytes32) view returns (bool)',
'function isLineOperational(bytes32 lineId) view returns (bool)',
];
const b = new Contract(addr, abi, new JsonRpcProvider(url, chainId));
const [globalPaused, linePaused, operational] = await Promise.all([
b.globalPaused(),
b.linePaused(lineId),
b.isLineOperational(lineId),
]);
return { globalPaused, linePaused, operational };
}

View File

@@ -0,0 +1,31 @@
import { loadIpsasRegistryPath } from './omnl-ipsas-gl';
import { loadJournalMatrixPath } from './omnl-journal-matrix';
/** Non-secret snapshot of which OMNL integrations are configured (for probes and dashboards). */
export function getOmnlIntegrationStatus(): Record<string, unknown> {
return {
fineract: Boolean(
(process.env.OMNL_FINERACT_BASE_URL || '').trim() && (process.env.OMNL_FINERACT_PASSWORD || '').trim()
),
complianceCore138: Boolean((process.env.OMNL_COMPLIANCE_CORE_138 || '').trim()),
complianceCore651940: Boolean((process.env.OMNL_COMPLIANCE_CORE_651940 || '').trim()),
instrumentRegistry138: Boolean(
(process.env.OMNL_INSTRUMENT_REGISTRY_138 || process.env.OMNL_INSTRUMENT_REGISTRY || '').trim()
),
reserveStore138: Boolean((process.env.OMNL_RESERVE_STORE_138 || '').trim()),
reserveStore651940: Boolean((process.env.OMNL_RESERVE_STORE_651940 || '').trim()),
circuitBreaker138: Boolean((process.env.OMNL_CIRCUIT_BREAKER_138 || '').trim()),
circuitBreaker651940: Boolean((process.env.OMNL_CIRCUIT_BREAKER_651940 || '').trim()),
mirrorCoordinator138: Boolean(
(process.env.OMNL_MIRROR_COORDINATOR_138 || process.env.OMNL_MIRROR_COORDINATOR || '').trim()
),
zkVerifierConfigured: Boolean((process.env.OMNL_ZK_VERIFIER || '').trim()),
webhooksConfigured: Boolean((process.env.OMNL_WEBHOOK_URLS || '').trim()),
eventPollerEnabled: ['1', 'true', 'yes', 'on'].includes(
(process.env.ENABLE_OMNL_EVENT_POLLER || '').trim().toLowerCase()
),
apiKeyRequiredRoutes: Boolean((process.env.OMNL_API_KEY || '').trim()),
ipsasRegistryPath: loadIpsasRegistryPath(),
journalMatrixPath: loadJournalMatrixPath(),
};
}

View File

@@ -0,0 +1,210 @@
import { readFileSync, existsSync } from 'fs';
import { resolve } from 'path';
import axios, { type AxiosRequestConfig } from 'axios';
import type { JournalMatrixFile } from './omnl-journal-matrix';
export interface IpsasGlAccount {
glCode: string;
name: string;
fineractType: string;
usage: string;
ipsasStandards: string[];
roles: string[];
}
export interface AllowedJournalPair {
debitGlCode: string;
creditGlCode: string;
ipsasRef: string;
memo?: string;
}
export interface IpsasGlRegistry {
version: string;
currencyCode: string;
accounts: IpsasGlAccount[];
allowedJournalPairs: AllowedJournalPair[];
monetaryLayerHints: Record<
string,
{ primaryGlCodes: string[]; ipsasNarrative: string }
>;
}
export function loadIpsasRegistryPath(): string {
return (
process.env.OMNL_IPSAS_GL_REGISTRY ||
resolve(__dirname, '../../../../config/omnl-ipsas-gl-registry.json')
);
}
export function loadIpsasRegistry(): IpsasGlRegistry {
const p = loadIpsasRegistryPath();
if (!existsSync(p)) {
throw new Error(`IPSAS GL registry not found: ${p}`);
}
return JSON.parse(readFileSync(p, 'utf8')) as IpsasGlRegistry;
}
export function validateJournalPair(
registry: IpsasGlRegistry,
debitGlCode: string,
creditGlCode: string
): { valid: boolean; ipsasRef?: string; memo?: string } {
const d = debitGlCode.trim();
const c = creditGlCode.trim();
const hit = registry.allowedJournalPairs.find(
(p) => p.debitGlCode === d && p.creditGlCode === c
);
if (hit) {
return { valid: true, ipsasRef: hit.ipsasRef, memo: hit.memo };
}
return { valid: false };
}
/** Prefer static registry; fall back to full journal matrix (T-001…T-008). */
export function validateJournalPairWithMatrix(
registry: IpsasGlRegistry,
matrix: JournalMatrixFile | null,
debitGlCode: string,
creditGlCode: string
): { valid: boolean; ipsasRef?: string; memo?: string; narrative?: string; source: 'registry' | 'matrix' | 'none' } {
const r0 = validateJournalPair(registry, debitGlCode, creditGlCode);
if (r0.valid) {
return { ...r0, source: 'registry' };
}
const d = debitGlCode.trim();
const c = creditGlCode.trim();
if (matrix) {
const hit = matrix.entries.find((e) => e.debitGlCode === d && e.creditGlCode === c);
if (hit) {
return {
valid: true,
ipsasRef: hit.ipsasRef,
memo: hit.memo,
narrative: hit.narrative,
source: 'matrix',
};
}
}
return { valid: false, source: 'none' };
}
export function assertGlCodeKnown(registry: IpsasGlRegistry, glCode: string): boolean {
return registry.accounts.some((a) => a.glCode === glCode.trim());
}
export interface FineractGlAccountRow {
id: number;
glCode?: string;
name?: string;
}
/** GET /glaccounts from Fineract (OMNL). Paginates until exhausted; handles top-level array or pageItems. */
export async function fetchFineractGlAccounts(): Promise<FineractGlAccountRow[]> {
const base = (process.env.OMNL_FINERACT_BASE_URL || '').replace(/\/$/, '');
const tenant = process.env.OMNL_FINERACT_TENANT || 'omnl';
const user =
process.env.OMNL_FINERACT_USER?.trim() ||
process.env.OMNL_FINERACT_USERNAME?.trim() ||
'app.omnl';
const pass = process.env.OMNL_FINERACT_PASSWORD || '';
if (!base || !pass) {
throw new Error('OMNL_FINERACT_BASE_URL and OMNL_FINERACT_PASSWORD required for Fineract GL sync');
}
const auth = Buffer.from(`${user}:${pass}`).toString('base64');
const headers: Record<string, string> = {
Authorization: `Basic ${auth}`,
'Fineract-Platform-TenantId': tenant,
'Content-Type': 'application/json',
};
const limit = parseInt(process.env.OMNL_FINERACT_GL_PAGE_LIMIT || '200', 10);
const out: FineractGlAccountRow[] = [];
let offset = 0;
for (;;) {
const url = `${base}/glaccounts?limit=${limit}&offset=${offset}`;
const cfg: AxiosRequestConfig = { headers, timeout: 120000 };
const { data } = await axios.get<
FineractGlAccountRow[] | { pageItems?: FineractGlAccountRow[]; totalFilteredRecords?: number }
>(url, cfg);
let batch: FineractGlAccountRow[] = [];
if (Array.isArray(data)) {
batch = data;
} else {
batch = data.pageItems ?? [];
}
out.push(...batch);
if (batch.length < limit) {
break;
}
offset += limit;
}
return out;
}
export function compareRegistryToFineract(
registry: IpsasGlRegistry,
fineractRows: FineractGlAccountRow[]
): {
registryCodes: string[];
fineractCodes: string[];
missingInFineract: string[];
aligned: boolean;
} {
const registryCodes = registry.accounts.map((a) => a.glCode).sort();
const fineractCodes = [
...new Set(
fineractRows
.map((r) => (r.glCode != null ? String(r.glCode).trim() : ''))
.filter(Boolean)
),
].sort();
const fs = new Set(fineractCodes);
const missingInFineract = registryCodes.filter((c) => !fs.has(c));
return {
registryCodes,
fineractCodes,
missingInFineract,
aligned: missingInFineract.length === 0,
};
}
/** Lightweight Fineract reachability (GET `/glaccounts?limit=1`). */
export async function checkFineractConnectivity(): Promise<{
ok: boolean;
statusCode?: number;
message: string;
}> {
const base = (process.env.OMNL_FINERACT_BASE_URL || '').replace(/\/$/, '');
const tenant = process.env.OMNL_FINERACT_TENANT || 'omnl';
const user =
process.env.OMNL_FINERACT_USER?.trim() ||
process.env.OMNL_FINERACT_USERNAME?.trim() ||
'app.omnl';
const pass = process.env.OMNL_FINERACT_PASSWORD || '';
if (!base || !pass) {
return { ok: false, message: 'OMNL_FINERACT_BASE_URL and OMNL_FINERACT_PASSWORD required' };
}
const auth = Buffer.from(`${user}:${pass}`).toString('base64');
const headers: Record<string, string> = {
Authorization: `Basic ${auth}`,
'Fineract-Platform-TenantId': tenant,
'Content-Type': 'application/json',
};
try {
const url = `${base}/glaccounts?limit=1&offset=0`;
const { status } = await axios.get(url, {
headers,
timeout: 20000,
validateStatus: () => true,
});
const ok = status >= 200 && status < 500;
return {
ok,
statusCode: status,
message: ok ? 'Fineract API reachable' : `Unexpected HTTP ${status}`,
};
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
return { ok: false, message: msg };
}
}

View File

@@ -0,0 +1,43 @@
import { readFileSync, existsSync } from 'fs';
import { resolve } from 'path';
export interface JournalMatrixEntry {
memo: string;
officeId: number;
debitGlCode: string;
creditGlCode: string;
amount: number;
narrative: string;
ipsasRef: string;
}
export interface JournalMatrixFile {
entries: JournalMatrixEntry[];
description?: string;
source?: string;
currencyCode?: string;
}
export function loadJournalMatrixPath(): string {
return (
process.env.OMNL_JOURNAL_MATRIX_PATH ||
resolve(__dirname, '../../../../config/omnl-journal-matrix.json')
);
}
export function loadJournalMatrix(): JournalMatrixFile {
const p = loadJournalMatrixPath();
if (!existsSync(p)) {
throw new Error(`Journal matrix not found: ${p}. Set OMNL_JOURNAL_MATRIX_PATH.`);
}
return JSON.parse(readFileSync(p, 'utf8')) as JournalMatrixFile;
}
/** Union of IPSAS registry pairs and matrix-derived pairs for validation. */
export function matrixPairSet(matrix: JournalMatrixFile): Set<string> {
const s = new Set<string>();
for (const e of matrix.entries) {
s.add(`${e.debitGlCode}|${e.creditGlCode}`);
}
return s;
}

View File

@@ -0,0 +1,19 @@
/**
* Deterministic GRU policy (must match PolicyMath.sol / HYBX_OMNL_POLICY_SPEC).
*/
const M0_NUM = 12n;
const M0_DEN = 10n;
const M1_CAP = 5n;
export function minReservesForM0(s0: bigint): bigint {
if (s0 === 0n) return 0n;
return (s0 * M0_NUM + M0_DEN - 1n) / M0_DEN;
}
export function maxM1ForM0(s0: bigint): bigint {
return s0 * M1_CAP;
}
export function isCompliant(s0: bigint, s1: bigint, r: bigint): boolean {
return r >= minReservesForM0(s0) && s1 <= maxM1ForM0(s0);
}

View File

@@ -0,0 +1,11 @@
import { computeOmnlReconcileAnchor } from './omnl-reconcile-anchor';
describe('computeOmnlReconcileAnchor', () => {
it('returns ok and 64-char hex sha256 when config files exist', () => {
const r = computeOmnlReconcileAnchor();
expect(r.ok).toBe(true);
expect(r.sha256).toMatch(/^[a-f0-9]{64}$/);
expect(r.registryPath).toContain('omnl-ipsas-gl-registry');
expect(r.matrixPath).toContain('omnl-journal-matrix');
});
});

View File

@@ -0,0 +1,60 @@
import { readFileSync, existsSync } from 'fs';
import { createHash } from 'crypto';
import { loadIpsasRegistryPath } from './omnl-ipsas-gl';
import { loadJournalMatrixPath } from './omnl-journal-matrix';
export interface OmnlReconcileAnchorResult {
ok: boolean;
sha256: string;
registryPath: string;
matrixPath: string;
generatedAt: string;
error?: string;
}
/** Same canonical SHA-256 as `scripts/omnl-reconcile-report.mjs` (IPSAS registry + journal matrix JSON). */
export function computeOmnlReconcileAnchor(): OmnlReconcileAnchorResult {
const generatedAt = new Date().toISOString();
try {
const registryPath = loadIpsasRegistryPath();
const matrixPath = loadJournalMatrixPath();
if (!existsSync(registryPath)) {
return {
ok: false,
sha256: '',
registryPath,
matrixPath,
generatedAt,
error: `IPSAS registry file missing: ${registryPath}`,
};
}
if (!existsSync(matrixPath)) {
return {
ok: false,
sha256: '',
registryPath,
matrixPath,
generatedAt,
error: `Journal matrix file missing: ${matrixPath}`,
};
}
const reg = readFileSync(registryPath, 'utf8');
const matrix = readFileSync(matrixPath, 'utf8');
const canonical = JSON.stringify({
registry: JSON.parse(reg) as unknown,
matrix: JSON.parse(matrix) as unknown,
});
const sha256 = createHash('sha256').update(canonical).digest('hex');
return { ok: true, sha256, registryPath, matrixPath, generatedAt };
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
return {
ok: false,
sha256: '',
registryPath: '',
matrixPath: '',
generatedAt,
error: msg,
};
}
}

View File

@@ -0,0 +1,18 @@
import { signOmnlWebhookBody, verifyOmnlWebhookSignature } from './omnl-webhooks';
describe('omnl-webhooks', () => {
it('signOmnlWebhookBody produces sha256= prefix', () => {
const raw = '{"event":"ReserveCommitted","ts":"2026-01-01T00:00:00.000Z"}';
const sig = signOmnlWebhookBody(raw, 'secret');
expect(sig.startsWith('sha256=')).toBe(true);
expect(sig.length).toBeGreaterThan(10);
});
it('verifyOmnlWebhookSignature accepts matching signature', () => {
const raw = '{"a":1}';
const secret = 'k';
const sig = signOmnlWebhookBody(raw, secret);
expect(verifyOmnlWebhookSignature(raw, sig, secret)).toBe(true);
expect(verifyOmnlWebhookSignature(raw, 'sha256=deadbeef', secret)).toBe(false);
});
});

View File

@@ -0,0 +1,69 @@
import axios from 'axios';
import { createHmac, timingSafeEqual } from 'crypto';
import { logger } from '../utils/logger';
export type OmnlWebhookPayload = {
event: string;
chainId?: number;
lineId?: string;
/** Idempotency: `${chainId}-${blockNumber}-${logIndex}` */
deliveryId?: string;
payload: Record<string, unknown>;
ts: string;
};
function parseWebhookUrls(): string[] {
const raw = process.env.OMNL_WEBHOOK_URLS || '';
return raw
.split(',')
.map((s) => s.trim())
.filter(Boolean);
}
/** HMAC-SHA256(hex) of UTF-8 body; header value `sha256=<hex>`. */
export function signOmnlWebhookBody(rawBodyUtf8: string, secret: string): string {
const h = createHmac('sha256', secret).update(rawBodyUtf8, 'utf8').digest('hex');
return `sha256=${h}`;
}
/**
* Verify `X-OMNL-Signature` from `signOmnlWebhookBody` (timing-safe).
*/
export function verifyOmnlWebhookSignature(rawBodyUtf8: string, signatureHeader: string | undefined, secret: string): boolean {
if (!signatureHeader || !secret) return false;
const expected = signOmnlWebhookBody(rawBodyUtf8, secret);
try {
const a = Buffer.from(signatureHeader.trim(), 'utf8');
const b = Buffer.from(expected, 'utf8');
if (a.length !== b.length) return false;
return timingSafeEqual(a, b);
} catch {
return false;
}
}
/**
* POST JSON to each configured OMNL_WEBHOOK_URLS (comma-separated). Failures are logged, not thrown.
* Sends `X-OMNL-Signature: sha256=<hmac>` when OMNL_WEBHOOK_SECRET is set (HMAC of exact JSON body bytes).
*/
export async function emitOmnlWebhook(body: OmnlWebhookPayload): Promise<void> {
const urls = parseWebhookUrls();
if (urls.length === 0) {
return;
}
const secret = process.env.OMNL_WEBHOOK_SECRET || '';
const rawBody = JSON.stringify(body);
const headers: Record<string, string> = {
'Content-Type': 'application/json; charset=utf-8',
...(secret ? { 'X-OMNL-Signature': signOmnlWebhookBody(rawBody, secret) } : {}),
};
await Promise.allSettled(
urls.map(async (url) => {
try {
await axios.post(url, rawBody, { timeout: 15000, headers });
} catch (e) {
logger.warn(`OMNL webhook failed ${url}`, { error: e instanceof Error ? e.message : String(e) });
}
})
);
}