feat: OMNL Bank online production — 128 chains, Central Bank UI, settlement stack
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m22s
CI/CD Pipeline / Security Scanning (push) Successful in 2m30s
CI/CD Pipeline / Lint and Format (push) Failing after 44s
CI/CD Pipeline / Terraform Validation (push) Failing after 27s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 31s
Deploy ChainID 138 / Deploy ChainID 138 (push) Failing after 56s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 29s
Validation / validate-genesis (push) Successful in 34s
Validation / validate-terraform (push) Failing after 39s
Validation / validate-kubernetes (push) Failing after 14s
Validation / validate-smart-contracts (push) Failing after 20s
Validation / validate-security (push) Failing after 1m19s
Validation / validate-documentation (push) Failing after 27s
Verify Deployment / Verify Deployment (push) Failing after 1m13s

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-28 08:44:42 -07:00
parent 2b1a3ba6af
commit 5dd67e2b1d
113 changed files with 13223 additions and 98 deletions

View File

@@ -0,0 +1,53 @@
import axios from 'axios';
function authHeaders(): Record<string, 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() ||
'ali_hospitallers_tenant';
const pass = process.env.OMNL_FINERACT_PASSWORD || '';
if (!base || !pass) throw new Error('Fineract credentials required');
const auth = Buffer.from(`${user}:${pass}`).toString('base64');
return {
Authorization: `Basic ${auth}`,
'Fineract-Platform-TenantId': tenant,
'Content-Type': 'application/json',
};
}
export async function postJournalEntry(entry: {
officeId: number;
transactionDate: string;
referenceNumber: string;
comments: string;
debits: { glAccountId: number; amount: number }[];
credits: { glAccountId: number; amount: number }[];
}): Promise<{ resourceId: number }> {
const base = (process.env.OMNL_FINERACT_BASE_URL || '').replace(/\/$/, '');
const { data } = await axios.post(`${base}/journalentries`, entry, {
headers: authHeaders(),
timeout: 60000,
});
return { resourceId: data.resourceId ?? data.entityId ?? 0 };
}
export async function fetchGlBalances(officeId: number): Promise<Record<string, string>> {
const base = (process.env.OMNL_FINERACT_BASE_URL || '').replace(/\/$/, '');
try {
const { data } = await axios.get(`${base}/runreports/General Ledger Report`, {
headers: authHeaders(),
params: { R_officeId: officeId, genericResultSet: false },
timeout: 60000,
});
const out: Record<string, string> = {};
const rows = Array.isArray(data) ? data : (data as { data?: unknown[] })?.data ?? [];
for (const row of rows as { glCode?: string; balance?: number }[]) {
if (row.glCode) out[row.glCode] = String(row.balance ?? 0);
}
return out;
} catch {
return {};
}
}

View File

@@ -0,0 +1,60 @@
import axios from 'axios';
import { loadHybxConfig } from '@dbis/integration-foundation';
/** Production HYBX rail client for real settlement dispatch */
export class HybxProductionRail {
private readonly baseUrl: string;
private readonly apiKey: string;
private readonly clientSecret: string;
constructor() {
const cfg = loadHybxConfig({ testMode: false });
if (cfg.environment === 'production' && process.env.SETTLEMENT_ALLOW_HYBX_PRODUCTION !== '1') {
throw new Error('SETTLEMENT_ALLOW_HYBX_PRODUCTION=1 required for live HYBX rails');
}
this.baseUrl = cfg.baseUrl.replace(/\/$/, '');
this.apiKey = cfg.apiKey;
this.clientSecret = cfg.clientSecret;
}
async dispatchPayment(payload: {
idempotencyKey: string;
amount: string;
currency: string;
creditorIban: string;
creditorBic?: string;
beneficiaryName: string;
remittanceInfo: string;
rail: string;
}): Promise<{ paymentId: string; status: string }> {
const sidecar = process.env.HYBX_MIFOS_FINERACT_SIDECAR_URL?.replace(/\/$/, '');
const url = sidecar ? `${sidecar}/v1/rtgs/payments` : `${this.baseUrl}/v1/payments`;
const { data } = await axios.post(
url,
{
externalId: payload.idempotencyKey,
amount: payload.amount,
currency: payload.currency,
beneficiary: {
iban: payload.creditorIban,
bic: payload.creditorBic,
name: payload.beneficiaryName,
},
remittanceInformation: payload.remittanceInfo,
rail: payload.rail,
},
{
headers: {
'Content-Type': 'application/json',
'X-API-Key': this.apiKey,
Authorization: `Bearer ${this.clientSecret}`,
},
timeout: 120000,
},
);
return {
paymentId: String(data.paymentId ?? data.id ?? payload.idempotencyKey),
status: String(data.status ?? 'DISPATCHED'),
};
}
}

View File

@@ -0,0 +1,59 @@
import axios from 'axios';
export async function archiveIso20022(payload: {
messageType: string;
direction: 'INBOUND' | 'OUTBOUND';
raw: string;
settlementRef: string;
}): Promise<{ messageId: string }> {
const base = (process.env.TOKEN_AGGREGATION_URL || 'http://localhost:3000').replace(/\/$/, '');
const key = process.env.OMNL_API_KEY || '';
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (key) headers.Authorization = `Bearer ${key}`;
const { data } = await axios.post(
`${base}/api/v1/omnl/iso20022/messages`,
{
messageType: payload.messageType,
direction: payload.direction,
payload: payload.raw,
metadata: { settlementRef: payload.settlementRef },
},
{ headers, timeout: 60000 },
);
return { messageId: String(data.messageId ?? data.id ?? payload.settlementRef) };
}
export async function requestTokenMint(payload: {
lineId: string;
amount: string;
recipient: string;
settlementRef: string;
dryRun: boolean;
symbol?: string;
tokenAddress?: string;
}): Promise<{ txHash?: string; status: string }> {
const base = (process.env.TOKEN_AGGREGATION_URL || 'http://localhost:3000').replace(/\/$/, '');
const key = process.env.OMNL_API_KEY || '';
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (key) headers.Authorization = `Bearer ${key}`;
try {
const { data } = await axios.post(
`${base}/api/v1/omnl/settlement/token-load`,
payload,
{ headers, timeout: 120000 },
);
return {
txHash: data.txHash,
status: String(data.status ?? (payload.dryRun ? 'DRY_RUN' : 'SUBMITTED')),
};
} catch (err) {
if (axios.isAxiosError(err) && err.response?.status === 404) {
return {
status: payload.dryRun ? 'DRY_RUN' : 'PENDING_CHAIN_ENDPOINT',
};
}
throw err;
}
}

View File

@@ -0,0 +1,56 @@
import axios from 'axios';
export async function forwardSwiftToListener(raw: string): Promise<{ accepted: boolean }> {
const url = (process.env.SWIFT_LISTENER_URL || 'http://192.168.11.114:8788').replace(/\/$/, '');
const token = process.env.SWIFT_LISTENER_WEBHOOK_TOKEN || '';
const headers: Record<string, string> = { 'Content-Type': 'text/plain' };
if (token) headers.Authorization = `Bearer ${token}`;
await axios.post(`${url}/inbound`, raw, { headers, timeout: 60000 });
return { accepted: true };
}
export function buildMt103Stub(fields: {
senderRef: string;
currency: string;
amount: string;
valueDate: string;
orderingCustomer: string;
beneficiary: string;
iban: string;
bic?: string;
remittance: string;
}): string {
const bicLine = fields.bic ? `:57A:${fields.bic}\n` : '';
return `{1:F01OMNLUS33XXXX0000000000}{2:I103BANKUS33XXXXN}{4:
:20:${fields.senderRef}
:23B:CRED
:32A:${fields.valueDate.replace(/-/g, '').slice(2)}${fields.currency}${fields.amount.replace('.', ',')}
:50K:${fields.orderingCustomer}
:59:/${fields.iban}
${fields.beneficiary}
${bicLine}:70:${fields.remittance}
:71A:OUR
-}`;
}
export function buildMt102Stub(fields: {
senderRef: string;
currency: string;
amount: string;
valueDate: string;
orderingInstitution: string;
beneficiary: string;
iban: string;
remittance: string;
}): string {
return `{1:F01OMNLUS33XXXX0000000000}{2:I102BANKUS33XXXXN}{4:
:20:${fields.senderRef}
:21:NONREF
:32B:${fields.currency}${fields.amount.replace('.', ',')}
:50A:${fields.orderingInstitution}
:59:/${fields.iban}
${fields.beneficiary}
:70:${fields.remittance}
-}`;
}

View File

@@ -0,0 +1,271 @@
import { Router, type Request, type Response } from 'express';
import type { ExternalTransferRequest, TradeFinanceInstrument, TokenLoadRequest, TransferRequest } from '@dbis/settlement-core';
import { loadOmnlChainRegistry, getActiveChains } from '@dbis/settlement-core';
import { loadConfig, loadOfficeProfile, settlementOfficeId } from '../../config';
import {
getMoneySupply,
getSettlementStatus,
processExternalTransfer,
} from '../../workflows/external-transfer';
import { processTokenLoad } from '../../workflows/token-load';
import { processTransfer } from '../../workflows/transfer';import { bindOffice24 } from '../../workflows/office-scope';
import { settlementStore } from '../../store/settlement-store';
import { processSwiftInbound } from '../../workflows/swift-inbound';
function requireApiKey(req: Request, res: Response): boolean {
const cfg = loadConfig();
if (!cfg.production.requireApiKey) return true;
const key = req.header('Authorization')?.replace(/^Bearer\s+/i, '') || req.header('X-API-Key');
if (key && key === process.env.OMNL_API_KEY) return true;
res.status(401).json({ error: 'Unauthorized' });
return false;
}
export function createSettlementRouter(): Router {
const router = Router();
const officeId = settlementOfficeId();
const profile = loadOfficeProfile();
router.get('/health', (_req, res) => {
let chainStats = { total: 0, active: 0 };
try {
const reg = loadOmnlChainRegistry();
chainStats = { total: reg.chains.length, active: getActiveChains().length };
} catch {
/* optional */
}
res.json({
service: 'omnl-settlement-middleware',
status: 'ok',
mode: process.env.SETTLEMENT_MIDDLEWARE_CONFIG?.includes('production') ? 'production' : 'standard',
role: 'OMNL central-bank settlement orchestration',
chainSupport: { max: 128, ...chainStats },
settlementOffice: {
officeId: profile.officeId,
externalId: profile.externalId,
name: profile.name,
},
enforceSingleOffice: loadConfig().enforceSingleOffice ?? false,
});
});
router.get('/office', (_req, res) => {
res.json({
...profile,
locked: true,
storePath: `data/settlement-store/office-${officeId}`,
});
});
router.get('/money-supply', async (req, res) => {
if (!requireApiKey(req, res)) return;
try {
res.json(await getMoneySupply(officeId));
} catch (e) {
res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
}
});
router.get('/money-supply/:officeIdParam', async (req, res) => {
if (!requireApiKey(req, res)) return;
try {
const requested = parseInt(req.params.officeIdParam, 10);
res.json(await getMoneySupply(settlementOfficeId(requested)));
} catch (e) {
res.status(400).json({ error: e instanceof Error ? e.message : String(e) });
}
});
router.get('/settlements', (req, res) => {
if (!requireApiKey(req, res)) return;
res.json({
officeId,
items: settlementStore.list(Number(req.query.limit ?? 50)),
});
});
router.get('/settlements/:id', (req, res) => {
if (!requireApiKey(req, res)) return;
const rec = getSettlementStatus(req.params.id);
if (!rec) {
res.status(404).json({ error: 'Not found' });
return;
}
res.json(rec);
});
router.post('/external-transfer', async (req, res) => {
if (!requireApiKey(req, res)) return;
try {
const merged = bindOffice24(req.body as ExternalTransferRequest);
const record = await processExternalTransfer(merged);
res.status(record.phase === 'FAILED' ? 422 : 200).json(record);
} catch (e) {
res.status(e instanceof Error && e.message.includes('locked') ? 403 : 500).json({
error: e instanceof Error ? e.message : String(e),
});
}
});
router.post('/swift/inbound', async (req, res) => {
if (!requireApiKey(req, res)) return;
try {
const raw = typeof req.body === 'string' ? req.body : req.body?.raw;
if (!raw) {
res.status(400).json({ error: 'raw SWIFT payload required' });
return;
}
const record = await processSwiftInbound(String(raw));
res.json(record);
} catch (e) {
res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
}
});
router.post('/trade-finance/:instrument', async (req, res) => {
if (!requireApiKey(req, res)) return;
const instrument = req.params.instrument.toUpperCase() as TradeFinanceInstrument;
if (!['SBLC', 'DLC', 'BG', 'LS'].includes(instrument)) {
res.status(400).json({ error: 'Invalid instrument' });
return;
}
try {
const body = req.body as ExternalTransferRequest;
const merged = bindOffice24({
...body,
tradeFinance: {
instrument,
reference: body.tradeFinance?.reference ?? body.idempotencyKey,
expiryDate: body.tradeFinance?.expiryDate,
},
rail: 'SWIFT',
moneyLayers: body.moneyLayers ?? ['M1', 'M2'],
});
const record = await processExternalTransfer(merged);
res.status(record.phase === 'FAILED' ? 422 : 200).json(record);
} catch (e) {
res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
}
});
router.post('/convert/fiat-to-crypto', async (req, res) => {
if (!requireApiKey(req, res)) return;
try {
const body = req.body as ExternalTransferRequest;
const merged = bindOffice24({
...body,
rail: body.rail ?? 'INTERNAL',
convertToCrypto: {
enabled: true,
targetChainId: body.convertToCrypto?.targetChainId ?? 138,
tokenLineId: body.convertToCrypto?.tokenLineId ?? `${body.currency ?? 'USD'}-M2`,
recipientAddress: body.convertToCrypto?.recipientAddress ?? '',
},
moneyLayers: ['M2'],
});
const record = await processExternalTransfer(merged);
res.status(record.phase === 'FAILED' ? 422 : 200).json(record);
} catch (e) {
res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
}
});
router.post('/token-load', async (req, res) => {
if (!requireApiKey(req, res)) return;
try {
const body = req.body as TokenLoadRequest;
if (!body.idempotencyKey || !body.lineId || !body.amount || !body.recipientAddress) {
res.status(400).json({ error: 'idempotencyKey, lineId, amount, recipientAddress required' });
return;
}
const record = await processTokenLoad({
...body,
officeId: settlementOfficeId(body.officeId),
});
res.status(record.phase === 'FAILED' ? 422 : 200).json(record);
} catch (e) {
res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
}
});
router.post('/transfer/internal', async (req, res) => {
if (!requireApiKey(req, res)) return;
try {
const body = req.body as TransferRequest;
if (!body.idempotencyKey || !body.tokenAddress || !body.amount || !body.recipientAddress) {
res.status(400).json({ error: 'idempotencyKey, tokenAddress, amount, recipientAddress required' });
return;
}
const record = await processTransfer({
...body,
officeId: settlementOfficeId(body.officeId),
rail: 'INTERNAL',
moneyLayers: body.moneyLayers ?? ['M2'],
});
res.status(record.phase === 'FAILED' ? 422 : 200).json(record);
} catch (e) {
res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
}
});
router.post('/transfer/external', async (req, res) => {
if (!requireApiKey(req, res)) return;
try {
const body = req.body as TransferRequest;
if (!body.creditorIban) {
res.status(400).json({ error: 'creditorIban required for external transfer' });
return;
}
const record = await processTransfer({
...body,
officeId: settlementOfficeId(body.officeId),
rail: body.rail === 'RTGS' ? 'RTGS' : 'SWIFT',
moneyLayers: body.moneyLayers ?? ['M2'],
});
res.status(record.phase === 'FAILED' ? 422 : 200).json(record);
} catch (e) {
res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
}
});
router.get('/tokens/m2', (_req, res) => {
try {
const fs = require('fs') as typeof import('fs');
const path = require('path') as typeof import('path');
const p =
process.env.OMNL_M2_TOKEN_REGISTRY ||
path.resolve(__dirname, '../../../../../config/omnl-m2-token-registry.v1.json');
const registry = JSON.parse(fs.readFileSync(p, 'utf8'));
res.json(registry);
} catch (e) {
res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
}
});
router.get('/chains', (_req, res) => {
try {
const registry = loadOmnlChainRegistry();
res.json({
brand: registry.brand,
maxCapacity: registry.maxCapacity,
primaryChainId: registry.primaryChainId,
mirrorChainId: registry.mirrorChainId,
settlementOfficeId: registry.settlementOfficeId,
stats: registry.stats,
chains: registry.chains,
});
} catch (e) {
res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
}
});
router.get('/chains/active', (_req, res) => {
try {
res.json({ count: getActiveChains().length, chains: getActiveChains() });
} catch (e) {
res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
}
});
return router;
}

View File

@@ -0,0 +1,112 @@
import fs from 'fs';
import path from 'path';
export type OfficeSettlementProfile = {
version: string;
officeId: number;
externalId: string;
name: string;
fineractTenant: string;
tenantUser: string;
adminUser: string;
operatorUser: string;
settlement: {
defaultCurrency: string;
defaultLineId: string;
moneyLayers: string[];
glM0: string;
glM1: string;
glM2: string;
glSettlement: string;
glTradeFinanceContingent: string;
};
rails: MiddlewareConfig['rails'];
omnlLegalName: string;
omnlLei: string;
};
export type MiddlewareConfig = {
version: string;
omnlLegalName: string;
defaultOfficeId: number;
enforceSingleOffice?: boolean;
settlementOffice?: {
officeId: number;
externalId: string;
name: string;
profilePath: string;
};
defaultCurrency: string;
rails: {
swift: { enabled: boolean; listenerUrl: string };
hybx: { enabled: boolean; sidecarUrl: string };
chain138: { enabled: boolean; rpcEnv: string; defaultLineId: string };
};
moneySupply: {
glM0: string;
glM1: string;
glM2: string;
glSettlement: string;
};
production: {
requireApiKey: boolean;
allowHybxProduction: boolean;
allowChainMintExecute: boolean;
};
};
let cached: MiddlewareConfig | null = null;
let cachedOffice: OfficeSettlementProfile | null = null;
function repoConfigPath(relative: string): string {
return path.resolve(__dirname, '../../../', relative.replace(/^\//, ''));
}
export function loadConfig(): MiddlewareConfig {
if (cached) return cached;
const configPath =
process.env.SETTLEMENT_MIDDLEWARE_CONFIG ||
repoConfigPath('config/settlement-middleware.v1.json');
cached = JSON.parse(fs.readFileSync(configPath, 'utf8')) as MiddlewareConfig;
return cached;
}
export function loadOfficeProfile(): OfficeSettlementProfile {
if (cachedOffice) return cachedOffice;
const cfg = loadConfig();
const profileRel =
cfg.settlementOffice?.profilePath ?? 'config/offices/office-24-settlement.v1.json';
const profilePath = process.env.OMNL_OFFICE_SETTLEMENT_PROFILE || repoConfigPath(profileRel);
cachedOffice = JSON.parse(fs.readFileSync(profilePath, 'utf8')) as OfficeSettlementProfile;
return cachedOffice;
}
/** All settlement runs behind Office 24 (Ali HOSPITALLERS). */
export function settlementOfficeId(requested?: number): number {
const cfg = loadConfig();
const profile = loadOfficeProfile();
const locked = cfg.settlementOffice?.officeId ?? profile.officeId ?? cfg.defaultOfficeId;
const fromEnv = parseInt(
process.env.ALI_HOSPITALLERS_OFFICE_ID ||
process.env.OMNL_SETTLEMENT_OFFICE_ID ||
String(locked),
10,
);
const officeId = fromEnv || locked;
if (cfg.enforceSingleOffice && requested !== undefined && requested !== officeId) {
throw new Error(`Settlement is locked to office ${officeId}; requested ${requested}`);
}
if (cfg.enforceSingleOffice && officeId !== locked) {
throw new Error(`Settlement is locked to office ${locked}; env specifies ${officeId}`);
}
return officeId;
}
export function port(): number {
return parseInt(process.env.SETTLEMENT_MIDDLEWARE_PORT || '3011', 10);
}
export function settlementStoreSubdir(): string {
return `office-${settlementOfficeId()}`;
}

View File

@@ -0,0 +1,10 @@
import * as dotenv from 'dotenv';
import path from 'path';
import { existsSync } from 'fs';
import { startServer } from './server';
const rootEnv = path.resolve(__dirname, '../../../.env');
if (existsSync(rootEnv)) dotenv.config({ path: rootEnv });
dotenv.config();
startServer();

View File

@@ -0,0 +1,21 @@
import express from 'express';
import cors from 'cors';
import { createSettlementRouter } from './api/routes/settlement';
import { port } from './config';
export function createApp() {
const app = express();
app.use(cors());
app.use(express.json({ limit: '2mb' }));
app.use(express.text({ type: 'text/plain', limit: '2mb' }));
app.use('/api/v1/settlement', createSettlementRouter());
return app;
}
export function startServer(): void {
const app = createApp();
const p = port();
app.listen(p, () => {
console.log(`OMNL settlement middleware listening on http://localhost:${p}/api/v1/settlement`);
});
}

View File

@@ -0,0 +1,46 @@
import fs from 'fs';
import path from 'path';
import type { SettlementRecord } from '@dbis/settlement-core';
import { settlementStoreSubdir } from '../config';
const baseDir =
process.env.SETTLEMENT_STORE_DIR ||
path.resolve(__dirname, '../../../data/settlement-store');
function storeDir(): string {
const dir = path.join(baseDir, settlementStoreSubdir());
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
return dir;
}
export class SettlementStore {
save(record: SettlementRecord): void {
const dir = storeDir();
fs.writeFileSync(path.join(dir, `${record.settlementId}.json`), JSON.stringify(record, null, 2));
fs.writeFileSync(path.join(dir, `key-${record.idempotencyKey}.json`), record.settlementId);
}
getById(id: string): SettlementRecord | null {
const p = path.join(storeDir(), `${id}.json`);
if (!fs.existsSync(p)) return null;
return JSON.parse(fs.readFileSync(p, 'utf8')) as SettlementRecord;
}
getByIdempotencyKey(key: string): SettlementRecord | null {
const ref = path.join(storeDir(), `key-${key}.json`);
if (!fs.existsSync(ref)) return null;
const id = fs.readFileSync(ref, 'utf8').trim();
return this.getById(id);
}
list(limit = 50): SettlementRecord[] {
const dir = storeDir();
return fs
.readdirSync(dir)
.filter((f) => f.endsWith('.json') && !f.startsWith('key-'))
.slice(0, limit)
.map((f) => JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8')) as SettlementRecord);
}
}
export const settlementStore = new SettlementStore();

View File

@@ -0,0 +1,171 @@
import { v4 as uuidv4 } from 'uuid';
import {
buildMoneySupplySnapshot,
isIbanValid,
m2FiatToTokenLoadAmount,
resolveInstrumentGl,
rollExternalTransferVerbiage,
swiftKindForRequest,
type ExternalTransferRequest,
type SettlementRecord,
} from '@dbis/settlement-core';
import { loadConfig, loadOfficeProfile, settlementOfficeId } from '../config';
import { fetchGlBalances, postJournalEntry } from '../adapters/fineract';
import { HybxProductionRail } from '../adapters/hybx-production';
import { archiveIso20022, requestTokenMint } from '../adapters/omnl';
import { buildMt102Stub, buildMt103Stub, forwardSwiftToListener } from '../adapters/swift';
import { settlementStore } from '../store/settlement-store';
function glId(code: string): number {
return parseInt(code, 10);
}
export async function processExternalTransfer(
input: ExternalTransferRequest,
): Promise<SettlementRecord> {
const cfg = loadConfig();
const profile = loadOfficeProfile();
const officeId = settlementOfficeId(input.officeId);
const req: ExternalTransferRequest = { ...input, officeId };
const existing = settlementStore.getByIdempotencyKey(req.idempotencyKey);
if (existing?.phase === 'SETTLED') return existing;
const now = new Date().toISOString();
let record: SettlementRecord = existing ?? {
settlementId: uuidv4(),
idempotencyKey: req.idempotencyKey,
phase: 'RECEIVED',
request: req,
errors: [],
createdAt: now,
updatedAt: now,
};
const advance = (phase: SettlementRecord['phase'], patch: Partial<SettlementRecord> = {}) => {
record = { ...record, phase, updatedAt: new Date().toISOString(), ...patch };
settlementStore.save(record);
};
try {
if (!isIbanValid(req.creditorIban)) {
throw new Error(`Invalid creditor IBAN: ${req.creditorIban}`);
}
advance('VALIDATED');
const verbiage = rollExternalTransferVerbiage(req);
advance('VERBIAGE_ROLLED', { verbiageDocument: verbiage });
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;
if (amount > 0) {
const je = await postJournalEntry({
officeId: req.officeId,
transactionDate: req.valueDate,
referenceNumber: req.idempotencyKey,
comments: verbiage.slice(0, 500),
debits: [{ glAccountId: glId(debitGl), amount }],
credits: [{ glAccountId: glId(creditGl), amount }],
});
advance('FINERACT_POSTED', { fineractJournalRef: String(je.resourceId) });
} else {
advance('FINERACT_POSTED');
}
if (cfg.rails.hybx.enabled && req.rail !== 'INTERNAL') {
const hybx = new HybxProductionRail();
const pay = await hybx.dispatchPayment({
idempotencyKey: req.idempotencyKey,
amount: req.amount,
currency: req.currency,
creditorIban: req.creditorIban,
creditorBic: req.creditorBic,
beneficiaryName: req.beneficiaryName,
remittanceInfo: req.remittanceInfo,
rail: req.rail,
});
advance('HYBX_RAIL_DISPATCHED', { hybxPaymentId: pay.paymentId });
} else {
advance('HYBX_RAIL_DISPATCHED');
}
const swiftKind = swiftKindForRequest(req);
const swiftRaw =
swiftKind === 'MT102'
? buildMt102Stub({
senderRef: req.idempotencyKey,
currency: req.currency,
amount: req.amount,
valueDate: req.valueDate,
orderingInstitution: req.orderingName ?? cfg.omnlLegalName,
beneficiary: req.beneficiaryName,
iban: req.creditorIban,
remittance: req.remittanceInfo,
})
: buildMt103Stub({
senderRef: req.idempotencyKey,
currency: req.currency,
amount: req.amount,
valueDate: req.valueDate,
orderingCustomer: req.orderingName ?? cfg.omnlLegalName,
beneficiary: req.beneficiaryName,
iban: req.creditorIban,
bic: req.creditorBic,
remittance: req.remittanceInfo,
});
if (cfg.rails.swift.enabled) {
await forwardSwiftToListener(swiftRaw);
}
const iso = await archiveIso20022({
messageType: swiftKind === 'MT102' ? 'pacs.008' : 'pacs.008',
direction: 'OUTBOUND',
raw: swiftRaw,
settlementRef: record.settlementId,
});
advance('ISO20022_ARCHIVED', { iso20022MessageId: iso.messageId });
if (req.convertToCrypto?.enabled) {
const balances = await fetchGlBalances(req.officeId);
const snap = buildMoneySupplySnapshot(req.officeId, balances);
const { loadAmount, fullyBacked } = m2FiatToTokenLoadAmount(
req.amount,
snap.M2.broadMoney,
);
if (!fullyBacked) {
record.errors.push(`M2 backing insufficient for token load (${loadAmount}/${req.amount})`);
}
const mint = await requestTokenMint({
lineId: req.convertToCrypto.tokenLineId,
amount: loadAmount,
recipient: req.convertToCrypto.recipientAddress,
settlementRef: record.settlementId,
dryRun: !cfg.production.allowChainMintExecute,
});
advance('CHAIN_MINT_REQUESTED', { chainTxHash: mint.txHash });
} else {
advance('CHAIN_MINT_REQUESTED');
}
advance('SETTLED');
return record;
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
record.errors.push(msg);
advance('FAILED');
return record;
}
}
export async function getMoneySupply(officeId: number) {
const balances = await fetchGlBalances(officeId);
return buildMoneySupplySnapshot(officeId, balances);
}
export function getSettlementStatus(id: string): SettlementRecord | null {
return settlementStore.getById(id);
}

View File

@@ -0,0 +1,33 @@
import type { ExternalTransferRequest } from '@dbis/settlement-core';
import { loadConfig, loadOfficeProfile, settlementOfficeId } from '../config';
/** Bind every settlement request to Office 24 (same profile as OMNL terminal). */
export function bindOffice24(req: Partial<ExternalTransferRequest>): ExternalTransferRequest {
const cfg = loadConfig();
const profile = loadOfficeProfile();
const officeId = settlementOfficeId(req.officeId);
if (!req.idempotencyKey || !req.creditorIban || !req.amount) {
throw new Error('idempotencyKey, creditorIban, amount required');
}
return {
officeId,
valueDate: req.valueDate ?? new Date().toISOString().slice(0, 10),
currency: req.currency ?? profile.settlement.defaultCurrency ?? cfg.defaultCurrency,
moneyLayers: (req.moneyLayers ?? profile.settlement.moneyLayers) as ExternalTransferRequest['moneyLayers'],
rail: req.rail ?? 'SWIFT',
remittanceInfo: req.remittanceInfo ?? `OMNL Office 24 settlement — ${profile.externalId}`,
beneficiaryName: req.beneficiaryName ?? 'Beneficiary',
idempotencyKey: req.idempotencyKey,
creditorIban: req.creditorIban,
amount: req.amount,
debtorIban: req.debtorIban,
creditorBic: req.creditorBic,
orderingName: req.orderingName ?? profile.omnlLegalName,
swiftMessageKind: req.swiftMessageKind,
tradeFinance: req.tradeFinance,
rwaVerbiage: req.rwaVerbiage,
convertToCrypto: req.convertToCrypto,
};
}

View File

@@ -0,0 +1,49 @@
import { v4 as uuidv4 } from 'uuid';
import type { ExternalTransferRequest, SettlementRecord } from '@dbis/settlement-core';
import { forwardSwiftToListener } from '../adapters/swift';
import { settlementOfficeId } from '../config';
import { settlementStore } from '../store/settlement-store';
import { processExternalTransfer } from './external-transfer';
/** Inbound MT103/MT102 → settlement workflow */
export async function processSwiftInbound(raw: string): Promise<SettlementRecord> {
await forwardSwiftToListener(raw);
const isMt102 = /\bMT102\b|:23:/.test(raw) || /:32B:/.test(raw);
const refMatch = raw.match(/:20:([^\n]+)/);
const amtMatch = raw.match(/:32[AB]:(\d{6})([A-Z]{3})([\d,]+)/);
const ibanMatch = raw.match(/\/([A-Z]{2}\d{2}[A-Z0-9]+)/);
const bnfMatch = raw.match(/:59:[^\n]*\n([^\n:]+)/);
const remMatch = raw.match(/:70:([^\n]+)/);
const req: ExternalTransferRequest = {
idempotencyKey: refMatch?.[1]?.trim() || `SWIFT-IN-${uuidv4()}`,
officeId: settlementOfficeId(),
valueDate: new Date().toISOString().slice(0, 10),
currency: amtMatch?.[2] ?? 'USD',
amount: (amtMatch?.[3] ?? '0').replace(',', '.'),
creditorIban: ibanMatch?.[1] ?? '',
beneficiaryName: bnfMatch?.[1]?.trim() ?? 'Inbound beneficiary',
remittanceInfo: remMatch?.[1]?.trim() ?? 'Inbound SWIFT settlement',
moneyLayers: isMt102 ? ['M1', 'M2'] : ['M0', 'M1'],
rail: 'SWIFT',
swiftMessageKind: isMt102 ? 'MT102' : 'MT103',
rwaVerbiage: { assetClass: 'RWA', tokenLineId: 'USD-M1', externalTransferRoll: true },
};
if (!req.creditorIban) {
const stub: SettlementRecord = {
settlementId: uuidv4(),
idempotencyKey: req.idempotencyKey,
phase: 'RECEIVED',
request: req,
errors: ['Parsed inbound SWIFT — manual IBAN review required'],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
settlementStore.save(stub);
return stub;
}
return processExternalTransfer(req);
}

View File

@@ -0,0 +1,115 @@
import { v4 as uuidv4 } from 'uuid';
import {
buildMoneySupplySnapshot,
GL_CODES,
m2FiatToTokenLoadAmount,
type TokenLoadRequest,
type SettlementRecord,
} from '@dbis/settlement-core';
import { loadConfig, loadOfficeProfile, settlementOfficeId } from '../config';
import { fetchGlBalances, postJournalEntry } from '../adapters/fineract';
import { requestTokenMint } from '../adapters/omnl';
import { settlementStore } from '../store/settlement-store';
function glId(code: string): number {
return parseInt(code, 10);
}
export async function processTokenLoad(input: TokenLoadRequest): Promise<SettlementRecord> {
const cfg = loadConfig();
const profile = loadOfficeProfile();
const officeId = settlementOfficeId(input.officeId);
const req = { ...input, officeId };
const existing = settlementStore.getByIdempotencyKey(req.idempotencyKey);
if (existing?.phase === 'SETTLED') return existing;
const now = new Date().toISOString();
let record: SettlementRecord = existing ?? {
settlementId: uuidv4(),
idempotencyKey: req.idempotencyKey,
phase: 'RECEIVED',
request: {
idempotencyKey: req.idempotencyKey,
officeId,
valueDate: now.slice(0, 10),
currency: req.currency ?? 'USD',
amount: req.amount,
creditorIban: 'INTERNAL-M2-TOKEN-LOAD',
beneficiaryName: req.recipientAddress,
remittanceInfo: `M2 token load ${req.lineId}${req.symbol ?? req.tokenAddress ?? 'token'}`,
moneyLayers: ['M2'],
rail: 'INTERNAL',
convertToCrypto: {
enabled: true,
targetChainId: 138,
tokenLineId: req.lineId,
recipientAddress: req.recipientAddress,
},
},
errors: [],
createdAt: now,
updatedAt: now,
};
const advance = (phase: SettlementRecord['phase'], patch: Partial<SettlementRecord> = {}) => {
record = { ...record, phase, updatedAt: new Date().toISOString(), ...patch };
settlementStore.save(record);
};
try {
advance('VALIDATED');
advance('VERBIAGE_ROLLED', {
verbiageDocument: [
`OMNL M2 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}`,
].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})`);
advance('FAILED');
return record;
}
const amount = parseFloat(loadAmount) || 0;
if (amount > 0) {
const je = await postJournalEntry({
officeId,
transactionDate: now.slice(0, 10),
referenceNumber: req.idempotencyKey,
comments: `M2 token load ${req.lineId}`,
debits: [{ glAccountId: glId(profile.settlement.glM2 ?? GL_CODES.M2_BROAD), amount }],
credits: [{ glAccountId: glId(GL_CODES.CRYPTO_TOKEN_LIABILITY), amount }],
});
advance('FINERACT_POSTED', { fineractJournalRef: String(je.resourceId) });
} else {
advance('FINERACT_POSTED');
}
advance('HYBX_RAIL_DISPATCHED');
advance('ISO20022_ARCHIVED');
const mint = await requestTokenMint({
lineId: req.lineId,
amount: loadAmount,
recipient: req.recipientAddress,
settlementRef: record.settlementId,
dryRun: !cfg.production.allowChainMintExecute,
symbol: req.symbol,
tokenAddress: req.tokenAddress,
});
advance('CHAIN_MINT_REQUESTED', { chainTxHash: mint.txHash });
advance('SETTLED');
return record;
} catch (err) {
record.errors.push(err instanceof Error ? err.message : String(err));
advance('FAILED');
return record;
}
}

View File

@@ -0,0 +1,132 @@
import { v4 as uuidv4 } from 'uuid';
import { GL_CODES, type TransferRequest, type SettlementRecord } from '@dbis/settlement-core';
import { loadConfig, loadOfficeProfile, settlementOfficeId } from '../config';
import { postJournalEntry } from '../adapters/fineract';
import { HybxProductionRail } from '../adapters/hybx-production';
import { archiveIso20022 } from '../adapters/omnl';
import { buildMt103Stub, forwardSwiftToListener } from '../adapters/swift';
import { settlementStore } from '../store/settlement-store';
function glId(code: string): number {
return parseInt(code, 10);
}
export async function processTransfer(input: TransferRequest): Promise<SettlementRecord> {
const cfg = loadConfig();
const profile = loadOfficeProfile();
const officeId = settlementOfficeId(input.officeId);
const req = { ...input, officeId };
const isExternal = req.rail === 'SWIFT' || req.rail === 'RTGS';
const existing = settlementStore.getByIdempotencyKey(req.idempotencyKey);
if (existing?.phase === 'SETTLED') return existing;
const now = new Date().toISOString();
let record: SettlementRecord = existing ?? {
settlementId: uuidv4(),
idempotencyKey: req.idempotencyKey,
phase: 'RECEIVED',
request: {
idempotencyKey: req.idempotencyKey,
officeId,
valueDate: now.slice(0, 10),
currency: req.currency ?? 'USD',
amount: req.amount,
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'],
rail: req.rail === 'CHAIN138' ? 'CHAIN138' : req.rail === 'INTERNAL' ? 'INTERNAL' : 'SWIFT',
},
errors: [],
createdAt: now,
updatedAt: now,
};
const advance = (phase: SettlementRecord['phase'], patch: Partial<SettlementRecord> = {}) => {
record = { ...record, phase, updatedAt: new Date().toISOString(), ...patch };
settlementStore.save(record);
};
try {
if (isExternal && !req.creditorIban) {
throw new Error('creditorIban required for external transfer');
}
advance('VALIDATED');
advance('VERBIAGE_ROLLED', {
verbiageDocument: [
`OMNL ${req.rail} transfer · ${req.tokenSymbol}`,
`M2 layer · ${req.amount}`,
`To ${req.recipientAddress}`,
isExternal ? `IBAN ${req.creditorIban}` : 'Internal / Chain 138',
].join('\n'),
});
const amount = parseFloat(req.amount) || 0;
if (amount > 0) {
const je = await postJournalEntry({
officeId,
transactionDate: now.slice(0, 10),
referenceNumber: req.idempotencyKey,
comments: `${req.rail} transfer ${req.tokenSymbol}`,
debits: [{ glAccountId: glId(profile.settlement.glM2 ?? GL_CODES.M2_BROAD), amount }],
credits: [{ glAccountId: glId(profile.settlement.glSettlement ?? GL_CODES.SETTLEMENT_SUSPENSE), amount }],
});
advance('FINERACT_POSTED', { fineractJournalRef: String(je.resourceId) });
} else {
advance('FINERACT_POSTED');
}
if (isExternal && cfg.rails.hybx.enabled) {
const hybx = new HybxProductionRail();
const pay = await hybx.dispatchPayment({
idempotencyKey: req.idempotencyKey,
amount: req.amount,
currency: req.currency ?? 'USD',
creditorIban: req.creditorIban!,
beneficiaryName: req.beneficiaryName ?? req.recipientAddress,
remittanceInfo: req.remittanceInfo ?? `OMNL ${req.tokenSymbol} external transfer`,
rail: req.rail === 'RTGS' ? 'RTGS' : 'SWIFT',
});
advance('HYBX_RAIL_DISPATCHED', { hybxPaymentId: pay.paymentId });
} else {
advance('HYBX_RAIL_DISPATCHED');
}
if (isExternal && cfg.rails.swift.enabled && req.creditorIban) {
const swiftRaw = buildMt103Stub({
senderRef: req.idempotencyKey,
currency: req.currency ?? 'USD',
amount: req.amount,
valueDate: now.slice(0, 10),
orderingCustomer: cfg.omnlLegalName,
beneficiary: req.beneficiaryName ?? req.recipientAddress,
iban: req.creditorIban,
remittance: req.remittanceInfo ?? `OMNL ${req.tokenSymbol}`,
});
await forwardSwiftToListener(swiftRaw);
const iso = await archiveIso20022({
messageType: 'pacs.008',
direction: 'OUTBOUND',
raw: swiftRaw,
settlementRef: record.settlementId,
});
advance('ISO20022_ARCHIVED', { iso20022MessageId: iso.messageId });
} else {
advance('ISO20022_ARCHIVED');
}
advance('CHAIN_MINT_REQUESTED', {
verbiageDocument: [
record.verbiageDocument,
`On-chain ERC-20 transfer prepared: ${req.tokenAddress}${req.recipientAddress}`,
].join('\n'),
});
advance('SETTLED');
return record;
} catch (err) {
record.errors.push(err instanceof Error ? err.message : String(err));
advance('FAILED');
return record;
}
}