feat(guosmm): deploy gazette notices across API, portal, and office configs
Some checks failed
CI/CD Pipeline / Security Scanning (push) Has been cancelled
CI/CD Pipeline / Lint and Format (push) Has been cancelled
CI/CD Pipeline / Terraform Validation (push) Has been cancelled
CI/CD Pipeline / Kubernetes Validation (push) Has been cancelled
CI/CD Pipeline / Solidity Contracts (push) Has been cancelled
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Has been cancelled
Validation / validate-genesis (push) Has been cancelled
Validation / validate-terraform (push) Has been cancelled
Validation / validate-kubernetes (push) Has been cancelled
Validation / validate-smart-contracts (push) Has been cancelled
Validation / validate-security (push) Has been cancelled
Validation / validate-documentation (push) Has been cancelled
Deploy ChainID 138 / Deploy ChainID 138 (push) Failing after 45s
Verify Deployment / Verify Deployment (push) Failing after 58s

Register GUOSMM-B-2026-001 and C-2026-003/004/005 in SSOT, expose via
/api/v1/omnl/gazette/notices, hub and central-bank UI, offices 29-31,
compliance console, and production deploy sync script.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-03 02:03:38 -07:00
parent 4919328162
commit 14238dc81d
26 changed files with 3159 additions and 7 deletions

View File

@@ -0,0 +1,128 @@
{
"$schema": "OMNL ecosystem link map — canonical URLs for portals, APIs, and local dev",
"version": "1.2.0",
"updated": "2026-07-03",
"notes": [
"Public nginx exposes /settlement/* and /exchange/* without /api/v1/... prefix",
"Tunnel save file logs/public-url-{operator}.txt stores primary portal URL (Z Online Bank)",
"/omnl/* operator UIs ship in frontend dist (public/omnl) — portal-serve proxies /api to explorer",
"POST /z-bank/settle-m0-m4 exists in settlement-middleware source; production may use POST /settlement/external-transfer until deployed",
"SPA anchors #z-full-settlement and #z-settlement-protocols exist on /central-bank"
],
"tunnel": {
"ephemeral": true,
"saveFile": "logs/public-url-ali.txt",
"primaryPath": "/central-bank",
"apps": {
"bridge": "/bridge",
"hub": "/hub",
"zOnlineBank": "/central-bank",
"zFullSettlement": "/central-bank#z-full-settlement",
"wireProtocols": "/central-bank#z-settlement-protocols",
"office24": "/office-24",
"omnlCompliance": "/omnl/compliance",
"omnlDashboard": "/omnl/dashboard",
"omnlTerminal": "/omnl/terminal",
"trade": "/trade",
"swap": "/swap",
"reserve": "/reserve",
"history": "/history",
"wallets": "/wallets",
"admin": "/admin",
"docs": "/docs"
},
"nginxOn3002": {
"settlementHealth": "/settlement/health",
"exchangeHealth": "/exchange/health",
"apiCatalog": "/api/v1/omnl/catalog"
},
"brokenOnServeOnly": []
},
"subjects": [
{ "id": "bridge", "label": "Bridge", "path": "/bridge", "group": "app" },
{ "id": "hub", "label": "DBIS Hub", "path": "/hub", "group": "app" },
{ "id": "zOnlineBank", "label": "Z Online Bank", "path": "/central-bank", "group": "app" },
{ "id": "zFullSettlement", "label": "Z M0-M4 settlement", "path": "/central-bank#z-full-settlement", "group": "app" },
{ "id": "wireProtocols", "label": "Wire protocols", "path": "/central-bank#z-settlement-protocols", "group": "app" },
{ "id": "office24", "label": "Office 24 Central Bank", "path": "/office-24", "group": "app" },
{ "id": "trade", "label": "DBIS Trade", "path": "/trade", "group": "app" },
{ "id": "swap", "label": "Digital Swap", "path": "/swap", "group": "app" },
{ "id": "reserve", "label": "Reserve", "path": "/reserve", "group": "app" },
{ "id": "history", "label": "History", "path": "/history", "group": "app" },
{ "id": "wallets", "label": "Wallets", "path": "/wallets", "group": "app" },
{ "id": "admin", "label": "Admin", "path": "/admin", "group": "app" },
{ "id": "docs", "label": "Developers", "path": "/docs", "group": "app" },
{ "id": "omnlCompliance", "label": "OMNL Compliance", "path": "/omnl/compliance", "group": "console" },
{ "id": "omnlDashboard", "label": "OMNL API snapshot", "path": "/omnl/dashboard", "group": "console" },
{ "id": "omnlTerminal", "label": "Settlement terminal", "path": "/omnl/terminal", "group": "console" },
{ "id": "apiCatalog", "label": "API catalog", "path": "/api/v1/omnl/catalog", "group": "api" },
{ "id": "gazetteNotices", "label": "GUOSMM gazette registry", "path": "/api/v1/omnl/gazette/notices", "group": "api" },
{ "id": "integrationStatus", "label": "Integration status", "path": "/api/v1/omnl/integration-status", "group": "api" },
{ "id": "settlementHealth", "label": "Settlement health", "path": "/settlement/health", "group": "api" },
{ "id": "exchangeHealth", "label": "Exchange health", "path": "/exchange/health", "group": "api" }
],
"local": {
"frontend": {
"port": 3002,
"bridge": "http://localhost:3002/bridge",
"centralBank": "http://localhost:3002/central-bank",
"hub": "http://localhost:3002/hub",
"office24": "http://localhost:3002/office-24",
"trade": "http://localhost:3002/trade",
"swap": "http://localhost:3002/swap"
},
"backend": {
"omnlCatalog": "http://localhost:3000/api/v1/omnl/catalog",
"compliance": "http://localhost:3000/omnl/compliance",
"omnlDashboard": "http://localhost:3000/omnl/dashboard",
"settlementTerminal": "http://localhost:3000/omnl/terminal",
"settlementHealth": "http://localhost:3011/api/v1/settlement/health",
"settlementChains": "http://localhost:3011/api/v1/settlement/chains",
"moneySupplyPublic": "http://localhost:3011/api/v1/settlement/money-supply/public",
"exchangeHealth": "http://localhost:3012/api/v1/exchange/health"
},
"nginx3002": {
"settlementHealth": "http://localhost:3002/settlement/health",
"exchangeHealth": "http://localhost:3002/exchange/health",
"apiCatalog": "http://localhost:3002/api/v1/omnl/catalog"
}
},
"productionPortals": {
"zOnlineBank": "https://online.omdnl.org/central-bank",
"centralBankSecure": "https://secure.omdnl.org/central-bank",
"office24": "https://office24.omdnl.org/office-24",
"exchangeTrade": "https://exchange.omdnl.org/trade",
"digitalSwap": "https://digital.omdnl.org/swap",
"forexTrade": "https://forex.omdnl.org/trade",
"omnlBankHybx": "https://bank.omnl.hybxfinance.io",
"exchangeDbisMirror": "https://exchange.d-bis.org/trade",
"centralBankDbisMirror": "https://secure.d-bis.org/central-bank",
"forexDbisMirror": "https://forex.d-bis.org/trade"
},
"productionApis": {
"explorerRoot": "https://explorer.d-bis.org/api/v1",
"catalog": "https://explorer.d-bis.org/api/v1/omnl/catalog",
"gazetteNotices": "https://explorer.d-bis.org/api/v1/omnl/gazette/notices",
"complianceUi": "https://explorer.d-bis.org/omnl/compliance",
"omnlDashboardUi": "https://explorer.d-bis.org/omnl/dashboard",
"settlementTerminalUi": "https://explorer.d-bis.org/omnl/terminal",
"settlementHealth": "https://secure.omdnl.org/settlement/health",
"settlementChains": "https://secure.omdnl.org/settlement/chains",
"moneySupplyPublic": "https://secure.omdnl.org/settlement/public/money-supply",
"externalTransfer": "https://secure.omdnl.org/settlement/external-transfer",
"exchangeHealth": "https://exchange.omdnl.org/exchange/health",
"exchangeTokens": "https://exchange.omdnl.org/exchange/tokens",
"chain138Rpc": "https://rpc-http-pub.d-bis.org",
"chain138Explorer": "https://explorer.d-bis.org",
"fineractSwagger": "https://omnl.hybxfinance.io/fineract-provider/swagger-ui/index.html",
"swiftListenerPublic": "https://swift-listener.d-bis.org"
},
"infra": {
"gitea": "https://gitea.d-bis.org",
"smomRepo": "https://gitea.d-bis.org/d-bis/smom-dbis-138.git",
"guosmmGazette": "https://gazette.shosjj.org/notices/2026",
"swiftListenerLan": "http://192.168.11.114:8788",
"fineractSidecarLan": "http://192.168.11.89:8080",
"chain138RpcLan": "http://192.168.11.211:8545"
}
}

View File

@@ -0,0 +1,115 @@
{
"$schema": "GUOSMM official gazette notices — OMNL / ISB member banks",
"version": "1.0.0",
"gazetteBase": "https://gazette.shosjj.org/notices/2026",
"updated": "2026-07-03",
"notices": [
{
"documentId": "GUOSMM-B-2026-001",
"series": "B",
"title": "Central Bank Charter — OMNL",
"documentType": "Central Bank Charter",
"url": "https://gazette.shosjj.org/notices/2026/GUOSMM-B-2026-001",
"publishedAt": "2026-06-12T12:00:00Z",
"effectiveAt": "2026-06-13T00:00:00Z",
"signedAt": "2026-07-02T00:45:01.965Z",
"signingAuthority": "Sovereign Military Order of Malta",
"contentSha256": "685ebdbfd6a9c1b2c0b7963b7771bf96e96d002e822107207df66f7f593b8032",
"republication": "v2",
"omnlLei": "98450070C57395F6B906",
"scope": ["omnl", "emcb", "central-bank", "office-24", "settlement"],
"enablingActs": ["GUOSMM-A-2026-001"],
"notes": "EMCB model — Treasury issues SCUDO; OMNL administers; not issuer"
},
{
"documentId": "GUOSMM-C-2026-003",
"series": "C",
"title": "zBank — ISB Membership Admission and Operations Charter",
"documentType": "ISB Membership Admission and Operations Charter",
"url": "https://gazette.shosjj.org/notices/2026/GUOSMM-C-2026-003",
"publishedAt": "2026-07-01T20:00:00Z",
"effectiveAt": "2026-05-02T00:00:00Z",
"operationsCommencement": "2026-05-02T00:00:00Z",
"signedAt": "2026-07-03T03:24:07.594Z",
"signingAuthority": "Sovereign Military Order of Malta",
"contentSha256": "c685fd513cbc1e5d20d8f91605f33a8e824680a58b85c67a1a8ef12cf42b71eb",
"licenseId": "LICENSE-ZBANK-SHOSJJ-2026-001",
"licenseClass": "entity.bank",
"entity": "zBank",
"fineractOfficeId": 29,
"fineractExternalId": "ZBANK-OMNL",
"issuingDelegate": "pack-dbis",
"prudentialSupervisor": "pack-omnl",
"parentActs": [
"GUOSMM-A-2026-001",
"GUOSMM-A-2026-002",
"GUOSMM-A-2026-003",
"GUOSMM-B-2026-001",
"GUOSMM-B-2026-002",
"GUOSMM-C-2026-001",
"GUOSMM-C-2026-002"
],
"scope": ["z-bank", "onex-bank", "central-bank", "office-29"]
},
{
"documentId": "GUOSMM-C-2026-004",
"series": "C",
"title": "NovaBank — ISB Membership Admission and Operations Charter",
"documentType": "ISB Membership Admission and Operations Charter",
"url": "https://gazette.shosjj.org/notices/2026/GUOSMM-C-2026-004",
"publishedAt": "2026-07-01T20:00:00Z",
"effectiveAt": "2026-05-02T00:00:00Z",
"operationsCommencement": "2026-05-02T00:00:00Z",
"signedAt": "2026-07-03T03:24:08.276Z",
"signingAuthority": "Sovereign Military Order of Malta",
"contentSha256": "6c524ad5207ae9ca8d53cd4f847593f175740291ad2f519c554bbb823b304b50",
"licenseId": "LICENSE-NOVABANK-SHOSJJ-2026-001",
"licenseClass": "entity.bank",
"entity": "NovaBank",
"fineractOfficeId": 30,
"fineractExternalId": "NOVABANK-OMNL",
"issuingDelegate": "pack-dbis",
"prudentialSupervisor": "pack-omnl",
"parentActs": [
"GUOSMM-A-2026-001",
"GUOSMM-A-2026-002",
"GUOSMM-A-2026-003",
"GUOSMM-B-2026-001",
"GUOSMM-B-2026-002",
"GUOSMM-C-2026-001",
"GUOSMM-C-2026-002"
],
"scope": ["nova-bank", "central-bank", "office-30"]
},
{
"documentId": "GUOSMM-C-2026-005",
"series": "C",
"title": "AnakaBank — ISB Membership Admission and Operations Charter",
"documentType": "ISB Membership Admission and Operations Charter",
"url": "https://gazette.shosjj.org/notices/2026/GUOSMM-C-2026-005",
"publishedAt": "2026-07-01T20:00:00Z",
"effectiveAt": "2026-05-02T00:00:00Z",
"operationsCommencement": "2026-05-02T00:00:00Z",
"signedAt": "2026-07-03T03:24:08.926Z",
"signingAuthority": "Sovereign Military Order of Malta",
"contentSha256": "b93879aeb6e1c4791084b1e327682ccbf9b482a2b9b4fbbaff193e6e7a8f5f3e",
"licenseId": "LICENSE-ANAKABANK-SHOSJJ-2026-001",
"licenseClass": "entity.bank",
"entity": "AnakaBank",
"fineractOfficeId": 31,
"fineractExternalId": "ANAKABANK-OMNL",
"issuingDelegate": "pack-dbis",
"prudentialSupervisor": "pack-omnl",
"parentActs": [
"GUOSMM-A-2026-001",
"GUOSMM-A-2026-002",
"GUOSMM-A-2026-003",
"GUOSMM-B-2026-001",
"GUOSMM-B-2026-002",
"GUOSMM-C-2026-001",
"GUOSMM-C-2026-002"
],
"scope": ["anaka-bank", "central-bank", "office-31"]
}
]
}

View File

@@ -554,6 +554,42 @@
"inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8",
"inboundSettlementType": "gnosis-safe",
"integrationRef": "config/onex-shiva-integration.v1.json"
},
{
"lineId": "0xa1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef12345678",
"officeId": 30,
"externalId": "NOVABANK-OMNL",
"entityName": "NovaBank",
"walletId": "HYBX-OFFICE-030",
"guosmmNotice": "GUOSMM-C-2026-004",
"treasuryWalletMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8",
"treasuryWalletChain138": "0xc93522ae4ab7482572cc29edd68fbb6d5ec6a585",
"chains": {
"138": {
"tokenM0": "0x0000000000000000000000000000000000000000",
"tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b"
}
},
"inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8",
"inboundSettlementType": "gnosis-safe"
},
{
"lineId": "0xb2c3d4e5f6789012345678901234567890abcdef1234567890abcdef1234567890",
"officeId": 31,
"externalId": "ANAKABANK-OMNL",
"entityName": "AnakaBank",
"walletId": "HYBX-OFFICE-031",
"guosmmNotice": "GUOSMM-C-2026-005",
"treasuryWalletMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8",
"treasuryWalletChain138": "0xc93522ae4ab7482572cc29edd68fbb6d5ec6a585",
"chains": {
"138": {
"tokenM0": "0x0000000000000000000000000000000000000000",
"tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b"
}
},
"inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8",
"inboundSettlementType": "gnosis-safe"
}
],
"globalInboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8"

View File

@@ -27,5 +27,7 @@
"chain138": { "enabled": true, "rpcEnv": "RPC_URL_138", "defaultLineId": "USD-M2" }
},
"omnlLegalName": "ORGANISATION MONDIALE DU NUMERIQUE L.P.B.C.",
"omnlLei": "98450070C57395F6B906"
"omnlLei": "98450070C57395F6B906",
"guosmmEnablingAct": "GUOSMM-B-2026-001",
"guosmmNoticeUrl": "https://gazette.shosjj.org/notices/2026/GUOSMM-B-2026-001"
}

View File

@@ -0,0 +1,22 @@
{
"$schema": "OMNL Office 29 — zBank (OneX Bank) per GUOSMM-C-2026-003",
"version": "1.0.0",
"officeId": 29,
"externalId": "ZBANK-OMNL",
"name": "zBank — ISB member (OneX Bank)",
"guosmmNotice": "GUOSMM-C-2026-003",
"licenseId": "LICENSE-ZBANK-SHOSJJ-2026-001",
"fineractTenant": "omnl",
"settlement": {
"defaultCurrency": "USD",
"moneyLayers": ["M0", "M1", "M2", "M3", "M4"],
"glM1": "2100",
"glM2": "2200",
"glM4NearMoney": "1000"
},
"rails": {
"swift": { "enabled": true },
"hybx": { "enabled": true },
"chain138": { "enabled": true, "defaultLineId": "USD-M2" }
}
}

View File

@@ -0,0 +1,22 @@
{
"$schema": "OMNL Office 30 — NovaBank per GUOSMM-C-2026-004",
"version": "1.0.0",
"officeId": 30,
"externalId": "NOVABANK-OMNL",
"name": "NovaBank — ISB member",
"guosmmNotice": "GUOSMM-C-2026-004",
"licenseId": "LICENSE-NOVABANK-SHOSJJ-2026-001",
"fineractTenant": "omnl",
"settlement": {
"defaultCurrency": "USD",
"moneyLayers": ["M0", "M1", "M2", "M3", "M4"],
"glM1": "2100",
"glM2": "2200",
"glM4NearMoney": "1000"
},
"rails": {
"swift": { "enabled": true },
"hybx": { "enabled": true },
"chain138": { "enabled": true, "defaultLineId": "USD-M2" }
}
}

View File

@@ -0,0 +1,22 @@
{
"$schema": "OMNL Office 31 — AnakaBank per GUOSMM-C-2026-005",
"version": "1.0.0",
"officeId": 31,
"externalId": "ANAKABANK-OMNL",
"name": "AnakaBank — ISB member",
"guosmmNotice": "GUOSMM-C-2026-005",
"licenseId": "LICENSE-ANAKABANK-SHOSJJ-2026-001",
"fineractTenant": "omnl",
"settlement": {
"defaultCurrency": "USD",
"moneyLayers": ["M0", "M1", "M2", "M3", "M4"],
"glM1": "2100",
"glM2": "2200",
"glM4NearMoney": "1000"
},
"rails": {
"swift": { "enabled": true },
"hybx": { "enabled": true },
"chain138": { "enabled": true, "defaultLineId": "USD-M2" }
}
}

View File

@@ -47,5 +47,10 @@
"allowHybxProduction": true,
"allowChainMintExecute": true,
"onlineBankMode": true
},
"guosmm": {
"emcbCharter": "GUOSMM-B-2026-001",
"registryPath": "config/guosmm-notices.v1.json",
"gazetteBase": "https://gazette.shosjj.org/notices/2026"
}
}

View File

@@ -0,0 +1,255 @@
(function () {
const params = new URLSearchParams(window.location.search);
const accessToken = params.get('access_token') || '';
const apiBase = window.resolveOmnlApiBase
? window.resolveOmnlApiBase()
: document.querySelector('meta[name="omnl-api-base"]')?.getAttribute('content')?.trim() || '/api/v1';
const els = {
status: document.getElementById('load-status'),
grid: document.getElementById('posture-grid'),
actions: document.getElementById('pending-actions'),
evidence: document.getElementById('evidence-kv'),
safe: document.getElementById('safe-kv'),
external: document.getElementById('external-kv'),
triple: document.getElementById('triple-summary'),
hoLiquidity: document.getElementById('ho-liquidity-kv'),
signoffs: document.getElementById('signoffs-summary'),
raw: document.getElementById('raw-json'),
refreshed: document.getElementById('refreshed-at'),
};
function apiHeaders() {
const h = { Accept: 'application/json' };
if (accessToken) h.Authorization = 'Bearer ' + accessToken;
return h;
}
function apiUrl(path) {
const base = apiBase.replace(/\/$/, '');
const rel = path.startsWith('/') ? path : '/' + path;
const u = new URL(base + rel, window.location.origin);
if (accessToken) u.searchParams.set('access_token', accessToken);
return u.toString();
}
function badge(ok, okLabel, badLabel) {
if (ok === true) return '<span class="badge ok">' + okLabel + '</span>';
if (ok === false) return '<span class="badge bad">' + badLabel + '</span>';
return '<span class="badge neutral">Unknown</span>';
}
function sevBadge(sev) {
const map = { critical: 'bad', high: 'bad', medium: 'warn', low: 'neutral' };
return '<span class="badge ' + (map[sev] || 'neutral') + '">' + sev + '</span>';
}
function renderPosture(data) {
const p = data.posture || {};
els.grid.innerHTML = [
card('Reporting', badge(p.reportingCompliant, 'Compliant', 'Not compliant'), ''),
card('Attestation', badge(!p.attestationStale, 'Fresh', 'Stale'), ''),
card('Triple reconcile', badge(p.tripleAligned, 'Aligned', 'Breaks'), ''),
card('Notary gate', badge(p.requireNotarizedEvidence, 'Enforced', 'Not enforced'), 'requireNotarizedEvidence'),
card('Threshold', '<div class="metric">' + (p.complianceThreshold || '—') + '</div>', 'IPSAS / IFRS / US GAAP'),
card('Policy v', '<div class="metric">' + (p.jurisdictionPolicyVersion ?? '—') + '</div>', 'ID jurisdiction'),
].join('');
}
function card(title, body, sub) {
return (
'<div class="card"><h2>' +
title +
'</h2>' +
body +
(sub ? '<small style="color:var(--muted);font-size:0.78rem">' + sub + '</small>' : '') +
'</div>'
);
}
function renderActions(actions) {
if (!actions || !actions.length) {
els.actions.innerHTML = '<p style="color:var(--muted)">No pending actions.</p>';
return;
}
els.actions.innerHTML =
'<ul class="actions-list">' +
actions
.map(function (a) {
const links = (a.links || [])
.map(function (l) {
const href = l.href.startsWith('http') ? l.href : apiUrl(l.href);
const dl = l.href.indexOf('safe-notary-gate-tx') >= 0 ? ' download="omnl-safe-notary-gate-tx.json"' : '';
return '<a href="' + href + '"' + dl + '>' + l.label + '</a>';
})
.join('');
return (
'<li><h3>' +
sevBadge(a.severity) +
' ' +
esc(a.title) +
'</h3><p>' +
esc(a.detail) +
'</p><div class="link-row">' +
links +
'</div></li>'
);
})
.join('') +
'</ul>';
}
function kv(rows) {
return (
'<dl class="kv">' +
rows
.map(function (r) {
return '<dt>' + esc(r[0]) + '</dt><dd>' + esc(String(r[1] ?? '—')) + '</dd>';
})
.join('') +
'</dl>'
);
}
function renderEvidence(data) {
const pr = data.proofReport;
const gate = data.onChainGate || {};
const triple = data.tripleReconcile;
els.evidence.innerHTML = kv([
['Evidence hash', pr && pr.evidenceHash],
['Merkle root', pr && pr.merkleRoot],
['Package 3-of-3', pr && pr.packageNotarized3of3],
['Reserve 3-of-3', pr && pr.reserveAttested3of3],
['Reserve store', gate.reserveStore],
['Notary registry', gate.notaryRegistry],
['Attestation threshold', gate.attestationThreshold],
['On-chain R', triple && triple.onChain && triple.onChain.r],
]);
}
function renderSafe(data) {
const pr = data.proofReport;
const g = (pr && pr.gnosisSafe) || {};
const sw = data.safeWallet || {};
els.safe.innerHTML = kv([
['Admin Safe', g.address || data.web3.deployed.GnosisSafeAdmin],
['Threshold', g.threshold ? g.threshold + '-of-' + g.owners : '—'],
['Safe Wallet registry', sw.npmRegistryHas138 ? 'chain 138 listed' : 'pending PR #1568'],
['Registry status', sw.status],
]);
}
function renderExternal(data) {
const ev = data.externalVisibility || {};
const guosmm = ev.guosmmGazette || {};
const noticeLinks = (guosmm.notices || [])
.map(function (n) {
return '<a href="' + esc(n.url) + '" target="_blank" rel="noopener">' + esc(n.documentId) + '</a>';
})
.join(' · ');
els.external.innerHTML =
kv([
['GUOSMM gazette', guosmm.base || '—'],
['API registry', guosmm.apiRegistry || '/api/v1/omnl/gazette/notices'],
['Notices', noticeLinks || '—'],
['DefiLlama chain', ev.defiLlama && ev.defiLlama.chainPage],
['Pricing PR', ev.defiLlama && ev.defiLlama.pr12094],
['Bridge TVL PR', ev.defiLlama && ev.defiLlama.pr19451],
['Safe deployments PR', ev.safeDeployments && ev.safeDeployments.pr1568],
]);
}
function renderHoLiquidity(snap) {
if (!els.hoLiquidity) return;
if (!snap) {
els.hoLiquidity.textContent = 'HO liquidity snapshot unavailable.';
return;
}
const cash = snap.cashInBank || {};
const cov = snap.reserveCoverage || {};
els.hoLiquidity.innerHTML = kv([
['Generated', snap.generatedAt],
['Nostro cash (GL 13010)', cash.cashInBankUsd != null ? '$' + Number(cash.cashInBankUsd).toLocaleString() : '—'],
['Reserve coverage', cov.coverageRatio != null ? cov.coverageRatio.toFixed(2) + '×' : '—'],
['Reserve assets', cov.reserveAssetsUsd != null ? '$' + Number(cov.reserveAssetsUsd).toLocaleString() : '—'],
['Issued c* USD', cov.issuedCStarUsd != null ? '$' + Number(cov.issuedCStarUsd).toLocaleString() : '—'],
['Public API', apiBase + '/omnl/ho-liquidity-snapshot'],
]);
}
function renderTriple(data) {
const t = data.tripleReconcile;
if (!t) {
els.triple.textContent = 'Triple-state reconcile unavailable.';
return;
}
const breaks = (t.breaks || []).length;
els.triple.innerHTML =
badge(t.aligned, 'Aligned', breaks + ' break(s)') +
' <span style="color:var(--muted);margin-left:0.5rem;font-size:0.88rem">line ' +
esc(t.lineId).slice(0, 18) +
'…</span>';
}
function renderSignoffs(data) {
const s = data.signoffs || {};
els.signoffs.innerHTML = '<pre class="raw" style="max-height:10rem">' + esc(JSON.stringify(s, null, 2)) + '</pre>';
}
function esc(s) {
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function setStatus(kind, msg) {
els.status.className = 'status-bar ' + kind;
els.status.textContent = msg;
}
async function load() {
setStatus('loading', 'Loading compliance console…');
try {
const res = await fetch(apiUrl('/omnl/compliance/console'), { headers: apiHeaders() });
const text = await res.text();
let data;
try {
data = JSON.parse(text);
} catch {
throw new Error(res.status + ' — non-JSON response');
}
if (!res.ok) throw new Error(data.error || res.statusText);
renderPosture(data);
renderActions(data.pendingActions);
renderEvidence(data);
renderSafe(data);
renderExternal(data);
renderTriple(data);
try {
const hoRes = await fetch(apiUrl('/omnl/ho-liquidity-snapshot'), { headers: apiHeaders() });
if (hoRes.ok) renderHoLiquidity(await hoRes.json());
} catch (_) {
renderHoLiquidity(null);
}
renderSignoffs(data);
els.raw.textContent = JSON.stringify(data, null, 2);
els.refreshed.textContent = 'Updated ' + new Date(data.generatedAt).toLocaleString();
setStatus('ok', 'Live — ' + data.pendingActions.length + ' pending action(s)');
} catch (e) {
setStatus('error', 'Failed to load: ' + (e && e.message ? e.message : e));
}
}
document.getElementById('btn-refresh').addEventListener('click', load);
document.getElementById('btn-download-safe').addEventListener('click', function () {
window.location.href = apiUrl('/omnl/compliance/safe-notary-gate-tx');
});
document.getElementById('btn-toggle-raw').addEventListener('click', function () {
document.getElementById('raw-section').classList.toggle('hidden');
});
load();
})();

View File

@@ -0,0 +1,46 @@
/** GUOSMM gazette notices — SSOT: config/guosmm-notices.v1.json */
export type GuosmmNoticeLink = {
documentId: string;
title: string;
url: string;
series: string;
entity?: string;
fineractOfficeId?: number;
};
export const GUOSMM_GAZETTE_BASE = 'https://gazette.shosjj.org/notices/2026';
export const GUOSMM_NOTICES: GuosmmNoticeLink[] = [
{
documentId: 'GUOSMM-B-2026-001',
series: 'B',
title: 'Central Bank Charter — OMNL',
url: `${GUOSMM_GAZETTE_BASE}/GUOSMM-B-2026-001`,
},
{
documentId: 'GUOSMM-C-2026-003',
series: 'C',
title: 'zBank — ISB Operations Charter',
url: `${GUOSMM_GAZETTE_BASE}/GUOSMM-C-2026-003`,
entity: 'zBank',
fineractOfficeId: 29,
},
{
documentId: 'GUOSMM-C-2026-004',
series: 'C',
title: 'NovaBank — ISB Operations Charter',
url: `${GUOSMM_GAZETTE_BASE}/GUOSMM-C-2026-004`,
entity: 'NovaBank',
fineractOfficeId: 30,
},
{
documentId: 'GUOSMM-C-2026-005',
series: 'C',
title: 'AnakaBank — ISB Operations Charter',
url: `${GUOSMM_GAZETTE_BASE}/GUOSMM-C-2026-005`,
entity: 'AnakaBank',
fineractOfficeId: 31,
},
];
export const GUOSMM_API_CATALOG = '/api/v1/omnl/gazette/notices';

View File

@@ -86,6 +86,8 @@ export const INTERNAL_PATH_GROUPS: InternalPathGroup[] = [
label: 'OMNL API',
entries: [
{ method: 'GET', path: '/api/v1/omnl/catalog', auth: 'none' },
{ method: 'GET', path: '/api/v1/omnl/gazette/notices', auth: 'none', notes: 'GUOSMM official gazette registry' },
{ method: 'GET', path: '/api/v1/omnl/gazette/notices/:documentId', auth: 'none' },
{ method: 'GET', path: '/api/v1/omnl/openapi.json', auth: 'none' },
{ method: 'GET', path: '/api/v1/omnl/health', auth: 'omnl-key-in-production' },
{ method: 'GET', path: '/api/v1/omnl/ipsas/fineract-health', auth: 'omnl-key-in-production' },

View File

@@ -0,0 +1,32 @@
import { GUOSMM_NOTICES } from '../../config/guosmmNotices';
export default function GuosmmNoticesPanel() {
return (
<section className="revolut-bank__gazette" aria-labelledby="guosmm-heading">
<h2 id="guosmm-heading" className="revolut-bank__section-title">
Official gazette (GUOSMM)
</h2>
<p className="text-xs text-[var(--nasa-muted)] mb-3 max-w-2xl">
Sovereign Military Order of Malta · SHOSJJ official notices governing OMNL EMCB and ISB member banks.
</p>
<ul className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{GUOSMM_NOTICES.map((n) => (
<li key={n.documentId}>
<a
href={n.url}
target="_blank"
rel="noopener noreferrer"
className="revolut-bank__service-card block !text-left"
>
<span className="revolut-bank__service-label">
{n.documentId}
{n.fineractOfficeId != null ? ` · Office ${n.fineractOfficeId}` : ''}
</span>
<span className="revolut-bank__service-desc">{n.title}</span>
</a>
</li>
))}
</ul>
</section>
);
}

View File

@@ -0,0 +1,242 @@
import { Link } from 'react-router-dom';
import NasaIcon from '../../components/icons/NasaIcon';
import { formatUsd } from '../../utils/formatMoney';
import CentralBankDashboard from '../central-bank/CentralBankDashboard';
import SettlementProtocolsPanel from './SettlementProtocolsPanel';
import GuosmmNoticesPanel from './GuosmmNoticesPanel';
import ZBankFullSettlementPanel from './ZBankFullSettlementPanel';
import { useOnlineBank } from './useOnlineBank';
const ACCOUNTS = [
{ key: 'main', label: 'USD account', meta: 'Main · M1 eMoney', flag: 'USD', balanceKey: 'primaryBalance' as const, active: true },
{ key: 'savings', label: 'Savings', meta: 'M2 tokenized', flag: 'M2', balanceKey: 'savingsBalance' as const },
{ key: 'reserve', label: 'Reserve', meta: 'M0 base money', flag: 'M0', balanceKey: 'reserveBalance' as const },
{ key: 'fiat', label: 'Fiat rails', meta: 'M4 SWIFT settlement', flag: 'M4', balanceKey: 'm4Balance' as const },
{ key: 'chain', label: 'On-chain', meta: 'M3 digital', flag: 'M3', balanceKey: 'm3Balance' as const },
];
function BalanceSkeleton() {
return (
<section className="revolut-bank__balance-card" aria-hidden>
<div className="revolut-bank__skeleton h-4 w-28 mb-3" />
<div className="revolut-bank__skeleton h-12 w-56" />
</section>
);
}
export default function OnlineBankApp() {
const bank = useOnlineBank();
return (
<div className="revolut-bank omnl-app">
<div className="revolut-bank__inner">
<header className="revolut-bank__page-header">
<div className="revolut-bank__logo-row">
<div className="revolut-bank__logo-mark" aria-hidden>
Z
</div>
<div className="revolut-bank__brand-block">
<h1 className="revolut-bank__brand-title">Z Online Bank</h1>
<p className="revolut-bank__greeting-label">
Hello · Good {timeOfDay()}, <span className="revolut-bank__greeting-name">{bank.displayName}</span>
</p>
</div>
</div>
<span className={`revolut-bank__status${bank.connected ? '' : ' revolut-bank__status--offline'}`}>
<NasaIcon name={bank.connected ? 'check' : 'x'} size={10} />
{bank.connected ? 'Live' : 'Offline'}
</span>
</header>
{bank.loading && !bank.primaryBalance ? (
<BalanceSkeleton />
) : (
<section className="revolut-bank__balance-card" aria-label="Total balance">
<p className="revolut-bank__balance-label">Total balance</p>
<p className="revolut-bank__balance-amount">{formatUsd(bank.primaryBalance)}</p>
<div className="revolut-bank__balance-sub">
<span>{bank.officeLabel}</span>
{bank.live && <span>Synced with Fineract</span>}
</div>
</section>
)}
<div className="revolut-bank__cta-row">
<a href="#z-full-settlement" className="revolut-bank__cta revolut-bank__cta--primary">
<NasaIcon name="money" size={18} />
Send
</a>
<a href="#z-full-settlement" className="revolut-bank__cta revolut-bank__cta--secondary">
<NasaIcon name="plus" size={18} />
Add money
</a>
</div>
<section className="revolut-bank__accounts-section" aria-label="Your accounts">
<h2 className="revolut-bank__accounts-heading">Your accounts</h2>
<div className="revolut-bank__accounts">
{ACCOUNTS.map((acct) => (
<div
key={acct.key}
className={`revolut-bank__account-pill${acct.active ? ' revolut-bank__account-pill--active' : ''}`}
>
<span className="revolut-bank__account-flag">{acct.flag}</span>
<div className="revolut-bank__account-pill-body">
<p className="revolut-bank__account-pill-label">{acct.label}</p>
<p className="revolut-bank__account-pill-meta">{acct.meta}</p>
</div>
<p className="revolut-bank__account-pill-value">{formatUsd(bank[acct.balanceKey])}</p>
</div>
))}
</div>
</section>
<nav className="revolut-bank__actions" aria-label="Quick actions">
<Link to="/swap" className="revolut-bank__action">
<span className="revolut-bank__action-icon">
<NasaIcon name="swap" size={20} />
</span>
Convert
</Link>
<Link to="/trade" className="revolut-bank__action">
<span className="revolut-bank__action-icon">
<NasaIcon name="trade" size={20} />
</span>
Invest
</Link>
<a href="#z-settlement-protocols" className="revolut-bank__action">
<span className="revolut-bank__action-icon">
<NasaIcon name="globe" size={20} />
</span>
International
</a>
<Link to="/office-24" className="revolut-bank__action">
<span className="revolut-bank__action-icon">
<NasaIcon name="office" size={20} />
</span>
O24 Central Bank
</Link>
</nav>
<ZBankFullSettlementPanel
onSuccess={() => bank.refresh()}
balances={{
m0: bank.reserveBalance,
m1: bank.primaryBalance,
m2: bank.savingsBalance,
m3: bank.m3Balance,
m4: bank.m4Balance,
}}
/>
<SettlementProtocolsPanel onSuccess={() => bank.refresh()} />
{bank.error && (
<div className="revolut-bank__activity revolut-bank__error mb-4 p-4 text-sm">
{bank.error}. Showing cached layout retrying every 30s.
</div>
)}
<section aria-labelledby="activity-heading">
<div className="revolut-bank__section-title">
<h2 id="activity-heading">Transactions</h2>
<button
type="button"
className="revolut-bank__section-link border-0 bg-transparent cursor-pointer"
onClick={() => bank.refresh()}
>
See all
</button>
</div>
<div className="revolut-bank__activity">
{bank.activities.length === 0 ? (
<div className="revolut-bank__activity-item">
<span className="revolut-bank__activity-icon">
<NasaIcon name="ledger" size={16} />
</span>
<div className="revolut-bank__activity-body">
<p className="revolut-bank__activity-title">No transactions yet</p>
<p className="revolut-bank__activity-meta">Swaps and settlements appear here</p>
</div>
</div>
) : (
bank.activities.map((item) => (
<div key={item.id} className="revolut-bank__activity-item">
<span
className={`revolut-bank__activity-icon${item.kind === 'swap' ? ' revolut-bank__activity-icon--swap' : ''}`}
>
<NasaIcon name={item.kind === 'swap' ? 'swap' : 'check'} size={16} />
</span>
<div className="revolut-bank__activity-body">
<p className="revolut-bank__activity-title">{item.title}</p>
<p className="revolut-bank__activity-meta">{item.meta}</p>
</div>
<span
className={`revolut-bank__activity-amount${item.positive ? ' revolut-bank__activity-amount--positive' : ''}`}
>
{item.amount}
</span>
</div>
))
)}
</div>
</section>
<section aria-labelledby="services-heading">
<h2 id="services-heading" className="revolut-bank__section-title">
Explore
</h2>
<div className="revolut-bank__services">
<Link to="/swap" className="revolut-bank__service-card">
<span className="revolut-bank__service-label">Digital Swap</span>
<span className="revolut-bank__service-desc">Convert & load M2 tokens</span>
</Link>
<Link to="/trade" className="revolut-bank__service-card">
<span className="revolut-bank__service-label">DBIS Trade</span>
<span className="revolut-bank__service-desc">Spot markets</span>
</Link>
<Link to="/office-24" className="revolut-bank__service-card">
<span className="revolut-bank__service-label">Office 24 Central Bank</span>
<span className="revolut-bank__service-desc">128-bank corridors · IPSAS</span>
</Link>
<Link to="/wallets" className="revolut-bank__service-card">
<span className="revolut-bank__service-label">OneX Wallet</span>
<span className="revolut-bank__service-desc">Shiva blockchain · MPC wallet</span>
</Link>
<a href="#z-settlement-protocols" className="revolut-bank__service-card">
<span className="revolut-bank__service-label">OneX Bank</span>
<span className="revolut-bank__service-desc">HYBX corridor · office 29</span>
</a>
<a href="#z-settlement-protocols" className="revolut-bank__service-card">
<span className="revolut-bank__service-label">Nova Bank Online</span>
<span className="revolut-bank__service-desc">Import wires · M4</span>
</a>
<Link to="/reserve" className="revolut-bank__service-card">
<span className="revolut-bank__service-label">Reserve</span>
<span className="revolut-bank__service-desc">Coverage & attestation</span>
</Link>
</div>
</section>
<GuosmmNoticesPanel />
<details className="revolut-bank__treasury">
<summary>
Treasury & operator console
<NasaIcon name="arrow-right" size={14} className="revolut-bank__chevron" />
</summary>
<div className="revolut-bank__treasury-body">
<CentralBankDashboard />
</div>
</details>
</div>
</div>
);
}
function timeOfDay(): string {
const h = new Date().getHours();
if (h < 12) return 'morning';
if (h < 17) return 'afternoon';
return 'evening';
}

View File

@@ -0,0 +1,366 @@
import { useEffect, useState } from 'react';
import { useAccount } from 'wagmi';
import { useZBankSettlement, type SettlementPhase } from './useZBankSettlement';
import { formatUsd } from '../../utils/formatMoney';
const PHASE_LABEL: Record<SettlementPhase, string> = {
RECEIVED: 'Received',
VALIDATED: 'Validated',
VERBIAGE_ROLLED: 'Verbiage rolled',
FINERACT_POSTED: 'Fineract posted',
HYBX_RAIL_DISPATCHED: 'HYBX dispatched',
ISO20022_ARCHIVED: 'ISO 20022 archived',
CHAIN_MINT_REQUESTED: 'Chain mint',
SETTLED: 'Settled',
FAILED: 'Failed',
};
const DEFAULT_IBAN = 'IQ98ZARD00000000000000000001';
const BILLION = '1000000000';
type ProtocolGroup = 'classic' | 'digital' | 'trade' | 'corridor';
type ProtocolId =
| 'mt103'
| 'mt102'
| 'mt103-lc'
| 'sblc'
| 'bg'
| 'nova-zbank'
| 'onex-zbank'
| 'onex-shiva-crypto'
| 'o24-zbank'
| 'internal'
| 'external'
| 'token-load'
| 'fiat-crypto'
| 'full-external';
const GROUPS: { id: ProtocolGroup; label: string; desc: string }[] = [
{ id: 'classic', label: 'Classic bank', desc: 'SWIFT wires · MT102/MT103 · correspondent banking' },
{ id: 'trade', label: 'Trade finance', desc: 'SBLC · BG · Documentary LC (MT103_LC)' },
{ id: 'digital', label: 'Digital bank', desc: 'M2/M3 · token load · fiat→crypto · on-chain' },
{ id: 'corridor', label: 'Corridor import', desc: 'Nova · OneX Bank · Office 24 · Shiva blockchain' },
];
const PROTOCOLS: Record<ProtocolGroup, { id: ProtocolId; label: string }[]> = {
classic: [
{ id: 'mt103', label: 'MT103 wire' },
{ id: 'mt102', label: 'MT102 bulk' },
],
trade: [
{ id: 'mt103-lc', label: 'MT103_LC (DLC)' },
{ id: 'sblc', label: 'SBLC' },
{ id: 'bg', label: 'Bank guarantee' },
],
digital: [
{ id: 'internal', label: 'Internal M2/M3' },
{ id: 'token-load', label: 'M2 token load' },
{ id: 'fiat-crypto', label: 'Fiat → crypto (138)' },
{ id: 'onex-shiva-crypto', label: 'Fiat → Shiva (OneX)' },
{ id: 'external', label: 'M4 SWIFT out' },
],
corridor: [
{ id: 'nova-zbank', label: 'Nova Bank → Z' },
{ id: 'onex-zbank', label: 'OneX Bank → Z' },
{ id: 'o24-zbank', label: 'O24 CB → Z' },
{ id: 'full-external', label: 'Full M0M4' },
],
};
export default function SettlementProtocolsPanel({ onSuccess }: { onSuccess?: () => void }) {
const { address, isConnected } = useAccount();
const s = useZBankSettlement();
const [group, setGroup] = useState<ProtocolGroup>('classic');
const [active, setActive] = useState<ProtocolId>('mt103');
const [amount, setAmount] = useState(BILLION);
const [iban, setIban] = useState(DEFAULT_IBAN);
const [recipient, setRecipient] = useState('');
const [tfReference, setTfReference] = useState('');
const [tokenSymbol, setTokenSymbol] = useState('cUSDC');
const [tokenAddress, setTokenAddress] = useState('0x71D6687F38b93CCad569Fa6352c876eea967201b');
const [lineId, setLineId] = useState('USD-M2');
useEffect(() => {
if (address) setRecipient(address);
s.refreshSettlements();
}, [address]);
useEffect(() => {
const first = PROTOCOLS[group][0]?.id;
if (first) setActive(first);
}, [group]);
async function submit() {
try {
switch (active) {
case 'mt103':
await s.swiftMessageTransfer({
amount: String(amount),
creditorIban: iban,
swiftMessageKind: 'MT103',
});
break;
case 'mt102':
await s.swiftMessageTransfer({
amount: String(amount),
creditorIban: iban,
swiftMessageKind: 'MT102',
});
break;
case 'mt103-lc':
await s.mt103LcTransfer(String(amount), iban, tfReference || undefined);
break;
case 'sblc':
await s.tradeFinanceTransfer({
instrument: 'SBLC',
amount: String(amount),
creditorIban: iban,
reference: tfReference || undefined,
});
break;
case 'bg':
await s.tradeFinanceTransfer({
instrument: 'BG',
amount: String(amount),
creditorIban: iban,
reference: tfReference || undefined,
});
break;
case 'nova-zbank':
await s.transferNovaBankToZBank(String(amount), iban);
break;
case 'onex-zbank':
await s.transferOneXBankToZBank(String(amount), iban);
break;
case 'o24-zbank':
await s.transferOffice24ToZBank(String(amount), iban);
break;
case 'internal':
if (!recipient) throw new Error('Connect wallet for recipient');
await s.internalTransfer({
tokenAddress,
tokenSymbol,
amount: String(amount),
recipientAddress: recipient,
senderAddress: address,
});
break;
case 'external':
if (!recipient) throw new Error('Recipient address required');
await s.externalTransfer({
tokenAddress,
tokenSymbol,
amount: String(amount),
recipientAddress: recipient,
creditorIban: iban,
beneficiaryName: 'Z Online Bank',
});
break;
case 'token-load':
if (!recipient) throw new Error('Connect wallet for mint recipient');
await s.tokenLoad({
lineId,
symbol: tokenSymbol,
tokenAddress,
amount: String(amount),
recipientAddress: recipient,
});
break;
case 'fiat-crypto':
if (!recipient) throw new Error('Wallet recipient required');
await s.fiatToCrypto({
amount: String(amount),
creditorIban: iban,
recipientAddress: recipient,
tokenLineId: lineId,
});
break;
case 'onex-shiva-crypto':
if (!recipient) throw new Error('OneX wallet recipient required');
await s.fiatToCryptoShiva({
amount: String(amount),
creditorIban: iban,
recipientAddress: recipient,
tokenLineId: lineId,
});
break;
case 'full-external':
await s.fullExternalTransfer({
amount: String(amount),
creditorIban: iban,
beneficiaryName: 'Z Online Bank',
});
break;
}
await s.refreshSettlements();
onSuccess?.();
} catch {
/* error in hook */
}
}
const needsWallet = ['internal', 'external', 'token-load', 'fiat-crypto', 'onex-shiva-crypto'].includes(active);
const needsTfRef = ['mt103-lc', 'sblc', 'bg'].includes(active);
const simpleWire = ['mt103', 'mt102', 'mt103-lc', 'sblc', 'bg', 'nova-zbank', 'onex-zbank', 'o24-zbank', 'full-external'].includes(
active,
);
return (
<section className="revolut-bank__protocols" id="z-settlement-protocols" aria-label="Banking protocols">
<h2 className="revolut-bank__section-title">Banking protocols</h2>
{!import.meta.env.VITE_OMNL_API_KEY && (
<p className="revolut-bank__protocols-hint">
Settlement auth: set OMNL_API_KEY in repo .env and rebuild, or use online.omdnl.org portal.
</p>
)}
<p className="revolut-bank__protocols-hint">
Classic · trade finance · digital · 128-bank corridors · OneX Bank · Shiva blockchain · HYBX · Chain 138
</p>
<div className="zbank-settle__protocol-groups" role="tablist" aria-label="Protocol groups">
{GROUPS.map((g) => (
<button
key={g.id}
type="button"
role="tab"
aria-selected={group === g.id}
className={`zbank-settle__protocol-group${group === g.id ? ' zbank-settle__protocol-group--active' : ''}`}
onClick={() => setGroup(g.id)}
>
<span className="zbank-settle__protocol-group-label">{g.label}</span>
<span className="zbank-settle__protocol-group-desc">{g.desc}</span>
</button>
))}
</div>
<div className="revolut-bank__protocol-tabs" role="tablist">
{PROTOCOLS[group].map((t) => (
<button
key={t.id}
type="button"
role="tab"
aria-selected={active === t.id}
className={`revolut-bank__protocol-tab${active === t.id ? ' revolut-bank__protocol-tab--active' : ''}`}
onClick={() => setActive(t.id)}
>
{t.label}
</button>
))}
</div>
<div className="revolut-bank__protocol-form">
<label className="revolut-bank__field">
<span>Amount (USD)</span>
<input
type="text"
inputMode="decimal"
value={amount}
onChange={(e) => setAmount(e.target.value)}
className="revolut-bank__input"
/>
</label>
<label className="revolut-bank__field">
<span>Creditor IBAN</span>
<input type="text" value={iban} onChange={(e) => setIban(e.target.value)} className="revolut-bank__input" />
</label>
{needsTfRef && (
<label className="revolut-bank__field">
<span>Trade finance reference</span>
<input
type="text"
value={tfReference}
onChange={(e) => setTfReference(e.target.value)}
placeholder="LC / SBLC / BG reference"
className="revolut-bank__input"
/>
</label>
)}
{!simpleWire && (
<>
{needsWallet && (
<label className="revolut-bank__field">
<span>Recipient wallet</span>
<input
type="text"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
placeholder={isConnected ? address : '0x…'}
className="revolut-bank__input"
/>
</label>
)}
{(active === 'internal' || active === 'external' || active === 'token-load') && (
<>
<label className="revolut-bank__field">
<span>Token symbol</span>
<input
type="text"
value={tokenSymbol}
onChange={(e) => setTokenSymbol(e.target.value)}
className="revolut-bank__input"
/>
</label>
<label className="revolut-bank__field">
<span>Token address</span>
<input
type="text"
value={tokenAddress}
onChange={(e) => setTokenAddress(e.target.value)}
className="revolut-bank__input"
/>
</label>
</>
)}
{(active === 'token-load' || active === 'fiat-crypto') && (
<label className="revolut-bank__field">
<span>OMNL line ID</span>
<input type="text" value={lineId} onChange={(e) => setLineId(e.target.value)} className="revolut-bank__input" />
</label>
)}
</>
)}
</div>
{s.error && (
<div className="revolut-bank__protocol-error" role="alert">
{s.error}
</div>
)}
{s.lastResult?.settlementId && (
<div className="revolut-bank__protocol-result">
<p>
<strong>Settlement:</strong> {s.lastResult.settlementId}
</p>
<p>
<strong>Phase:</strong> {s.lastResult.phase ? PHASE_LABEL[s.lastResult.phase] : '—'}
</p>
{s.lastResult.fineractJournalRef && (
<p>
<strong>Fineract:</strong> {s.lastResult.fineractJournalRef}
</p>
)}
</div>
)}
<button type="button" className="revolut-bank__submit" disabled={s.loading} onClick={() => submit()}>
{s.loading ? 'Processing settlement…' : `Execute · ${PROTOCOLS[group].find((p) => p.id === active)?.label ?? active}`}
</button>
{s.settlements.length > 0 && (
<div className="revolut-bank__settlements-list">
<h3 className="revolut-bank__section-title">Recent settlements</h3>
<ul>
{s.settlements.slice(0, 8).map((row) => (
<li key={row.settlementId}>
<span>{row.settlementId?.slice(0, 12)}</span>
<span>{row.phase ? PHASE_LABEL[row.phase] : '—'}</span>
</li>
))}
</ul>
</div>
)}
</section>
);
}

View File

@@ -0,0 +1,204 @@
import { useEffect, useState } from 'react';
import { useAccount } from 'wagmi';
import { useZBankSettlement, type SettlementPhase } from './useZBankSettlement';
import { formatUsd } from '../../utils/formatMoney';
const PHASE_LABEL: Record<SettlementPhase, string> = {
RECEIVED: 'Received',
VALIDATED: 'Validated',
VERBIAGE_ROLLED: 'Verbiage rolled',
FINERACT_POSTED: 'Fineract posted',
HYBX_RAIL_DISPATCHED: 'HYBX dispatched',
ISO20022_ARCHIVED: 'ISO 20022 archived',
CHAIN_MINT_REQUESTED: 'Chain mint',
SETTLED: 'Settled',
FAILED: 'Failed',
};
const DEFAULT_IBAN = 'IQ98ZARD00000000000000000001';
const LAYERS = [
{ id: 'M0', label: 'M0 Reserve', gl: '1050', desc: 'Base money · IPSAS reserve' },
{ id: 'M1', label: 'M1 eMoney', gl: '2100', desc: 'Circulating liabilities' },
{ id: 'M2', label: 'M2 Broad', gl: '2200', desc: 'Meta-fiat broad money' },
{ id: 'M3', label: 'M3 On-chain', gl: '2300', desc: 'Token liabilities · Chain 138' },
{ id: 'M4', label: 'M4 Fiat rails', gl: '1000', desc: 'SWIFT · HYBX · settlement suspense' },
] as const;
type Props = {
onSuccess?: () => void;
balances?: {
m0?: string;
m1?: string;
m2?: string;
m3?: string;
m4?: string;
};
};
export default function ZBankFullSettlementPanel({ onSuccess, balances }: Props) {
const { address, isConnected } = useAccount();
const s = useZBankSettlement();
const [amount, setAmount] = useState('1000000000');
const [iban, setIban] = useState(DEFAULT_IBAN);
const [recipient, setRecipient] = useState('');
const [includeM3, setIncludeM3] = useState(true);
useEffect(() => {
if (address) setRecipient(address);
s.refreshSettlements();
}, [address]);
async function submit() {
try {
await s.zBankSettleM0ToM4({
amount: String(amount),
creditorIban: iban,
recipientAddress: includeM3 ? recipient : undefined,
});
await s.refreshSettlements();
onSuccess?.();
} catch {
/* hook sets error */
}
}
const layerSteps = s.lastResult?.layerSteps ?? [];
const activeLayerIdx = layerSteps.filter((st) => st.status === 'posted').length;
return (
<section className="zbank-settle" id="z-full-settlement" aria-label="M0 to M4 settlement">
<h2 className="revolut-bank__section-title">Full settlement · M0 M4</h2>
<p className="revolut-bank__protocols-hint">
Posts sequential Fineract journals through every OMNL money layer, then dispatches HYBX + SWIFT MT103 to your Z
Online Bank IBAN.
</p>
<div className="zbank-settle__pipeline" role="list" aria-label="Money layer pipeline">
{LAYERS.map((layer, i) => {
const posted = layerSteps.find((st) => st.to === layer.id)?.status === 'posted';
const active = s.loading && i === activeLayerIdx;
const done = posted || (s.lastResult?.phase === 'SETTLED' && i <= LAYERS.length - 1);
return (
<div key={layer.id} className="zbank-settle__layer-wrap" role="listitem">
{i > 0 && <div className={`zbank-settle__connector${done ? ' zbank-settle__connector--done' : ''}`} />}
<div
className={`zbank-settle__layer${done ? ' zbank-settle__layer--done' : ''}${active ? ' zbank-settle__layer--active' : ''}`}
>
<span className="zbank-settle__layer-id">{layer.id}</span>
<span className="zbank-settle__layer-name">{layer.label}</span>
<span className="zbank-settle__layer-gl">GL {layer.gl}</span>
{balances && (
<span className="zbank-settle__layer-bal">
{formatUsd(
layer.id === 'M0'
? balances.m0
: layer.id === 'M1'
? balances.m1
: layer.id === 'M2'
? balances.m2
: layer.id === 'M3'
? balances.m3
: balances.m4,
)}
</span>
)}
<span className="zbank-settle__layer-desc">{layer.desc}</span>
</div>
</div>
);
})}
</div>
<div className="revolut-bank__protocol-form">
<label className="revolut-bank__field">
<span>Settlement amount (USD)</span>
<input
type="text"
inputMode="decimal"
value={amount}
onChange={(e) => setAmount(e.target.value)}
className="revolut-bank__input"
/>
</label>
<label className="revolut-bank__field">
<span>Z Online Bank IBAN (creditor)</span>
<input type="text" value={iban} onChange={(e) => setIban(e.target.value)} className="revolut-bank__input" />
</label>
<label className="revolut-bank__field zbank-settle__checkbox">
<input type="checkbox" checked={includeM3} onChange={(e) => setIncludeM3(e.target.checked)} />
<span>Include M3 on-chain mint (M2M3M4 path)</span>
</label>
{includeM3 && (
<label className="revolut-bank__field">
<span>M3 recipient wallet (Chain 138)</span>
<input
type="text"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
placeholder={isConnected ? address : '0x…'}
className="revolut-bank__input"
/>
</label>
)}
</div>
{layerSteps.length > 0 && (
<div className="zbank-settle__steps">
<h3 className="zbank-settle__steps-title">Layer journals</h3>
<ul>
{layerSteps.map((step) => (
<li key={`${step.from}-${step.to}`}>
<span>
{step.from}{step.to}
</span>
<span>
Dr {step.debitGl} / Cr {step.creditGl}
</span>
<span className={`zbank-settle__step-status zbank-settle__step-status--${step.status}`}>
{step.status}
{step.journalRef ? ` · JE ${step.journalRef}` : ''}
</span>
</li>
))}
</ul>
</div>
)}
{s.error && (
<div className="revolut-bank__protocol-error" role="alert">
{s.error}
</div>
)}
{s.lastResult?.settlementId && (
<div className="revolut-bank__protocol-result">
<p>
<strong>Settlement:</strong> {s.lastResult.settlementId}
</p>
<p>
<strong>Phase:</strong> {s.lastResult.phase ? PHASE_LABEL[s.lastResult.phase] : '—'}
</p>
{s.lastResult.fineractJournalRef && (
<p>
<strong>Last journal:</strong> {s.lastResult.fineractJournalRef}
</p>
)}
{s.lastResult.hybxPaymentId && (
<p>
<strong>HYBX:</strong> {s.lastResult.hybxPaymentId}
</p>
)}
{s.lastResult.chainTxHash && (
<p>
<strong>Chain mint:</strong> {s.lastResult.chainTxHash}
</p>
)}
</div>
)}
<button type="button" className="revolut-bank__submit" disabled={s.loading} onClick={() => submit()}>
{s.loading ? 'Settling M0→M4…' : `Settle ${formatUsd(amount)} through M0M4`}
</button>
</section>
);
}

View File

@@ -0,0 +1,77 @@
import { useMemo } from 'react';
import { useCentralBank } from '../central-bank/useCentralBank';
import { formatUsd } from '../../utils/formatMoney';
export type BankActivity = {
id: string;
title: string;
meta: string;
amount: string;
positive?: boolean;
kind: 'swap' | 'transfer' | 'system';
};
export function useOnlineBank() {
const cb = useCentralBank();
const office = cb.office as {
officeId?: number;
name?: string;
externalId?: string;
} | null;
const healthOk = (cb.health as { status?: string } | null)?.status === 'ok';
const live = Boolean(cb.moneySupply?.fineractLive);
const connected = Boolean(cb.moneySupply?.fineractConnected) || healthOk;
const primaryBalance = cb.moneySupply?.M1?.circulating ?? cb.moneySupply?.M2?.broadMoney;
const savingsBalance = cb.moneySupply?.M2?.broadMoney;
const reserveBalance = cb.moneySupply?.M0?.gl1050;
const m4Balance = cb.moneySupply?.M4?.nearMoney;
const m3Balance = cb.moneySupply?.M3?.tokenLiabilities;
const activities = useMemo<BankActivity[]>(() => {
const items: BankActivity[] = [];
const swaps = (cb.monitor as { swaps?: { status: string; tokenIn: string; tokenOut?: string; amountIn?: string; createdAt: string }[] } | null)?.swaps ?? [];
for (const [i, s] of swaps.slice(0, 12).entries()) {
items.push({
id: `swap-${i}`,
title: `Exchange · ${s.tokenIn?.slice(0, 8) ?? 'token'}`,
meta: `${s.status} · ${s.createdAt?.slice(0, 16) ?? 'recent'}`,
amount: s.amountIn ? formatUsd(s.amountIn) : '—',
positive: s.status?.toLowerCase() === 'completed',
kind: 'swap',
});
}
if (connected && items.length === 0) {
items.push({
id: 'system-live',
title: 'Z Online Bank connected',
meta: live ? 'Fineract ledger live' : 'Settlement middleware online',
amount: formatUsd(primaryBalance),
positive: true,
kind: 'system',
});
}
return items;
}, [cb.monitor, connected, live, primaryBalance]);
return {
...cb,
office,
healthOk,
live,
connected,
primaryBalance,
savingsBalance,
reserveBalance,
m4Balance,
m3Balance,
activities,
displayName: office?.name?.split('—')[0]?.trim() ?? 'DBIS Account',
officeLabel: office?.externalId ?? `Office ${office?.officeId ?? 24}`,
};
}

View File

@@ -0,0 +1,455 @@
import { useCallback, useState } from 'react';
import { exchangeApi, omnlApiHeaders, settlementApi } from '../../config/dex';
import { onexShiva, SHIVA_ONEX_CHAIN_ID } from '../../config/onexShiva';
export type SettlementPhase =
| 'RECEIVED'
| 'VALIDATED'
| 'VERBIAGE_ROLLED'
| 'FINERACT_POSTED'
| 'HYBX_RAIL_DISPATCHED'
| 'ISO20022_ARCHIVED'
| 'CHAIN_MINT_REQUESTED'
| 'SETTLED'
| 'FAILED';
export type SettlementResult = {
settlementId?: string;
phase?: SettlementPhase;
errors?: string[];
fineractJournalRef?: string;
hybxPaymentId?: string;
chainTxHash?: string;
error?: string;
layerSteps?: {
from: string;
to: string;
debitGl: string;
creditGl: string;
journalRef?: string;
status: 'pending' | 'posted' | 'skipped' | 'failed';
}[];
};
async function postJson<T>(url: string, body: unknown): Promise<{ ok: boolean; status: number; data: T }> {
const res = await fetch(url, {
method: 'POST',
headers: omnlApiHeaders(),
body: JSON.stringify(body),
});
const data = (await res.json().catch(() => ({ error: res.statusText || `HTTP ${res.status}` }))) as T;
return { ok: res.ok, status: res.status, data };
}
async function getJson<T>(url: string): Promise<T | null> {
try {
const res = await fetch(url, { headers: omnlApiHeaders(false) });
return res.ok ? ((await res.json()) as T) : null;
} catch {
return null;
}
}
function idem(prefix: string) {
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
}
/** Nova Bank Online → Z Online Bank: inbound wire import to M4 fiat */
export async function novaBankToZBankM4Fiat(params: {
amount: string;
creditorIban: string;
beneficiaryName?: string;
remittanceInfo?: string;
}) {
const base = {
idempotencyKey: idem('NOVA-Z-M4'),
officeId: 24,
currency: 'USD',
amount: params.amount,
creditorIban: params.creditorIban,
beneficiaryName: params.beneficiaryName ?? 'Z Online Bank',
orderingName: 'Nova Bank Online',
creditorBic: 'NOVAGB2LXXX',
debtorIban: 'GB29NOVA60161331926819',
remittanceInfo: params.remittanceInfo ?? 'Nova Bank Online → Z Online Bank · M4 fiat import',
moneyLayers: ['M1', 'M2', 'M4'] as const,
rail: 'SWIFT' as const,
swiftMessageKind: 'MT103' as const,
};
return postJson<SettlementResult>(settlementApi('/external-transfer'), base);
}
/** OneX Bank (Shiva / zBank office 29) → Z Online Bank: inbound wire to M4 */
export async function onexBankToZBankM4Fiat(params: {
amount: string;
creditorIban: string;
beneficiaryName?: string;
remittanceInfo?: string;
}) {
const corridor = onexShiva.bankCorridor;
const base = {
idempotencyKey: idem('ONEX-Z-M4'),
officeId: onexShiva.settlement.zBridgeOfficeId,
currency: 'USD',
amount: params.amount,
creditorIban: params.creditorIban,
beneficiaryName: params.beneficiaryName ?? 'Z Online Bank',
orderingName: corridor.orderingName,
creditorBic: corridor.creditorBic,
debtorIban: corridor.debtorIban,
remittanceInfo: params.remittanceInfo ?? corridor.remittanceTemplate,
moneyLayers: ['M1', 'M2', 'M4'] as const,
rail: 'HYBX' as const,
swiftMessageKind: corridor.swiftMessageKind as 'MT103',
metadata: {
sourceOfficeId: onexShiva.fineract.officeId,
sourceHybxWallet: onexShiva.fineract.hybxWalletId,
targetChainId: SHIVA_ONEX_CHAIN_ID,
integration: 'onex-shiva',
},
};
return postJson<SettlementResult>(settlementApi('/external-transfer'), base);
}
/** Office 24 → Z Online Bank: credit M1 then settle to M4 fiat rails */
export async function office24ToZBankM4Fiat(params: {
amount: string;
creditorIban: string;
beneficiaryName?: string;
remittanceInfo?: string;
}) {
const base = {
idempotencyKey: idem('Z-O24-M4'),
officeId: 24,
currency: 'USD',
amount: params.amount,
creditorIban: params.creditorIban,
beneficiaryName: params.beneficiaryName ?? 'Z Online Bank',
orderingName: 'Office 24 Central Bank — HOSPITALLERS',
remittanceInfo: params.remittanceInfo ?? 'Office 24 → Z Online Bank · M4 fiat settlement',
moneyLayers: ['M1', 'M2', 'M4'] as const,
rail: 'SWIFT' as const,
swiftMessageKind: 'MT103' as const,
};
return postJson<SettlementResult>(settlementApi('/external-transfer'), base);
}
export function useZBankSettlement() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [lastResult, setLastResult] = useState<SettlementResult | null>(null);
const [settlements, setSettlements] = useState<SettlementResult[]>([]);
const run = useCallback(async (label: string, fn: () => Promise<{ ok: boolean; status: number; data: SettlementResult }>) => {
setLoading(true);
setError(null);
try {
const { ok, data, status } = await fn();
setLastResult(data);
if (!ok || data.phase === 'FAILED') {
const errBody = data as SettlementResult & { message?: string };
throw new Error(
errBody.error || errBody.errors?.join(', ') || errBody.message || `${label} failed (HTTP ${status})`,
);
}
return data;
} catch (e) {
const msg = e instanceof Error ? e.message : `${label} failed`;
setError(msg);
throw e;
} finally {
setLoading(false);
}
}, []);
const transferNovaBankToZBank = useCallback(
(amount: string, creditorIban: string) =>
run('Nova Bank Online → Z Online Bank M4 import', async () => {
const r = await novaBankToZBankM4Fiat({ amount, creditorIban });
return { ok: r.ok, status: r.status, data: r.data };
}),
[run],
);
const transferOffice24ToZBank = useCallback(
(amount: string, creditorIban: string) =>
run('Office 24 → Z Online Bank M4 transfer', async () => {
const r = await office24ToZBankM4Fiat({ amount, creditorIban });
return { ok: r.ok, status: r.status, data: r.data };
}),
[run],
);
const transferOneXBankToZBank = useCallback(
(amount: string, creditorIban: string) =>
run('OneX Bank → Z Online Bank M4 import (Shiva)', async () => {
const r = await onexBankToZBankM4Fiat({ amount, creditorIban });
return { ok: r.ok, status: r.status, data: r.data };
}),
[run],
);
const internalTransfer = useCallback(
(body: {
tokenAddress: string;
tokenSymbol: string;
amount: string;
recipientAddress: string;
senderAddress?: string;
}) =>
run('Internal transfer', async () => {
const r = await postJson<SettlementResult>(exchangeApi('/transfer/internal'), {
idempotencyKey: idem('Z-INT'),
...body,
moneyLayers: ['M2', 'M3'],
});
return { ok: r.ok, status: r.status, data: r.data };
}),
[run],
);
const externalTransfer = useCallback(
(body: {
tokenAddress: string;
tokenSymbol: string;
amount: string;
recipientAddress: string;
creditorIban: string;
beneficiaryName?: string;
}) =>
run('External M4 transfer', async () => {
const r = await postJson<SettlementResult>(exchangeApi('/transfer/external'), {
idempotencyKey: idem('Z-EXT'),
...body,
moneyLayers: ['M2', 'M4'],
rail: 'SWIFT',
remittanceInfo: 'Z Online Bank M4 fiat outbound',
});
return { ok: r.ok, status: r.status, data: r.data };
}),
[run],
);
const tokenLoad = useCallback(
(body: {
lineId: string;
symbol: string;
tokenAddress: string;
amount: string;
recipientAddress: string;
}) =>
run('M2 token load', async () => {
const r = await postJson<SettlementResult>(exchangeApi('/token-load'), {
idempotencyKey: idem('Z-LOAD'),
currency: 'USD',
...body,
});
return { ok: r.ok, status: r.status, data: r.data };
}),
[run],
);
const fiatToCrypto = useCallback(
(body: {
amount: string;
creditorIban: string;
recipientAddress: string;
tokenLineId?: string;
targetChainId?: number;
}) =>
run('Fiat to crypto', async () => {
const chainId = body.targetChainId ?? 138;
const r = await postJson<SettlementResult>(settlementApi('/convert/fiat-to-crypto'), {
idempotencyKey: idem('Z-F2C'),
officeId: 24,
currency: 'USD',
amount: body.amount,
creditorIban: body.creditorIban,
beneficiaryName: 'Z Online Bank',
recipientAddress: body.recipientAddress,
convertToCrypto: {
enabled: true,
targetChainId: chainId,
tokenLineId: body.tokenLineId ?? 'USD-M2',
recipientAddress: body.recipientAddress,
},
moneyLayers: ['M2', 'M3'],
});
return { ok: r.ok, status: r.status, data: r.data };
}),
[run],
);
const fiatToCryptoShiva = useCallback(
(body: { amount: string; creditorIban: string; recipientAddress: string; tokenLineId?: string }) =>
fiatToCrypto({ ...body, targetChainId: SHIVA_ONEX_CHAIN_ID }),
[fiatToCrypto],
);
const zBankSettleM0ToM4 = useCallback(
(body: { amount: string; creditorIban: string; recipientAddress?: string; tokenLineId?: string }) =>
run('Z Online Bank M0→M4 settlement', async () => {
const payload = {
idempotencyKey: idem('Z-M0M4'),
officeId: 24,
currency: 'USD',
amount: body.amount,
creditorIban: body.creditorIban,
beneficiaryName: 'Z Online Bank',
remittanceInfo: 'Z Online Bank full-stack settlement M0→M1→M2→M3→M4',
recipientAddress: body.recipientAddress,
tokenLineId: body.tokenLineId ?? 'USD-M2',
};
let r = await postJson<SettlementResult>(settlementApi('/z-bank/settle-m0-m4'), payload);
if (r.status === 404 || r.status === 405) {
r = await postJson<SettlementResult>(settlementApi('/external-transfer'), {
...payload,
moneyLayers: ['M0', 'M1', 'M2', 'M3', 'M4'],
rail: 'SWIFT',
swiftMessageKind: 'MT103',
convertToCrypto: body.recipientAddress
? {
enabled: true,
targetChainId: 138,
tokenLineId: body.tokenLineId ?? 'USD-M2',
recipientAddress: body.recipientAddress,
}
: undefined,
});
}
return { ok: r.ok, status: r.status, data: r.data };
}),
[run],
);
const tradeFinanceTransfer = useCallback(
(body: {
instrument: 'SBLC' | 'DLC' | 'BG' | 'LS';
amount: string;
creditorIban: string;
reference?: string;
expiryDate?: string;
swiftMessageKind?: 'MT102' | 'MT103';
}) =>
run(`${body.instrument} trade finance`, async () => {
const r = await postJson<SettlementResult>(settlementApi(`/trade-finance/${body.instrument}`), {
idempotencyKey: idem(`Z-TF-${body.instrument}`),
officeId: 24,
currency: 'USD',
amount: body.amount,
creditorIban: body.creditorIban,
beneficiaryName: 'Z Online Bank',
orderingName: 'Z Online Bank — OMNL Office 24 Central Bank',
remittanceInfo: `Z Online Bank ${body.instrument} · ${body.swiftMessageKind ?? 'MT103'}`,
moneyLayers: ['M0', 'M1', 'M2', 'M3', 'M4'],
rail: 'SWIFT',
swiftMessageKind: body.swiftMessageKind ?? 'MT103',
tradeFinance: {
instrument: body.instrument,
reference: body.reference ?? idem('TF-REF'),
expiryDate: body.expiryDate,
},
});
return { ok: r.ok, status: r.status, data: r.data };
}),
[run],
);
const swiftMessageTransfer = useCallback(
(body: {
amount: string;
creditorIban: string;
swiftMessageKind: 'MT102' | 'MT103';
moneyLayers?: string[];
remittanceInfo?: string;
}) =>
run(`SWIFT ${body.swiftMessageKind}`, async () => {
const layers =
body.moneyLayers ??
(body.swiftMessageKind === 'MT102' ? ['M1', 'M2', 'M3', 'M4'] : ['M0', 'M1', 'M2', 'M4']);
const r = await postJson<SettlementResult>(settlementApi('/external-transfer'), {
idempotencyKey: idem(`Z-${body.swiftMessageKind}`),
officeId: 24,
currency: 'USD',
amount: body.amount,
creditorIban: body.creditorIban,
beneficiaryName: 'Z Online Bank',
orderingName: 'Z Online Bank — OMNL Office 24 Central Bank',
remittanceInfo: body.remittanceInfo ?? `Z Online Bank SWIFT ${body.swiftMessageKind}`,
moneyLayers: layers,
rail: 'SWIFT',
swiftMessageKind: body.swiftMessageKind,
});
return { ok: r.ok, status: r.status, data: r.data };
}),
[run],
);
const mt103LcTransfer = useCallback(
(amount: string, creditorIban: string, reference?: string) =>
tradeFinanceTransfer({
instrument: 'DLC',
amount,
creditorIban,
reference,
swiftMessageKind: 'MT103',
}),
[tradeFinanceTransfer],
);
const fullExternalTransfer = useCallback(
(body: {
amount: string;
creditorIban: string;
beneficiaryName?: string;
moneyLayers?: string[];
}) =>
run('Full external transfer', async () => {
const r = await postJson<SettlementResult>(settlementApi('/external-transfer'), {
idempotencyKey: idem('Z-EXT-FULL'),
officeId: 24,
currency: 'USD',
amount: body.amount,
creditorIban: body.creditorIban,
beneficiaryName: body.beneficiaryName ?? 'Beneficiary',
moneyLayers: body.moneyLayers ?? ['M0', 'M1', 'M2', 'M3', 'M4'],
rail: 'SWIFT',
});
return { ok: r.ok, status: r.status, data: r.data };
}),
[run],
);
const refreshSettlements = useCallback(async () => {
const list = await getJson<{ settlements?: SettlementResult[]; items?: SettlementResult[] }>(
`${settlementApi('/settlements')}?limit=20`,
);
const rows = list?.settlements ?? list?.items;
if (rows) setSettlements(rows);
}, []);
return {
loading,
error,
lastResult,
settlements,
transferNovaBankToZBank,
transferOffice24ToZBank,
transferOneXBankToZBank,
internalTransfer,
externalTransfer,
tokenLoad,
fiatToCrypto,
fiatToCryptoShiva,
swiftMessageTransfer,
tradeFinanceTransfer,
mt103LcTransfer,
zBankSettleM0ToM4,
fullExternalTransfer,
refreshSettlements,
clearError: () => setError(null),
};
}

View File

@@ -1,5 +1,6 @@
import { Link } from 'react-router-dom';
import NasaIcon, { type NasaIconName } from '../components/icons/NasaIcon';
import { GUOSMM_NOTICES } from '../config/guosmmNotices';
const BANKING: { path: string; label: string; icon: NasaIconName }[] = [
{ path: '/central-bank', label: 'Z Online Bank / Central Bank', icon: 'central-bank' },
@@ -71,6 +72,24 @@ export default function HubPage() {
<LinkGrid title="Chain 138 & operators" items={CHAIN138} />
</div>
<LinkGrid title="OMNL operator consoles" items={OMNL_OPS} />
<section className="dash-card cb-card p-4 mt-4">
<h2 className="dash-section-title mb-3">GUOSMM official gazette</h2>
<ul className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{GUOSMM_NOTICES.map((n) => (
<li key={n.documentId}>
<a
href={n.url}
target="_blank"
rel="noopener noreferrer"
className="dash-quick-links__chip inline-flex w-full !justify-start gap-2 py-2.5 px-3"
>
<NasaIcon name="shield" size={14} />
{n.documentId} {n.entity ?? 'OMNL EMCB'}
</a>
</li>
))}
</ul>
</section>
</div>
);
}

View File

@@ -0,0 +1,966 @@
/**
* Z Online Bank — Wise-inspired online banking UI
*/
.revolut-bank {
--rb-bg: #f2f2f2;
--rb-surface: #ffffff;
--rb-card: #ffffff;
--rb-text: #163300;
--rb-text-body: #454745;
--rb-muted: #6a6a6a;
--rb-subject: #454745;
--rb-accent: #9fe870;
--rb-accent-hover: #8fd960;
--rb-accent-text: #163300;
--rb-accent-deep: #0e2f0a;
--rb-accent-soft: #e8f8e0;
--rb-border: #e2e2e2;
--rb-border-strong: #d3d3d3;
--rb-shadow: 0 1px 3px rgba(22, 51, 0, 0.06);
--rb-shadow-md: 0 4px 12px rgba(22, 51, 0, 0.08);
--rb-radius: 16px;
--rb-radius-sm: 12px;
--rb-radius-pill: 9999px;
min-height: calc(100dvh - 3.5rem);
background: var(--rb-bg);
color: var(--rb-text-body);
font-family: Inter, 'Segoe UI', system-ui, -apple-system, sans-serif;
-webkit-font-smoothing: antialiased;
}
.omnl-banking-theme .omnl-app-header {
background: #ffffff !important;
border-bottom: 1px solid var(--rb-border) !important;
box-shadow: none !important;
}
.omnl-banking-theme .omnl-app-header a,
.omnl-banking-theme .omnl-app-header .font-semibold,
.omnl-banking-theme .omnl-app-header button {
color: var(--rb-text) !important;
}
.omnl-banking-theme .omnl-app-header nav a {
color: var(--rb-muted) !important;
}
.omnl-banking-theme .omnl-app-header nav a.bg-green-600 {
background: var(--rb-accent) !important;
color: var(--rb-accent-text) !important;
}
.omnl-banking-theme .omnl-app-header .bg-green-600 {
background: var(--rb-accent-deep) !important;
}
.omnl-banking-theme .omnl-app-header .text-white {
color: var(--rb-text) !important;
}
.omnl-banking-theme .omnl-app-header .hover\:bg-white\/10:hover {
background: var(--rb-bg) !important;
}
.revolut-bank__inner {
max-width: 42rem;
margin: 0 auto;
padding: 1.25rem 1rem 6rem;
}
@media (min-width: 768px) {
.revolut-bank__inner {
max-width: 52rem;
padding: 2rem 1.5rem 3rem;
}
}
/* Page header */
.revolut-bank__page-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1.75rem;
}
.revolut-bank__logo-row {
display: flex;
align-items: center;
gap: 0.75rem;
min-width: 0;
}
.revolut-bank__logo-mark {
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
background: var(--rb-accent);
color: var(--rb-accent-text);
display: flex;
align-items: center;
justify-content: center;
font-weight: 800;
font-size: 1rem;
flex-shrink: 0;
}
.revolut-bank__brand-block {
min-width: 0;
}
.revolut-bank__brand-title {
font-size: 1.125rem;
font-weight: 700;
color: var(--rb-text);
letter-spacing: -0.02em;
margin: 0;
line-height: 1.2;
}
.revolut-bank__greeting-label {
font-size: 0.8125rem;
color: var(--rb-muted);
margin: 0;
}
.revolut-bank__greeting-name {
font-size: 0.8125rem;
font-weight: 600;
color: var(--rb-text);
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.revolut-bank__status {
display: inline-flex;
align-items: center;
gap: 0.35rem;
font-size: 0.6875rem;
font-weight: 600;
padding: 0.35rem 0.65rem;
border-radius: var(--rb-radius-pill);
background: var(--rb-accent-soft);
color: var(--rb-accent-deep);
border: 1px solid rgba(159, 232, 112, 0.5);
flex-shrink: 0;
}
.revolut-bank__status--offline {
background: #f5f5f5;
color: var(--rb-muted);
border-color: var(--rb-border);
}
/* Balance hero */
.revolut-bank__balance-card {
background: transparent;
padding: 0 0 1.5rem;
margin-bottom: 0.5rem;
border: none;
box-shadow: none;
position: relative;
}
.revolut-bank__balance-card::before {
display: none;
}
.revolut-bank__balance-label {
font-size: 0.875rem;
color: var(--rb-muted);
margin-bottom: 0.5rem;
font-weight: 400;
}
.revolut-bank__balance-amount {
font-size: 2.75rem;
font-weight: 700;
letter-spacing: -0.04em;
line-height: 1.05;
color: var(--rb-text);
}
@media (min-width: 768px) {
.revolut-bank__balance-amount {
font-size: 3.25rem;
}
}
.revolut-bank__balance-sub {
margin-top: 0.65rem;
font-size: 0.8125rem;
color: var(--rb-muted);
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
border: none;
padding: 0;
}
.revolut-bank__balance-sub span:last-child {
color: var(--rb-accent-deep);
font-weight: 600;
}
/* Primary CTAs — Wise Send / Add money */
.revolut-bank__cta-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
margin-bottom: 2rem;
}
.revolut-bank__cta {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.875rem 1rem;
border-radius: var(--rb-radius-pill);
font-size: 0.9375rem;
font-weight: 600;
text-decoration: none;
cursor: pointer;
border: none;
transition: background 0.15s, transform 0.1s;
min-height: 48px;
}
.revolut-bank__cta--primary {
background: var(--rb-accent);
color: var(--rb-accent-text);
}
.revolut-bank__cta--primary:hover {
background: var(--rb-accent-hover);
}
.revolut-bank__cta--secondary {
background: var(--rb-surface);
color: var(--rb-text);
border: 1px solid var(--rb-border-strong);
}
.revolut-bank__cta--secondary:hover {
background: #fafafa;
}
/* Account list — Wise-style rows */
.revolut-bank__accounts-section {
margin-bottom: 2rem;
}
.revolut-bank__accounts-heading {
font-size: 1.125rem;
font-weight: 700;
color: var(--rb-text);
margin: 0 0 0.75rem;
letter-spacing: -0.02em;
}
.revolut-bank__accounts {
display: flex;
flex-direction: column;
gap: 0.5rem;
overflow: visible;
padding: 0;
margin: 0;
}
.revolut-bank__account-pill {
display: flex;
align-items: center;
gap: 0.875rem;
min-width: 0;
padding: 1rem 1.125rem;
border-radius: var(--rb-radius-sm);
background: var(--rb-surface);
border: 1px solid var(--rb-border);
box-shadow: var(--rb-shadow);
cursor: default;
}
.revolut-bank__account-pill--active {
border-color: var(--rb-accent);
background: var(--rb-surface);
box-shadow: var(--rb-shadow-md);
}
.revolut-bank__account-flag {
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
background: var(--rb-bg);
border: 1px solid var(--rb-border);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.6875rem;
font-weight: 800;
color: var(--rb-text);
flex-shrink: 0;
letter-spacing: -0.02em;
}
.revolut-bank__account-pill--active .revolut-bank__account-flag {
background: var(--rb-accent-soft);
border-color: rgba(159, 232, 112, 0.6);
}
.revolut-bank__account-pill-body {
flex: 1;
min-width: 0;
}
.revolut-bank__account-pill-label {
font-size: 0.9375rem;
font-weight: 600;
color: var(--rb-text);
margin-bottom: 0.1rem;
text-transform: none;
letter-spacing: 0;
}
.revolut-bank__account-pill-meta {
font-size: 0.75rem;
color: var(--rb-muted);
}
.revolut-bank__account-pill-value {
font-size: 1rem;
font-weight: 700;
color: var(--rb-text);
flex-shrink: 0;
text-align: right;
}
/* Quick links row */
.revolut-bank__actions {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.5rem;
margin-bottom: 2rem;
padding: 0;
background: transparent;
border: none;
box-shadow: none;
}
.revolut-bank__action {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 0.25rem;
text-decoration: none;
color: var(--rb-subject);
border: none;
background: none;
cursor: pointer;
font-size: 0.6875rem;
font-weight: 600;
}
.revolut-bank__action-icon {
width: 3rem;
height: 3rem;
border-radius: 50%;
background: var(--rb-surface);
border: 1px solid var(--rb-border);
display: flex;
align-items: center;
justify-content: center;
color: var(--rb-text);
transition: background 0.15s, border-color 0.15s;
box-shadow: var(--rb-shadow);
}
.revolut-bank__action:hover .revolut-bank__action-icon {
background: var(--rb-accent-soft);
border-color: rgba(159, 232, 112, 0.5);
transform: none;
}
/* Sections */
.revolut-bank__section-title {
font-size: 1.125rem;
font-weight: 700;
margin-bottom: 0.75rem;
display: flex;
align-items: center;
justify-content: space-between;
color: var(--rb-text);
letter-spacing: -0.02em;
}
.revolut-bank__section-title h2,
.revolut-bank__section-title h3 {
color: inherit;
font-size: inherit;
font-weight: inherit;
margin: 0;
}
.revolut-bank__section-link {
font-size: 0.875rem;
font-weight: 600;
color: var(--rb-accent-deep);
text-decoration: underline;
text-underline-offset: 2px;
}
/* Activity */
.revolut-bank__activity {
background: var(--rb-surface);
border-radius: var(--rb-radius-sm);
border: 1px solid var(--rb-border);
box-shadow: var(--rb-shadow);
overflow: hidden;
margin-bottom: 2rem;
}
.revolut-bank__error {
color: #9a6700;
background: #fffbeb;
border-color: #fcd34d;
}
.revolut-bank__activity-item {
display: flex;
align-items: center;
gap: 0.875rem;
padding: 1rem 1.125rem;
border-bottom: 1px solid var(--rb-border);
}
.revolut-bank__activity-item:last-child {
border-bottom: none;
}
.revolut-bank__activity-icon {
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
background: var(--rb-bg);
border: 1px solid var(--rb-border);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: var(--rb-text);
}
.revolut-bank__activity-icon--swap {
background: #eef6ff;
border-color: #cce0ff;
color: #2563eb;
}
.revolut-bank__activity-body {
flex: 1;
min-width: 0;
}
.revolut-bank__activity-title {
font-size: 0.9375rem;
font-weight: 600;
color: var(--rb-text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.revolut-bank__activity-meta {
font-size: 0.8125rem;
color: var(--rb-muted);
margin-top: 0.15rem;
}
.revolut-bank__activity-amount {
font-size: 0.9375rem;
font-weight: 700;
flex-shrink: 0;
color: var(--rb-text);
}
.revolut-bank__activity-amount--positive {
color: #0d7a4f;
}
/* Services grid */
.revolut-bank__services {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.65rem;
margin-bottom: 2rem;
}
@media (min-width: 640px) {
.revolut-bank__services {
grid-template-columns: repeat(3, 1fr);
}
}
.revolut-bank__service-card {
display: flex;
flex-direction: column;
gap: 0.25rem;
padding: 1rem 1.125rem;
border-radius: var(--rb-radius-sm);
background: var(--rb-surface);
border: 1px solid var(--rb-border);
box-shadow: var(--rb-shadow);
text-decoration: none;
color: var(--rb-text);
transition: border-color 0.15s, box-shadow 0.15s;
min-height: 5rem;
}
.revolut-bank__service-card:hover {
border-color: var(--rb-border-strong);
box-shadow: var(--rb-shadow-md);
}
.revolut-bank__service-label {
font-size: 0.875rem;
font-weight: 700;
color: var(--rb-text);
}
.revolut-bank__service-desc {
font-size: 0.75rem;
color: var(--rb-muted);
line-height: 1.4;
}
/* Treasury accordion */
.revolut-bank__treasury {
margin-top: 0.5rem;
}
.revolut-bank__treasury summary {
list-style: none;
cursor: pointer;
padding: 1rem 1.125rem;
border-radius: var(--rb-radius-sm);
background: var(--rb-surface);
border: 1px solid var(--rb-border);
font-size: 0.9375rem;
font-weight: 600;
color: var(--rb-text);
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: var(--rb-shadow);
}
.revolut-bank__treasury summary::-webkit-details-marker {
display: none;
}
.revolut-bank__chevron {
color: var(--rb-muted);
}
.revolut-bank__treasury-body {
margin-top: 0.75rem;
border-radius: var(--rb-radius-sm);
overflow: hidden;
}
.revolut-bank__skeleton {
background: linear-gradient(90deg, #ececec 25%, #e0e0e0 50%, #ececec 75%);
background-size: 200% 100%;
animation: rb-shimmer 1.2s infinite;
border-radius: 8px;
}
@keyframes rb-shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* Settlement panel */
.revolut-bank__protocols {
margin-bottom: 2rem;
padding: 1.25rem;
border-radius: var(--rb-radius-sm);
background: var(--rb-surface);
border: 1px solid var(--rb-border);
box-shadow: var(--rb-shadow);
}
.revolut-bank__protocols-hint {
font-size: 0.8125rem;
color: var(--rb-muted);
margin-bottom: 0.75rem;
line-height: 1.5;
}
.revolut-bank__protocol-tabs {
display: flex;
gap: 0.5rem;
overflow-x: auto;
margin-bottom: 1rem;
scrollbar-width: none;
padding-bottom: 0.25rem;
}
.revolut-bank__protocol-tabs::-webkit-scrollbar {
display: none;
}
.revolut-bank__protocol-tab {
flex: 0 0 auto;
padding: 0.5rem 0.875rem;
border-radius: var(--rb-radius-pill);
border: 1px solid var(--rb-border);
background: var(--rb-surface);
color: var(--rb-subject);
font-size: 0.75rem;
font-weight: 600;
cursor: pointer;
}
.revolut-bank__protocol-tab--active {
background: var(--rb-accent);
border-color: var(--rb-accent);
color: var(--rb-accent-text);
}
.revolut-bank__protocol-form {
display: grid;
gap: 0.875rem;
margin-bottom: 1rem;
}
.revolut-bank__field {
display: flex;
flex-direction: column;
gap: 0.35rem;
font-size: 0.8125rem;
color: var(--rb-text);
font-weight: 600;
}
.revolut-bank__input {
width: 100%;
padding: 0.75rem 0.875rem;
border-radius: 10px;
border: 1px solid var(--rb-border-strong);
background: #fff;
color: var(--rb-text);
font-size: 1rem;
font-weight: 400;
}
.revolut-bank__input:focus {
outline: none;
border-color: var(--rb-accent-deep);
box-shadow: 0 0 0 3px rgba(159, 232, 112, 0.35);
}
.revolut-bank__protocol-error {
padding: 0.875rem;
border-radius: 10px;
background: #fef2f2;
border: 1px solid #fecaca;
color: #b91c1c;
font-size: 0.8125rem;
margin-bottom: 0.875rem;
}
.revolut-bank__protocol-result {
padding: 0.875rem;
border-radius: 10px;
background: var(--rb-accent-soft);
border: 1px solid rgba(159, 232, 112, 0.45);
font-size: 0.8125rem;
color: var(--rb-subject);
margin-bottom: 0.875rem;
}
.revolut-bank__protocol-result strong {
color: var(--rb-accent-deep);
}
.revolut-bank__submit {
width: 100%;
padding: 0.9375rem 1rem;
border-radius: var(--rb-radius-pill);
border: none;
background: var(--rb-accent);
color: var(--rb-accent-text);
font-weight: 700;
font-size: 1rem;
cursor: pointer;
min-height: 48px;
transition: background 0.15s;
}
.revolut-bank__submit:hover:not(:disabled) {
background: var(--rb-accent-hover);
}
.revolut-bank__submit:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.revolut-bank__settlements-list {
margin-top: 1.25rem;
padding-top: 1rem;
border-top: 1px solid var(--rb-border);
}
.revolut-bank__settlements-list ul {
list-style: none;
margin: 0;
padding: 0;
}
.revolut-bank__settlements-list li {
display: flex;
justify-content: space-between;
gap: 0.5rem;
padding: 0.65rem 0;
border-bottom: 1px solid var(--rb-border);
font-size: 0.8125rem;
color: var(--rb-muted);
}
.revolut-bank__panel {
background: var(--rb-surface);
border: 1px solid var(--rb-border);
border-radius: var(--rb-radius-sm);
box-shadow: var(--rb-shadow);
padding: 1.25rem;
margin-bottom: 2rem;
}
/* M0→M4 full settlement pipeline */
.zbank-settle {
margin-bottom: 2rem;
padding: 1.25rem;
border-radius: var(--rb-radius-sm);
background: var(--rb-surface);
border: 1px solid var(--rb-border);
box-shadow: var(--rb-shadow);
}
.zbank-settle__pipeline {
display: flex;
flex-direction: column;
gap: 0;
margin: 1.25rem 0 1.5rem;
overflow-x: auto;
padding-bottom: 0.25rem;
}
@media (min-width: 640px) {
.zbank-settle__pipeline {
flex-direction: row;
align-items: stretch;
}
}
.zbank-settle__layer-wrap {
display: flex;
flex-direction: column;
flex: 1;
min-width: 7.5rem;
}
@media (min-width: 640px) {
.zbank-settle__layer-wrap {
flex-direction: row;
align-items: stretch;
}
}
.zbank-settle__connector {
width: 2px;
height: 1rem;
background: var(--rb-border-strong);
margin: 0 auto;
}
@media (min-width: 640px) {
.zbank-settle__connector {
width: 1rem;
height: 2px;
margin: auto 0;
flex-shrink: 0;
}
}
.zbank-settle__connector--done {
background: var(--rb-accent);
}
.zbank-settle__layer {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.2rem;
padding: 0.85rem 0.75rem;
border-radius: var(--rb-radius-sm);
border: 1px solid var(--rb-border);
background: var(--rb-bg);
min-height: 6.5rem;
}
.zbank-settle__layer--active {
border-color: var(--rb-accent);
box-shadow: 0 0 0 2px rgba(159, 232, 112, 0.35);
}
.zbank-settle__layer--done {
border-color: rgba(159, 232, 112, 0.6);
background: var(--rb-accent-soft);
}
.zbank-settle__layer-id {
font-size: 0.6875rem;
font-weight: 800;
letter-spacing: 0.06em;
color: var(--rb-accent-deep);
}
.zbank-settle__layer-name {
font-size: 0.8125rem;
font-weight: 700;
color: var(--rb-text);
}
.zbank-settle__layer-gl {
font-size: 0.6875rem;
color: var(--rb-muted);
font-family: ui-monospace, monospace;
}
.zbank-settle__layer-bal {
font-size: 0.75rem;
font-weight: 700;
color: var(--rb-text);
margin-top: 0.15rem;
}
.zbank-settle__layer-desc {
font-size: 0.625rem;
color: var(--rb-muted);
line-height: 1.35;
margin-top: auto;
}
.zbank-settle__checkbox {
flex-direction: row !important;
align-items: center;
gap: 0.5rem !important;
font-weight: 500 !important;
}
.zbank-settle__checkbox input {
width: 1rem;
height: 1rem;
accent-color: var(--rb-accent-deep);
}
.zbank-settle__steps {
margin-bottom: 1rem;
padding: 0.875rem;
border-radius: 10px;
background: var(--rb-bg);
border: 1px solid var(--rb-border);
}
.zbank-settle__steps-title {
font-size: 0.8125rem;
font-weight: 700;
color: var(--rb-text);
margin: 0 0 0.5rem;
}
.zbank-settle__steps ul {
list-style: none;
margin: 0;
padding: 0;
}
.zbank-settle__steps li {
display: grid;
grid-template-columns: 3.5rem 1fr auto;
gap: 0.5rem;
padding: 0.4rem 0;
border-bottom: 1px solid var(--rb-border);
font-size: 0.75rem;
color: var(--rb-muted);
}
.zbank-settle__steps li:last-child {
border-bottom: none;
}
.zbank-settle__step-status--posted {
color: #0d7a4f;
font-weight: 600;
}
.zbank-settle__step-status--failed {
color: #b91c1c;
font-weight: 600;
}
.zbank-settle__protocol-groups {
display: grid;
grid-template-columns: 1fr;
gap: 0.5rem;
margin-bottom: 1rem;
}
@media (min-width: 640px) {
.zbank-settle__protocol-groups {
grid-template-columns: repeat(2, 1fr);
}
}
.zbank-settle__protocol-group {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.2rem;
padding: 0.75rem 0.875rem;
border-radius: var(--rb-radius-sm);
border: 1px solid var(--rb-border);
background: var(--rb-surface);
cursor: pointer;
text-align: left;
}
.zbank-settle__protocol-group--active {
border-color: var(--rb-accent);
background: var(--rb-accent-soft);
}
.zbank-settle__protocol-group-label {
font-size: 0.8125rem;
font-weight: 700;
color: var(--rb-text);
}
.zbank-settle__protocol-group-desc {
font-size: 0.6875rem;
color: var(--rb-muted);
line-height: 1.35;
}
/* Legacy header elements — hidden when using Wise layout */
.revolut-bank__header-bar,
.revolut-bank__brand,
.revolut-bank__topbar,
.revolut-bank__avatar {
display: none;
}

View File

@@ -189,6 +189,7 @@ if [[ -n "${DATABASE_URL:-}" ]] && [[ "${SEED_SUPER_ADMIN_USERS:-1}" == "1" ]];
fi
log "OMNL Bank deploy complete"
bash "$REPO_DIR/scripts/deployment/sync-guosmm-notices.sh" || log "WARN: GUOSMM sync check failed"
log " Central Bank: http://127.0.0.1:$FRONTEND_PORT/central-bank"
log " Office 24: http://127.0.0.1:$FRONTEND_PORT/office-24"
log " DBIS Trade: http://127.0.0.1:$FRONTEND_PORT/trade"

View File

@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Verify GUOSMM gazette notices are live and propagate config to banking stack.
set -euo pipefail
REPO_DIR="${OMNL_BANK_ROOT:-$HOME/smom-dbis-138}"
REGISTRY="${REPO_DIR}/config/guosmm-notices.v1.json"
log() { echo "[$(date -Iseconds)] guosmm-sync $*"; }
[[ -f "$REGISTRY" ]] || { log "Missing $REGISTRY"; exit 1; }
log "Verifying gazette URLs..."
while IFS= read -r url; do
[[ -n "$url" ]] || continue
code="$(curl -sS -o /dev/null -w '%{http_code}' "$url")"
log " $url -> HTTP $code"
[[ "$code" == "200" ]] || exit 1
done < <(node -e "
const r=require('${REGISTRY//\\/\\\\}');
for (const n of r.notices) console.log(n.url);
")
log "API catalog probe (local token-aggregation)..."
curl -sf "http://127.0.0.1:3000/api/v1/omnl/gazette/notices" | head -c 200 || log "WARN: local API not up — redeploy first"
echo
if [[ "${GUOSMM_SYNC_LXCS:-0}" == "1" ]]; then
PROXMOX_ROOT="${PROXMOX_ROOT:-/srv/projects/proxmox}"
if [[ -f "${PROXMOX_ROOT}/scripts/deployment/sync-zardasht-omnl-banking-portal-lxcs.sh" ]]; then
log "LXC sync..."
bash "${PROXMOX_ROOT}/scripts/deployment/sync-zardasht-omnl-banking-portal-lxcs.sh" --apply
fi
fi
log "GUOSMM notices deployed to config + API registry"
log " Portal: /central-bank and /hub (gazette section)"
log " API: GET /api/v1/omnl/gazette/notices"

View File

@@ -141,12 +141,22 @@
function renderExternal(data) {
const ev = data.externalVisibility || {};
els.external.innerHTML = kv([
['DefiLlama chain', ev.defiLlama && ev.defiLlama.chainPage],
['Pricing PR', ev.defiLlama && ev.defiLlama.pr12094],
['Bridge TVL PR', ev.defiLlama && ev.defiLlama.pr19451],
['Safe deployments PR', ev.safeDeployments && ev.safeDeployments.pr1568],
]);
const guosmm = ev.guosmmGazette || {};
const noticeLinks = (guosmm.notices || [])
.map(function (n) {
return '<a href="' + esc(n.url) + '" target="_blank" rel="noopener">' + esc(n.documentId) + '</a>';
})
.join(' · ');
els.external.innerHTML =
kv([
['GUOSMM gazette', guosmm.base || '—'],
['API registry', guosmm.apiRegistry || '/api/v1/omnl/gazette/notices'],
['Notices', noticeLinks || '—'],
['DefiLlama chain', ev.defiLlama && ev.defiLlama.chainPage],
['Pricing PR', ev.defiLlama && ev.defiLlama.pr12094],
['Bridge TVL PR', ev.defiLlama && ev.defiLlama.pr19451],
['Safe deployments PR', ev.safeDeployments && ev.safeDeployments.pr1568],
]);
}
function renderHoLiquidity(snap) {

View File

@@ -15,6 +15,7 @@ import { omnlComplianceCore138, omnlReserveStore138 } from '../../services/omnl-
import { computeOmnlReconcileAnchor } from '../../services/omnl-reconcile-anchor';
import { getOmnlIntegrationStatus } from '../../services/omnl-integration-status';
import { getOmnlApiCatalog } from '../../services/omnl-api-catalog';
import { loadGuosmmNotices, getGuosmmNotice } from '../../services/omnl-gazette-notices';
import omnlOpenApi from '../../resources/omnl-openapi.json';
import {
executeErc20Transfer,
@@ -90,6 +91,28 @@ router.get('/omnl/integration-status', (_req: Request, res: Response) => {
});
});
/**
* GET /omnl/gazette/notices — GUOSMM official gazette registry (public disclosure).
*/
router.get('/omnl/gazette/notices', (_req: Request, res: Response) => {
const reg = loadGuosmmNotices();
res.json({
generatedAt: new Date().toISOString(),
gazetteBase: reg.gazetteBase,
count: reg.notices.length,
notices: reg.notices,
});
});
router.get('/omnl/gazette/notices/:documentId', (req: Request, res: Response) => {
const notice = getGuosmmNotice(req.params.documentId);
if (!notice) {
res.status(404).json({ error: 'Notice not found' });
return;
}
res.json(notice);
});
/**
* GET /omnl/reconcile-anchor — same SHA-256 as omnl-reconcile-report.mjs (IPSAS + matrix files).
*/

View File

@@ -19,6 +19,8 @@ export function getOmnlApiCatalog(): {
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/gazette/notices', description: 'GUOSMM official gazette notice registry', auth: 'none' },
{ method: 'GET', path: '/omnl/gazette/notices/:documentId', description: 'Single GUOSMM notice by document ID', 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: 'OMNL_API_KEY when OMNL_REQUIRE_API_KEY=1' },
{ method: 'GET', path: '/omnl/reconcile/triple-state', description: 'Fineract + on-chain + custodian reconcile', query: ['lineId'], auth: 'OMNL_API_KEY' },

View File

@@ -6,6 +6,7 @@ import { getWeb3ComplianceSummary } from './omnl-web3-compliance';
import { getComplianceSignoffsSummary } from './omnl-ifrs-disclosures';
import { omnlReserveStore138 } from './omnl-chain138-addresses';
import { buildSafeNotaryGateTransaction } from './omnl-safe-notary-gate-tx';
import { loadGuosmmNotices } from './omnl-gazette-notices';
function projectRoot(): string {
return (
@@ -83,6 +84,11 @@ export interface ComplianceConsoleSnapshot {
externalVisibility: {
defiLlama: { chainPage: string; pr12094: string; pr19451: string; note: string };
safeDeployments: { pr1568: string; note: string };
guosmmGazette: {
base: string;
apiRegistry: string;
notices: Array<{ documentId: string; title: string; url: string }>;
};
};
explorerBase: string;
}
@@ -280,6 +286,15 @@ export async function buildComplianceConsoleSnapshot(
pr1568: 'https://github.com/safe-global/safe-deployments/pull/1568',
note: 'Safe Wallet npm registry for chain 138',
},
guosmmGazette: {
base: loadGuosmmNotices().gazetteBase,
apiRegistry: '/api/v1/omnl/gazette/notices',
notices: loadGuosmmNotices().notices.map((n) => ({
documentId: n.documentId,
title: n.title,
url: n.url,
})),
},
},
explorerBase: process.env.EXPLORER_PUBLIC_URL?.trim() || 'https://explorer.d-bis.org',
};

View File

@@ -0,0 +1,49 @@
import { readFileSync, existsSync } from 'node:fs';
import { resolve } from 'node:path';
export type GuosmmNotice = {
documentId: string;
series: string;
title: string;
documentType: string;
url: string;
publishedAt: string;
effectiveAt: string;
signingAuthority: string;
contentSha256: string;
[key: string]: unknown;
};
export type GuosmmNoticeRegistry = {
version: string;
gazetteBase: string;
updated: string;
notices: GuosmmNotice[];
};
function repoRoot(): string {
return (
process.env.OMNL_BANK_ROOT?.trim() ||
process.env.PROXMOX_ROOT?.trim() ||
resolve(process.cwd(), '../../..')
);
}
export function loadGuosmmNotices(): GuosmmNoticeRegistry {
const path = resolve(repoRoot(), 'config/guosmm-notices.v1.json');
if (!existsSync(path)) {
return { version: '0', gazetteBase: 'https://gazette.shosjj.org/notices/2026', updated: '', notices: [] };
}
return JSON.parse(readFileSync(path, 'utf8')) as GuosmmNoticeRegistry;
}
export function getGuosmmNotice(documentId: string): GuosmmNotice | null {
const reg = loadGuosmmNotices();
return reg.notices.find((n) => n.documentId === documentId) ?? null;
}
export function listGuosmmNoticesByScope(scope?: string): GuosmmNotice[] {
const reg = loadGuosmmNotices();
if (!scope) return reg.notices;
return reg.notices.filter((n) => Array.isArray(n.scope) && (n.scope as string[]).includes(scope));
}