From 14238dc81d69bde8946622e2a8e3182a3bc97cf0 Mon Sep 17 00:00:00 2001 From: zaragoza444 Date: Fri, 3 Jul 2026 02:03:38 -0700 Subject: [PATCH] feat(guosmm): deploy gazette notices across API, portal, and office configs 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 --- config/ecosystem-links.v1.json | 128 +++ config/guosmm-notices.v1.json | 115 +++ config/hybx-omnl-cross-chain-lines.json | 36 + config/offices/office-24-settlement.v1.json | 4 +- config/offices/office-29-zbank.v1.json | 22 + config/offices/office-30-novabank.v1.json | 22 + config/offices/office-31-anakabank.v1.json | 22 + .../settlement-middleware.production.v1.json | 5 + .../omnl/static/omnl-compliance-console.js | 255 +++++ frontend-dapp/src/config/guosmmNotices.ts | 46 + frontend-dapp/src/config/omnlInternalPaths.ts | 2 + .../online-bank/GuosmmNoticesPanel.tsx | 32 + .../features/online-bank/OnlineBankApp.tsx | 242 +++++ .../online-bank/SettlementProtocolsPanel.tsx | 366 +++++++ .../online-bank/ZBankFullSettlementPanel.tsx | 204 ++++ .../src/features/online-bank/useOnlineBank.ts | 77 ++ .../online-bank/useZBankSettlement.ts | 455 +++++++++ frontend-dapp/src/pages/HubPage.tsx | 19 + frontend-dapp/src/styles/online-bank.css | 966 ++++++++++++++++++ .../deployment/deploy-omnl-bank-production.sh | 1 + scripts/deployment/sync-guosmm-notices.sh | 36 + .../public/omnl-compliance-console.js | 22 +- .../token-aggregation/src/api/routes/omnl.ts | 23 + .../src/services/omnl-api-catalog.ts | 2 + .../src/services/omnl-compliance-console.ts | 15 + .../src/services/omnl-gazette-notices.ts | 49 + 26 files changed, 3159 insertions(+), 7 deletions(-) create mode 100644 config/ecosystem-links.v1.json create mode 100644 config/guosmm-notices.v1.json create mode 100644 config/offices/office-29-zbank.v1.json create mode 100644 config/offices/office-30-novabank.v1.json create mode 100644 config/offices/office-31-anakabank.v1.json create mode 100644 frontend-dapp/public/omnl/static/omnl-compliance-console.js create mode 100644 frontend-dapp/src/config/guosmmNotices.ts create mode 100644 frontend-dapp/src/features/online-bank/GuosmmNoticesPanel.tsx create mode 100644 frontend-dapp/src/features/online-bank/OnlineBankApp.tsx create mode 100644 frontend-dapp/src/features/online-bank/SettlementProtocolsPanel.tsx create mode 100644 frontend-dapp/src/features/online-bank/ZBankFullSettlementPanel.tsx create mode 100644 frontend-dapp/src/features/online-bank/useOnlineBank.ts create mode 100644 frontend-dapp/src/features/online-bank/useZBankSettlement.ts create mode 100644 frontend-dapp/src/styles/online-bank.css create mode 100644 scripts/deployment/sync-guosmm-notices.sh create mode 100644 services/token-aggregation/src/services/omnl-gazette-notices.ts diff --git a/config/ecosystem-links.v1.json b/config/ecosystem-links.v1.json new file mode 100644 index 0000000..63699bf --- /dev/null +++ b/config/ecosystem-links.v1.json @@ -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" + } +} diff --git a/config/guosmm-notices.v1.json b/config/guosmm-notices.v1.json new file mode 100644 index 0000000..0a77eab --- /dev/null +++ b/config/guosmm-notices.v1.json @@ -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"] + } + ] +} diff --git a/config/hybx-omnl-cross-chain-lines.json b/config/hybx-omnl-cross-chain-lines.json index 3971e0e..d1d1098 100644 --- a/config/hybx-omnl-cross-chain-lines.json +++ b/config/hybx-omnl-cross-chain-lines.json @@ -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" diff --git a/config/offices/office-24-settlement.v1.json b/config/offices/office-24-settlement.v1.json index 2ee898d..02ff5e2 100644 --- a/config/offices/office-24-settlement.v1.json +++ b/config/offices/office-24-settlement.v1.json @@ -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" } diff --git a/config/offices/office-29-zbank.v1.json b/config/offices/office-29-zbank.v1.json new file mode 100644 index 0000000..31fa8ec --- /dev/null +++ b/config/offices/office-29-zbank.v1.json @@ -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" } + } +} diff --git a/config/offices/office-30-novabank.v1.json b/config/offices/office-30-novabank.v1.json new file mode 100644 index 0000000..b57a130 --- /dev/null +++ b/config/offices/office-30-novabank.v1.json @@ -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" } + } +} diff --git a/config/offices/office-31-anakabank.v1.json b/config/offices/office-31-anakabank.v1.json new file mode 100644 index 0000000..ba054dc --- /dev/null +++ b/config/offices/office-31-anakabank.v1.json @@ -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" } + } +} diff --git a/config/settlement-middleware.production.v1.json b/config/settlement-middleware.production.v1.json index 9af6ed1..27d66c3 100644 --- a/config/settlement-middleware.production.v1.json +++ b/config/settlement-middleware.production.v1.json @@ -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" } } diff --git a/frontend-dapp/public/omnl/static/omnl-compliance-console.js b/frontend-dapp/public/omnl/static/omnl-compliance-console.js new file mode 100644 index 0000000..e4b878c --- /dev/null +++ b/frontend-dapp/public/omnl/static/omnl-compliance-console.js @@ -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 '' + okLabel + ''; + if (ok === false) return '' + badLabel + ''; + return 'Unknown'; + } + + function sevBadge(sev) { + const map = { critical: 'bad', high: 'bad', medium: 'warn', low: 'neutral' }; + return '' + sev + ''; + } + + 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', '
' + (p.complianceThreshold || '—') + '
', 'IPSAS / IFRS / US GAAP'), + card('Policy v', '
' + (p.jurisdictionPolicyVersion ?? '—') + '
', 'ID jurisdiction'), + ].join(''); + } + + function card(title, body, sub) { + return ( + '

' + + title + + '

' + + body + + (sub ? '' + sub + '' : '') + + '
' + ); + } + + function renderActions(actions) { + if (!actions || !actions.length) { + els.actions.innerHTML = '

No pending actions.

'; + return; + } + els.actions.innerHTML = + ''; + } + + function kv(rows) { + return ( + '
' + + rows + .map(function (r) { + return '
' + esc(r[0]) + '
' + esc(String(r[1] ?? '—')) + '
'; + }) + .join('') + + '
' + ); + } + + 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 '' + esc(n.documentId) + ''; + }) + .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)') + + ' line ' + + esc(t.lineId).slice(0, 18) + + '…'; + } + + function renderSignoffs(data) { + const s = data.signoffs || {}; + els.signoffs.innerHTML = '
' + esc(JSON.stringify(s, null, 2)) + '
'; + } + + function esc(s) { + return String(s) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + } + + 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(); +})(); diff --git a/frontend-dapp/src/config/guosmmNotices.ts b/frontend-dapp/src/config/guosmmNotices.ts new file mode 100644 index 0000000..cddf1e5 --- /dev/null +++ b/frontend-dapp/src/config/guosmmNotices.ts @@ -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'; diff --git a/frontend-dapp/src/config/omnlInternalPaths.ts b/frontend-dapp/src/config/omnlInternalPaths.ts index 2302c1b..617b663 100644 --- a/frontend-dapp/src/config/omnlInternalPaths.ts +++ b/frontend-dapp/src/config/omnlInternalPaths.ts @@ -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' }, diff --git a/frontend-dapp/src/features/online-bank/GuosmmNoticesPanel.tsx b/frontend-dapp/src/features/online-bank/GuosmmNoticesPanel.tsx new file mode 100644 index 0000000..b8b4a31 --- /dev/null +++ b/frontend-dapp/src/features/online-bank/GuosmmNoticesPanel.tsx @@ -0,0 +1,32 @@ +import { GUOSMM_NOTICES } from '../../config/guosmmNotices'; + +export default function GuosmmNoticesPanel() { + return ( +
+

+ Official gazette (GUOSMM) +

+

+ Sovereign Military Order of Malta · SHOSJJ official notices governing OMNL EMCB and ISB member banks. +

+ +
+ ); +} diff --git a/frontend-dapp/src/features/online-bank/OnlineBankApp.tsx b/frontend-dapp/src/features/online-bank/OnlineBankApp.tsx new file mode 100644 index 0000000..a46a969 --- /dev/null +++ b/frontend-dapp/src/features/online-bank/OnlineBankApp.tsx @@ -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 ( +
+
+
+
+ ); +} + +export default function OnlineBankApp() { + const bank = useOnlineBank(); + + return ( +
+
+
+
+
+ Z +
+
+

Z Online Bank

+

+ Hello · Good {timeOfDay()}, {bank.displayName} +

+
+
+ + + {bank.connected ? 'Live' : 'Offline'} + +
+ + {bank.loading && !bank.primaryBalance ? ( + + ) : ( +
+

Total balance

+

{formatUsd(bank.primaryBalance)}

+
+ {bank.officeLabel} + {bank.live && Synced with Fineract} +
+
+ )} + + + +
+

Your accounts

+
+ {ACCOUNTS.map((acct) => ( +
+ {acct.flag} +
+

{acct.label}

+

{acct.meta}

+
+

{formatUsd(bank[acct.balanceKey])}

+
+ ))} +
+
+ + + + bank.refresh()} + balances={{ + m0: bank.reserveBalance, + m1: bank.primaryBalance, + m2: bank.savingsBalance, + m3: bank.m3Balance, + m4: bank.m4Balance, + }} + /> + + bank.refresh()} /> + + {bank.error && ( +
+ {bank.error}. Showing cached layout — retrying every 30s. +
+ )} + +
+
+

Transactions

+ +
+
+ {bank.activities.length === 0 ? ( +
+ + + +
+

No transactions yet

+

Swaps and settlements appear here

+
+
+ ) : ( + bank.activities.map((item) => ( +
+ + + +
+

{item.title}

+

{item.meta}

+
+ + {item.amount} + +
+ )) + )} +
+
+ +
+

+ Explore +

+
+ + Digital Swap + Convert & load M2 tokens + + + DBIS Trade + Spot markets + + + Office 24 Central Bank + 128-bank corridors · IPSAS + + + OneX Wallet + Shiva blockchain · MPC wallet + + + OneX Bank + HYBX corridor · office 29 + + + Nova Bank Online + Import wires · M4 + + + Reserve + Coverage & attestation + +
+
+ + + +
+ + Treasury & operator console + + +
+ +
+
+
+
+ ); +} + +function timeOfDay(): string { + const h = new Date().getHours(); + if (h < 12) return 'morning'; + if (h < 17) return 'afternoon'; + return 'evening'; +} diff --git a/frontend-dapp/src/features/online-bank/SettlementProtocolsPanel.tsx b/frontend-dapp/src/features/online-bank/SettlementProtocolsPanel.tsx new file mode 100644 index 0000000..edc54b8 --- /dev/null +++ b/frontend-dapp/src/features/online-bank/SettlementProtocolsPanel.tsx @@ -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 = { + 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 = { + 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 M0–M4' }, + ], +}; + +export default function SettlementProtocolsPanel({ onSuccess }: { onSuccess?: () => void }) { + const { address, isConnected } = useAccount(); + const s = useZBankSettlement(); + const [group, setGroup] = useState('classic'); + const [active, setActive] = useState('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 ( +
+

Banking protocols

+ {!import.meta.env.VITE_OMNL_API_KEY && ( +

+ Settlement auth: set OMNL_API_KEY in repo .env and rebuild, or use online.omdnl.org portal. +

+ )} +

+ Classic · trade finance · digital · 128-bank corridors · OneX Bank · Shiva blockchain · HYBX · Chain 138 +

+ +
+ {GROUPS.map((g) => ( + + ))} +
+ +
+ {PROTOCOLS[group].map((t) => ( + + ))} +
+ +
+ + + {needsTfRef && ( + + )} + {!simpleWire && ( + <> + {needsWallet && ( + + )} + {(active === 'internal' || active === 'external' || active === 'token-load') && ( + <> + + + + )} + {(active === 'token-load' || active === 'fiat-crypto') && ( + + )} + + )} +
+ + {s.error && ( +
+ {s.error} +
+ )} + + {s.lastResult?.settlementId && ( +
+

+ Settlement: {s.lastResult.settlementId} +

+

+ Phase: {s.lastResult.phase ? PHASE_LABEL[s.lastResult.phase] : '—'} +

+ {s.lastResult.fineractJournalRef && ( +

+ Fineract: {s.lastResult.fineractJournalRef} +

+ )} +
+ )} + + + + {s.settlements.length > 0 && ( +
+

Recent settlements

+
    + {s.settlements.slice(0, 8).map((row) => ( +
  • + {row.settlementId?.slice(0, 12)}… + {row.phase ? PHASE_LABEL[row.phase] : '—'} +
  • + ))} +
+
+ )} +
+ ); +} diff --git a/frontend-dapp/src/features/online-bank/ZBankFullSettlementPanel.tsx b/frontend-dapp/src/features/online-bank/ZBankFullSettlementPanel.tsx new file mode 100644 index 0000000..deb27d1 --- /dev/null +++ b/frontend-dapp/src/features/online-bank/ZBankFullSettlementPanel.tsx @@ -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 = { + 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 ( +
+

Full settlement · M0 → M4

+

+ Posts sequential Fineract journals through every OMNL money layer, then dispatches HYBX + SWIFT MT103 to your Z + Online Bank IBAN. +

+ +
+ {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 ( +
+ {i > 0 &&
} +
+ {layer.id} + {layer.label} + GL {layer.gl} + {balances && ( + + {formatUsd( + layer.id === 'M0' + ? balances.m0 + : layer.id === 'M1' + ? balances.m1 + : layer.id === 'M2' + ? balances.m2 + : layer.id === 'M3' + ? balances.m3 + : balances.m4, + )} + + )} + {layer.desc} +
+
+ ); + })} +
+ +
+ + + + {includeM3 && ( + + )} +
+ + {layerSteps.length > 0 && ( +
+

Layer journals

+
    + {layerSteps.map((step) => ( +
  • + + {step.from}→{step.to} + + + Dr {step.debitGl} / Cr {step.creditGl} + + + {step.status} + {step.journalRef ? ` · JE ${step.journalRef}` : ''} + +
  • + ))} +
+
+ )} + + {s.error && ( +
+ {s.error} +
+ )} + + {s.lastResult?.settlementId && ( +
+

+ Settlement: {s.lastResult.settlementId} +

+

+ Phase: {s.lastResult.phase ? PHASE_LABEL[s.lastResult.phase] : '—'} +

+ {s.lastResult.fineractJournalRef && ( +

+ Last journal: {s.lastResult.fineractJournalRef} +

+ )} + {s.lastResult.hybxPaymentId && ( +

+ HYBX: {s.lastResult.hybxPaymentId} +

+ )} + {s.lastResult.chainTxHash && ( +

+ Chain mint: {s.lastResult.chainTxHash} +

+ )} +
+ )} + + +
+ ); +} diff --git a/frontend-dapp/src/features/online-bank/useOnlineBank.ts b/frontend-dapp/src/features/online-bank/useOnlineBank.ts new file mode 100644 index 0000000..8504240 --- /dev/null +++ b/frontend-dapp/src/features/online-bank/useOnlineBank.ts @@ -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(() => { + 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}`, + }; +} diff --git a/frontend-dapp/src/features/online-bank/useZBankSettlement.ts b/frontend-dapp/src/features/online-bank/useZBankSettlement.ts new file mode 100644 index 0000000..1aabeaa --- /dev/null +++ b/frontend-dapp/src/features/online-bank/useZBankSettlement.ts @@ -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(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(url: string): Promise { + 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(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(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(settlementApi('/external-transfer'), base); +} + +export function useZBankSettlement() { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [lastResult, setLastResult] = useState(null); + const [settlements, setSettlements] = useState([]); + + 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(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(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(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(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(settlementApi('/z-bank/settle-m0-m4'), payload); + if (r.status === 404 || r.status === 405) { + r = await postJson(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(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(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(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), + }; +} diff --git a/frontend-dapp/src/pages/HubPage.tsx b/frontend-dapp/src/pages/HubPage.tsx index d0d694e..4261a09 100644 --- a/frontend-dapp/src/pages/HubPage.tsx +++ b/frontend-dapp/src/pages/HubPage.tsx @@ -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() { +
+

GUOSMM official gazette

+ +
); } diff --git a/frontend-dapp/src/styles/online-bank.css b/frontend-dapp/src/styles/online-bank.css new file mode 100644 index 0000000..8aa3ce8 --- /dev/null +++ b/frontend-dapp/src/styles/online-bank.css @@ -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; +} diff --git a/scripts/deployment/deploy-omnl-bank-production.sh b/scripts/deployment/deploy-omnl-bank-production.sh index f1a8c56..a314918 100644 --- a/scripts/deployment/deploy-omnl-bank-production.sh +++ b/scripts/deployment/deploy-omnl-bank-production.sh @@ -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" diff --git a/scripts/deployment/sync-guosmm-notices.sh b/scripts/deployment/sync-guosmm-notices.sh new file mode 100644 index 0000000..e69eeb6 --- /dev/null +++ b/scripts/deployment/sync-guosmm-notices.sh @@ -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" diff --git a/services/token-aggregation/public/omnl-compliance-console.js b/services/token-aggregation/public/omnl-compliance-console.js index 92566fe..e4b878c 100644 --- a/services/token-aggregation/public/omnl-compliance-console.js +++ b/services/token-aggregation/public/omnl-compliance-console.js @@ -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 '' + esc(n.documentId) + ''; + }) + .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) { diff --git a/services/token-aggregation/src/api/routes/omnl.ts b/services/token-aggregation/src/api/routes/omnl.ts index 39b4faa..1ed47e1 100644 --- a/services/token-aggregation/src/api/routes/omnl.ts +++ b/services/token-aggregation/src/api/routes/omnl.ts @@ -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). */ diff --git a/services/token-aggregation/src/services/omnl-api-catalog.ts b/services/token-aggregation/src/services/omnl-api-catalog.ts index eb8c932..c86a7e8 100644 --- a/services/token-aggregation/src/services/omnl-api-catalog.ts +++ b/services/token-aggregation/src/services/omnl-api-catalog.ts @@ -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' }, diff --git a/services/token-aggregation/src/services/omnl-compliance-console.ts b/services/token-aggregation/src/services/omnl-compliance-console.ts index fd30396..0031b7c 100644 --- a/services/token-aggregation/src/services/omnl-compliance-console.ts +++ b/services/token-aggregation/src/services/omnl-compliance-console.ts @@ -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', }; diff --git a/services/token-aggregation/src/services/omnl-gazette-notices.ts b/services/token-aggregation/src/services/omnl-gazette-notices.ts new file mode 100644 index 0000000..08f0c9a --- /dev/null +++ b/services/token-aggregation/src/services/omnl-gazette-notices.ts @@ -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)); +}