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
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:
128
config/ecosystem-links.v1.json
Normal file
128
config/ecosystem-links.v1.json
Normal 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"
|
||||
}
|
||||
}
|
||||
115
config/guosmm-notices.v1.json
Normal file
115
config/guosmm-notices.v1.json
Normal 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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
22
config/offices/office-29-zbank.v1.json
Normal file
22
config/offices/office-29-zbank.v1.json
Normal 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" }
|
||||
}
|
||||
}
|
||||
22
config/offices/office-30-novabank.v1.json
Normal file
22
config/offices/office-30-novabank.v1.json
Normal 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" }
|
||||
}
|
||||
}
|
||||
22
config/offices/office-31-anakabank.v1.json
Normal file
22
config/offices/office-31-anakabank.v1.json
Normal 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" }
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
255
frontend-dapp/public/omnl/static/omnl-compliance-console.js
Normal file
255
frontend-dapp/public/omnl/static/omnl-compliance-console.js
Normal 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, '&')
|
||||
.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();
|
||||
})();
|
||||
46
frontend-dapp/src/config/guosmmNotices.ts
Normal file
46
frontend-dapp/src/config/guosmmNotices.ts
Normal 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';
|
||||
@@ -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' },
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
242
frontend-dapp/src/features/online-bank/OnlineBankApp.tsx
Normal file
242
frontend-dapp/src/features/online-bank/OnlineBankApp.tsx
Normal 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';
|
||||
}
|
||||
@@ -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 M0–M4' },
|
||||
],
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -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 (M2→M3→M4 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 M0–M4`}
|
||||
</button>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
77
frontend-dapp/src/features/online-bank/useOnlineBank.ts
Normal file
77
frontend-dapp/src/features/online-bank/useOnlineBank.ts
Normal 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}`,
|
||||
};
|
||||
}
|
||||
455
frontend-dapp/src/features/online-bank/useZBankSettlement.ts
Normal file
455
frontend-dapp/src/features/online-bank/useZBankSettlement.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
966
frontend-dapp/src/styles/online-bank.css
Normal file
966
frontend-dapp/src/styles/online-bank.css
Normal 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;
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
36
scripts/deployment/sync-guosmm-notices.sh
Normal file
36
scripts/deployment/sync-guosmm-notices.sh
Normal 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"
|
||||
@@ -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) {
|
||||
|
||||
@@ -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).
|
||||
*/
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
Reference in New Issue
Block a user