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
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:
271
services/settlement-middleware/src/api/routes/settlement.ts
Normal file
271
services/settlement-middleware/src/api/routes/settlement.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user