diff --git a/frontend-dapp/index.html b/frontend-dapp/index.html index 5227525..7ac72df 100644 --- a/frontend-dapp/index.html +++ b/frontend-dapp/index.html @@ -7,7 +7,7 @@ - DBIS — Chain 138 + OMNL / DBIS — Secure Portal
-
DBIS
-
Loading…
+
OMNL / DBIS
+
Loading secure portal…
diff --git a/frontend-dapp/public/novabank/terminal/index.html b/frontend-dapp/public/novabank/terminal/index.html new file mode 100644 index 0000000..00263ed --- /dev/null +++ b/frontend-dapp/public/novabank/terminal/index.html @@ -0,0 +1,78 @@ + + + + + + + NovaBank Core Ledger System + + + +
+
+
+

NovaBank Core Ledger System

+

Fineract SoR · Alchemy on-chain · internal document API

+
+
+ + + + Compliance + Status JSON +
+
+ +
+ + + + + + + + +
+ +
+
+
Loading…
+
+ +
+ + +
+ + + + diff --git a/frontend-dapp/public/omnl/static/omnl-settlement-terminal.css b/frontend-dapp/public/omnl/static/omnl-settlement-terminal.css index 74a8da0..8fc0ca0 100644 --- a/frontend-dapp/public/omnl/static/omnl-settlement-terminal.css +++ b/frontend-dapp/public/omnl/static/omnl-settlement-terminal.css @@ -9,7 +9,9 @@ body { margin: 0; min-height: 100vh; - font-family: system-ui, sans-serif; + font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; } body.theme-black { @@ -23,7 +25,7 @@ body.theme-color { } .layout { - max-width: 1200px; + max-width: 980px; margin: 0 auto; padding: 1rem 1.25rem 2rem; } @@ -38,20 +40,17 @@ body.theme-color { } h1 { - font-size: 1.15rem; - margin: 0 0 0.25rem; + font-size: 1.1rem; + margin: 0 0 0.2rem; + letter-spacing: 0.02em; + font-weight: 650; } .sub { margin: 0; opacity: 0.85; font-size: 0.85rem; -} - -.toolbar { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; + letter-spacing: 0.01em; } button, @@ -61,10 +60,10 @@ button, border: 1px solid currentColor; background: transparent; color: inherit; - padding: 0.35rem 0.65rem; - border-radius: 4px; + padding: 0.22rem 0.5rem; + border-radius: 2px; text-decoration: none; - font-size: 0.85rem; + font-size: 0.8rem; } button.active { @@ -73,8 +72,8 @@ button.active { .card { border: 1px solid currentColor; - border-radius: 6px; - padding: 0.75rem; + border-radius: 3px; + padding: 0.8rem 0.85rem; margin-bottom: 1rem; } @@ -95,17 +94,32 @@ button.active { .params input { font: inherit; - padding: 0.35rem 0.5rem; + padding: 0.32rem 0.45rem; border: 1px solid currentColor; background: transparent; color: inherit; - border-radius: 4px; + border-radius: 2px; } .main { - display: grid; - grid-template-columns: 1fr min(22rem, 100%); - gap: 1rem; + display: block; +} + +body.doc-view .layout { + max-width: 860px; +} + +body.doc-view .params, +body.doc-view .side { + display: none; +} + +body.doc-view .screen-wrap { + margin-top: 0.25rem; +} + +body.doc-view .terminal { + height: 22.4rem; } @media (max-width: 900px) { @@ -116,26 +130,49 @@ button.active { .terminal { margin: 0; - min-height: 28rem; - max-height: 70vh; + width: min(100%, 80ch); + min-height: 22.4rem; + max-height: 22.4rem; + height: 22.4rem; overflow: auto; font-family: var(--font-mono); - font-size: 0.78rem; - line-height: 1.35; + font-size: 0.76rem; + line-height: 1.18; white-space: pre-wrap; word-break: break-word; + font-variant-ligatures: none; + letter-spacing: 0.01em; +} + +.terminal-shell { + display: grid; + gap: 0.35rem; + justify-items: center; + padding-top: 0.35rem; } body.theme-black .terminal { background: #000; color: #33ff66; - padding: 1rem; + padding: 0.72rem 0.85rem; } body.theme-color .terminal { - background: #ececec; + background: #dcdcdc; color: #111; - padding: 1rem; + padding: 0.72rem 0.85rem; +} + +body.theme-color .card, +body.theme-color button, +body.theme-color .btn, +body.theme-color .params input { + border-color: #444; +} + +body.theme-color .sub, +body.theme-color footer { + color: #222; } .side h2 { @@ -145,33 +182,56 @@ body.theme-color .terminal { letter-spacing: 0.05em; } -.cmd-grid { +.terminal-band, +.terminal-status { + width: min(100%, 80ch); + font-family: var(--font-mono); + letter-spacing: 0.02em; + text-transform: uppercase; + border: 1px solid currentColor; + padding: 0.3rem 0.5rem; +} + +.terminal-band { + font-size: 0.76rem; + background: transparent; +} + +.terminal-status { + font-size: 0.72rem; + opacity: 0.88; + white-space: pre; + line-height: 1.2; +} + +.command-line { + width: min(100%, 80ch); display: grid; - grid-template-columns: 1fr 1fr; - gap: 0.35rem; - margin-bottom: 0.75rem; + grid-template-columns: auto 1fr; + gap: 0.5rem; + align-items: center; + font-family: var(--font-mono); + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.04em; } -.alchemy-row { - display: flex; - flex-direction: column; - gap: 0.25rem; - font-size: 0.75rem; - margin-bottom: 0.75rem; +.command-line .prompt { + opacity: 0.9; } -.alchemy-row input { +.command-line input { font: inherit; - padding: 0.35rem; + padding: 0.35rem 0.5rem; border: 1px solid currentColor; background: transparent; color: inherit; - border-radius: 4px; + border-radius: 2px; } .json-out { margin: 0; - max-height: 16rem; + display: none; overflow: auto; font-family: var(--font-mono); font-size: 0.7rem; @@ -181,8 +241,11 @@ body.theme-color .terminal { } footer { - font-size: 0.75rem; - opacity: 0.8; + font-size: 0.7rem; + opacity: 0.82; + margin-top: 0.85rem; + padding-top: 0.8rem; + border-top: 1px solid currentColor; } footer code { diff --git a/frontend-dapp/public/omnl/static/omnl-settlement-terminal.js b/frontend-dapp/public/omnl/static/omnl-settlement-terminal.js index 09e8174..3b6349b 100644 --- a/frontend-dapp/public/omnl/static/omnl-settlement-terminal.js +++ b/frontend-dapp/public/omnl/static/omnl-settlement-terminal.js @@ -1,7 +1,10 @@ (function () { const API_BASE = window.resolveOmnlApiBase ? window.resolveOmnlApiBase() - : document.querySelector('meta[name="omnl-api-base"]')?.getAttribute('content') || '/api/v1'; + : (() => { + const meta = document.querySelector('meta[name="omnl-api-base"]')?.getAttribute('content')?.trim(); + return meta && meta !== 'auto' ? meta.replace(/\/$/, '') : '/api/v1'; + })(); const screenEl = document.getElementById('terminal-screen'); const jsonEl = document.getElementById('json-out'); const inpRef = document.getElementById('inp-settlement-ref'); @@ -9,13 +12,21 @@ const inpDate = document.getElementById('inp-value-date'); const inpAmount = document.getElementById('inp-amount'); const inpToken = document.getElementById('inp-token'); - const inpAlchemyAddr = document.getElementById('inp-alchemy-addr'); + const inpUser = document.getElementById('inp-user'); + const inpOfficer = document.getElementById('inp-officer'); + const inpCommand = document.getElementById('inp-command'); + const terminalBand = document.getElementById('terminal-band'); + const terminalStatus = document.getElementById('terminal-status'); - let screenMode = 'black'; + const tenant = (document.body.dataset.tenant || 'omnl').toLowerCase(); + const defaultTheme = (document.body.dataset.defaultTheme || (tenant === 'novabank' ? 'black' : 'color')).toLowerCase(); + const brandName = document.body.dataset.brandName || (tenant === 'novabank' ? 'NovaBank' : 'OMNL'); + let screenMode = defaultTheme === 'color' ? 'color' : 'black'; const inpConnection = document.getElementById('inp-connection'); - const DEFAULT_CONNECTION = 'mk-albert-gate-limited'; - const DEFAULT_WALLET = '0xb3B416BdE671c256aAbFB56358baD91D46BB9c39'; + const DEFAULT_CONNECTION = document.body.dataset.defaultConnectionId || 'mk-albert-gate-limited'; + let currentArtifact = 'package'; + let currentCommand = 'status'; (function initFromQuery() { const p = new URLSearchParams(window.location.search); @@ -23,12 +34,15 @@ if (inpConnection) inpConnection.value = conn; const ref = p.get('settlementRef'); if (ref) inpRef.value = ref; + if (inpUser) inpUser.value = p.get('user') || inpUser.value; + if (inpOfficer) inpOfficer.value = p.get('officer') || inpOfficer.value; })(); function qs() { const p = new URLSearchParams(window.location.search); const token = inpToken.value.trim() || p.get('access_token') || ''; const q = new URLSearchParams({ + tenant, settlementRef: inpRef.value.trim(), officeId: String(inpOffice.value || '1'), valueDate: inpDate.value, @@ -36,6 +50,8 @@ }); const conn = inpConnection && inpConnection.value ? inpConnection.value.trim() : ''; if (conn) q.set('connectionId', conn); + if (inpUser && inpUser.value.trim()) q.set('user', inpUser.value.trim()); + if (inpOfficer && inpOfficer.value.trim()) q.set('officer', inpOfficer.value.trim()); if (token) q.set('access_token', token); return q; } @@ -79,25 +95,197 @@ } function showJson(data) { - jsonEl.textContent = JSON.stringify(data, null, 2); + if (jsonEl) { + jsonEl.textContent = JSON.stringify(data, null, 2); + } } function showScreen(text) { screenEl.textContent = text; } + function line(label, value, width) { + return ` ${label.padEnd(width, '.')} ${value}`; + } + + function utcStamp() { + return new Date().toISOString().replace(/\.\d{3}Z$/, 'Z'); + } + + function formatHeader(title, subtitle, classification) { + return [ + title, + subtitle, + '', + line('Document Class', classification, 20), + line('Connection', currentConnectionId(), 20), + line('Generated UTC', utcStamp(), 20), + '', + ]; + } + + function formatFooter() { + return [ + '', + ' TERMINAL CONTROL', + line('REV', '1.2.0', 18), + line('FOR', brandName.toUpperCase(), 18), + line('CLASS', 'INTERNAL USE ONLY', 18), + line('DISP', 'READ ONLY', 18), + line('TERM', '3270 STYLE', 18), + ]; + } + + function formatHelpScreen() { + return [ + ...formatHeader( + `${brandName} — OPERATOR QUICK REFERENCE`, + 'Institutional appendix view', + 'operator reference' + ), + '', + ' COMMANDS', + line('status', 'public integration status', 20), + line('package', 'full JSON package', 20), + line('proof-of-funds', 'proof of funds letter', 20), + line('debit-note', 'debit note', 20), + line('remittance', 'remittance advice', 20), + line('token-san', 'tokenization sanitized copy', 20), + line('swift-copy', 'conversion SWIFT copy', 20), + line('screen', 'terminal screen (current theme)', 20), + '', + ' OPERATIONAL NOTES', + ' • Black / Green = terminal-default documentation style.', + ' • Gray / Black = print-friendly bank form style.', + ' • Status is public and non-sensitive.', + ' • Package / POF remain operator-authenticated.', + '', + ' Use the command line first.', + ...formatFooter(), + ].join('\n'); + } + + function formatStatusScreen(data) { + const connections = Array.isArray(data.connections) ? data.connections : []; + const activeConnection = currentConnectionLabel(); + const connectionCount = connections.length; + const isNovaBank = tenant === 'novabank'; + const baseRoute = isNovaBank ? '/novabank/terminal' : data.routes?.ui || data.ui || '/omnl/terminal'; + const statusRoute = data.routes?.status || `${baseRoute}/status`; + const packageRoute = data.routes?.package || `/api/v1${baseRoute}/package`; + const surfaceLabel = isNovaBank ? 'NovaBank terminal API' : 'OMNL terminal API'; + const serviceName = isNovaBank ? 'novabank-settlement-terminal' : data.service || 'omnl-settlement-terminal'; + const routeLines = [ + line('UI Route', baseRoute, 20), + line('Status Route', statusRoute, 20), + line('Package Route', packageRoute, 20), + line('API Surface', surfaceLabel, 20), + ]; + return [ + ...formatHeader( + `${brandName} — PUBLIC STATUS`, + 'Read-only terminal summary', + 'public status' + ), + '', + line('Service', serviceName, 20), + line('Version', data.version || 'unknown', 20), + line('Access', data.access || 'public', 20), + ...routeLines, + line('Fineract Env', data.fineractEnv ? 'configured' : 'not detected', 20), + line('Connections', String(connectionCount), 20), + line('Active Conn', activeConnection, 20), + '', + ' READ-ONLY SUMMARY', + ' No balances or secret material are shown on this view.', + ...formatFooter(), + ].join('\n'); + } + function setTheme(mode) { screenMode = mode === 'color' ? 'color' : 'black'; document.body.classList.toggle('theme-black', screenMode === 'black'); document.body.classList.toggle('theme-color', screenMode === 'color'); - document.getElementById('btn-mode-black').classList.toggle('active', screenMode === 'black'); - document.getElementById('btn-mode-color').classList.toggle('active', screenMode === 'color'); + const btnBlack = document.getElementById('btn-mode-black'); + const btnColor = document.getElementById('btn-mode-color'); + if (btnBlack) btnBlack.classList.toggle('active', screenMode === 'black'); + if (btnColor) btnColor.classList.toggle('active', screenMode === 'color'); + } + + function setDocumentView(enabled) { + document.body.classList.toggle('doc-view', Boolean(enabled)); + } + + function currentConnectionId() { + const selected = inpConnection && inpConnection.value ? inpConnection.value.trim() : ''; + return selected || DEFAULT_CONNECTION; + } + + function currentConnectionLabel() { + const option = inpConnection?.selectedOptions?.[0]; + return option?.textContent?.trim() || currentConnectionId(); + } + + function updateHostChrome(state = 'READY') { + if (terminalBand) { + const brand = brandName.toUpperCase(); + const connection = currentConnectionId(); + terminalBand.textContent = `${brand} | HOST SESSION | ${connection}`; + } + if (terminalStatus) { + const user = (inpUser && inpUser.value.trim()) || 'UNAUTHENTICATED'; + const termId = currentConnectionId(); + terminalStatus.textContent = [ + `${state} PF1=HELP PF3=EXIT MODE=${screenMode.toUpperCase()}`, + `SESSION ACTIVE USER=${user.toUpperCase()} TERM=${termId.toUpperCase()}`, + ].join('\n'); + } + } + + function normalizeCommand(raw) { + const value = String(raw || '').trim().toLowerCase(); + if (!value) return ''; + if (value === 'black' || value === 'green') return 'mode:black'; + if (value === 'color' || value === 'gray') return 'mode:color'; + if (value.startsWith('mode ')) { + const mode = value.slice(5).trim(); + if (mode === 'black' || mode === 'green') return 'mode:black'; + if (mode === 'color' || mode === 'gray') return 'mode:color'; + } + if (value === 'refresh') return 'refresh'; + const aliases = new Map([ + ['help', 'help'], + ['status', 'status'], + ['package', 'package'], + ['proof of funds', 'pof'], + ['proof-of-funds', 'pof'], + ['pof', 'pof'], + ['debit', 'debit'], + ['debit-note', 'debit'], + ['remittance', 'remittance'], + ['remittance-advice', 'remittance'], + ['token', 'token'], + ['token-san', 'token'], + ['swift', 'swift'], + ['swift-copy', 'swift'], + ['screen', 'screen'], + ]); + return aliases.get(value) || value; + } + + async function submitCommand(raw) { + const cmd = normalizeCommand(raw); + if (!cmd) return; + if (inpCommand) inpCommand.value = raw; + await runCmd(cmd); } async function loadScreen(artifact) { + currentArtifact = artifact || 'package'; + setDocumentView(currentArtifact === 'proof-of-funds-blackscreen'); const data = await apiGet('/omnl/terminal/screen', { mode: screenMode, - artifact: artifact || 'package', + artifact: currentArtifact, format: 'json', }); showScreen(data.text || ''); @@ -105,115 +293,116 @@ } async function runCmd(cmd) { + if (cmd === 'refresh') { + updateHostChrome('RUNNING'); + await runCmd(currentCommand); + return; + } + if (cmd === 'mode:black' || cmd === 'mode:color') { + setTheme(cmd === 'mode:black' ? 'black' : 'color'); + updateHostChrome('READY'); + await runCmd(currentCommand); + return; + } + + currentCommand = cmd; + if (inpCommand) inpCommand.value = cmd; + updateHostChrome('RUNNING'); try { if (cmd === 'help') { - showScreen( - [ - 'OMNL / HYBX SETTLEMENT TERMINAL — COMMANDS', - '', - ' status — integration status', - ' package — full JSON package', - ' pof — proof of funds', - ' debit-note — debit note', - ' remittance — remittance advice', - ' token-san — tokenization sanitized copy', - ' swift-copy — conversion SWIFT copy', - ' screen — terminal screen (current theme)', - ' alchemy balance — eth_getBalance via Alchemy', - '', - 'Toggle Black/Green vs Gray/Black with toolbar buttons.', - ].join('\n') - ); + showScreen(formatHelpScreen()); + updateHostChrome('READY'); return; } if (cmd === 'status') { - const q = qs(); - const token = q.get('access_token'); - const url = API_BASE + '/omnl/terminal/status' + (token ? '?access_token=' + encodeURIComponent(token) : ''); - const data = await fetch(url, { headers: authHeaders() }).then(function (r) { + setDocumentView(false); + const data = await fetch(API_BASE + '/omnl/terminal/status?connectionId=' + encodeURIComponent((inpConnection && inpConnection.value) || DEFAULT_CONNECTION), { + headers: authHeaders(), + }).then(function (r) { return r.json(); }); showJson(data); - showScreen(JSON.stringify(data, null, 2)); + showScreen(formatStatusScreen(data)); + updateHostChrome('READY'); return; } if (cmd === 'package') { + setDocumentView(false); const data = await apiGet('/omnl/terminal/package'); showJson(data); if (data.artifacts && data.artifacts.screens) { showScreen(data.artifacts.screens[screenMode] || ''); } + updateHostChrome('READY'); return; } if (cmd === 'pof') { const data = await apiGet('/omnl/terminal/proof-of-funds'); showJson(data); - await loadScreen('proof-of-funds'); + await loadScreen('proof-of-funds-blackscreen'); + updateHostChrome('READY'); return; } if (cmd === 'debit') { + setDocumentView(false); const data = await apiGet('/omnl/terminal/debit-note'); showJson(data); await loadScreen('debit-note'); + updateHostChrome('READY'); return; } if (cmd === 'remittance') { + setDocumentView(false); const data = await apiGet('/omnl/terminal/remittance-advice'); showJson(data); await loadScreen('remittance-advice'); + updateHostChrome('READY'); return; } if (cmd === 'token') { + setDocumentView(false); const data = await apiGet('/omnl/terminal/tokenization-sanitized'); showJson(data); await loadScreen('tokenization-sanitized'); + updateHostChrome('READY'); return; } if (cmd === 'swift') { + setDocumentView(false); const data = await apiGet('/omnl/terminal/conversion-swift-copy'); showJson(data); await loadScreen('conversion-swift-copy'); + updateHostChrome('READY'); return; } if (cmd === 'screen') { - await loadScreen('package'); - return; - } - if (cmd === 'alchemy-balance') { - const addr = (inpAlchemyAddr.value || '').trim(); - if (!addr) throw new Error('Set Alchemy address first'); - const data = await apiPost('/omnl/terminal/alchemy/rpc', { - network: 'eth-mainnet', - method: 'eth_getBalance', - params: [addr, 'latest'], - connectionId: (inpConnection && inpConnection.value) || DEFAULT_CONNECTION, - }); + setDocumentView(false); + const data = await apiGet('/omnl/terminal/screen', { artifact: 'screen' }); showJson(data); - showScreen('ALCHEMY eth_getBalance\nADDRESS: ' + addr + '\nRESULT: ' + data.result); + showScreen(data.text || ''); + updateHostChrome('READY'); return; } + showScreen(`ERROR: UNKNOWN COMMAND ${cmd}`); + updateHostChrome('ERROR'); } catch (e) { showScreen('ERROR: ' + (e && e.message ? e.message : String(e))); + updateHostChrome('ERROR'); } } - document.getElementById('btn-mode-black').addEventListener('click', function () { - setTheme('black'); - runCmd('screen'); - }); - document.getElementById('btn-mode-color').addEventListener('click', function () { - setTheme('color'); - runCmd('screen'); - }); - document.getElementById('btn-refresh').addEventListener('click', function () { - runCmd('screen'); - }); - document.querySelectorAll('[data-cmd]').forEach(function (btn) { - btn.addEventListener('click', function () { - runCmd(btn.getAttribute('data-cmd')); + if (inpCommand) { + inpCommand.addEventListener('keydown', function (ev) { + if (ev.key === 'Enter') { + ev.preventDefault(); + submitCommand(inpCommand.value); + } }); - }); + } - setTheme('black'); - runCmd('screen'); + setTheme(defaultTheme); + setDocumentView(false); + updateHostChrome('READY'); + runCmd('status'); + if (inpCommand) inpCommand.focus(); })(); diff --git a/frontend-dapp/public/omnl/terminal/index.html b/frontend-dapp/public/omnl/terminal/index.html index 17803b4..78c8017 100644 --- a/frontend-dapp/public/omnl/terminal/index.html +++ b/frontend-dapp/public/omnl/terminal/index.html @@ -4,22 +4,15 @@ - OMNL / HYBX Settlement Terminal + OMNL Core Ledger System - +
-

OMNL / HYBX Settlement Terminal

-

Fineract SoR · Alchemy on-chain · institutional document API

-
-
- - - - Compliance - Status JSON +

OMNL Core Ledger System

+

Institutional appendix view · Fineract SoR · OMNL terminal API

@@ -35,38 +28,27 @@ + +
-
+
+
OMNL CORE LEDGER SYSTEM | HOST SESSION | READY
Loading…
-
- +
READY
+ +
diff --git a/frontend-dapp/src/App.tsx b/frontend-dapp/src/App.tsx index 41fbd8d..7ef74c5 100644 --- a/frontend-dapp/src/App.tsx +++ b/frontend-dapp/src/App.tsx @@ -1,5 +1,5 @@ -import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' -import { useEffect } from 'react' +import { BrowserRouter, Routes, Route, Navigate, useLocation } from 'react-router-dom' +import { Suspense, lazy, useEffect } from 'react' import { WagmiProvider } from 'wagmi' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { ThirdwebProvider } from '@thirdweb-dev/react' @@ -16,15 +16,17 @@ import HistoryPage from './pages/HistoryPage' import AdminPanel from './pages/AdminPanel' import DocsPage from './pages/DocsPage' import HubPage from './pages/HubPage' -import WalletsDemoPage from './pages/WalletsDemoPage' +import PublicationsPage from './pages/PublicationsPage' import ZEcosystemHubPage from './pages/ZEcosystemHubPage' -import ZWalletPage from './pages/ZWalletPage' import Layout from './components/layout/Layout' import OmnlProductLayout from './components/layout/OmnlProductLayout' import ZProductLayout from './components/layout/ZProductLayout' import ToastProvider from './components/ui/ToastProvider' import { isZEcosystem, isOmnlEcosystem } from './config/ecosystemMode' +const WalletsDemoPage = lazy(() => import('./pages/WalletsDemoPage')) +const ZWalletPage = lazy(() => import('./pages/ZWalletPage')) + const queryClient = new QueryClient({ defaultOptions: { queries: { @@ -47,6 +49,35 @@ function ExternalRedirect({ to }: { to: string }) { return null } +function RouteLoading() { + return
Loading secure portal…
+} + +function RouteDocumentTitle() { + const { pathname } = useLocation() + + useEffect(() => { + const titleMap: Record = { + '/central-bank': 'OMNL / DBIS — Central Bank', + '/office-24': 'OMNL / DBIS — Office 24', + '/hub': 'OMNL / DBIS — Hub', + '/bridge': 'OMNL / DBIS — Bridge', + '/trade': 'OMNL / DBIS — Trade', + '/swap': 'OMNL / DBIS — Swap', + '/publications': 'OMNL / DBIS — Public Content', + '/admin': 'OMNL / DBIS — Admin', + '/docs': 'OMNL / DBIS — API Docs', + '/reserve': 'OMNL / DBIS — Reserve', + '/history': 'OMNL / DBIS — History', + '/wallets': 'OMNL / DBIS — Wallets', + } + + document.title = titleMap[pathname] ?? 'OMNL / DBIS — Secure Portal' + }, [pathname]) + + return null +} + function ZRoutes() { return ( @@ -58,7 +89,14 @@ function ZRoutes() { } > } /> - } /> + }> + + + } + /> } /> } /> @@ -75,6 +113,7 @@ function OmnlRoutes() { } /> } /> } /> + } /> } /> } /> @@ -90,7 +129,14 @@ function OmnlRoutes() { } /> } /> } /> - } /> + }> + + + } + /> } /> } /> @@ -110,6 +156,7 @@ function App() { v7_relativeSplatPath: true, }} > + {isZEcosystem ? : isOmnlEcosystem ? : } diff --git a/frontend-dapp/src/components/admin/AuditLogViewer.tsx b/frontend-dapp/src/components/admin/AuditLogViewer.tsx index eda9694..ad21c6d 100644 --- a/frontend-dapp/src/components/admin/AuditLogViewer.tsx +++ b/frontend-dapp/src/components/admin/AuditLogViewer.tsx @@ -6,6 +6,12 @@ import { useState, useMemo, useEffect } from 'react' import { useAdmin } from '../../contexts/AdminContext' import Pagination from './Pagination' +function toText(value: unknown): string { + if (typeof value === 'string') return value + if (typeof value === 'number' || typeof value === 'bigint' || typeof value === 'boolean') return String(value) + return '' +} + export default function AuditLogViewer() { const { auditLogs, exportAuditLogs } = useAdmin() const [filter, setFilter] = useState<'all' | 'success' | 'failure'>('all') @@ -19,10 +25,10 @@ export default function AuditLogViewer() { if (searchTerm) { const searchLower = searchTerm.toLowerCase() return ( - log.action.toLowerCase().includes(searchLower) || - log.user.toLowerCase().includes(searchLower) || - log.resourceType.toLowerCase().includes(searchLower) || - log.resourceId.toLowerCase().includes(searchLower) + toText(log.action).toLowerCase().includes(searchLower) || + toText(log.user).toLowerCase().includes(searchLower) || + toText(log.resourceType).toLowerCase().includes(searchLower) || + toText(log.resourceId).toLowerCase().includes(searchLower) ) } return true @@ -120,12 +126,12 @@ export default function AuditLogViewer() {

- User: {log.user.slice(0, 10)}... |{' '} - {log.resourceType} | {log.resourceId.slice(0, 10)}... + User: {toText(log.user).slice(0, 10)}... |{' '} + {toText(log.resourceType)} | {toText(log.resourceId).slice(0, 10)}...

- {log.details && ( + {Boolean(log.details) && (

- {JSON.stringify(log.details).slice(0, 100)}... + {String(JSON.stringify(log.details)).slice(0, 100)}...

)} diff --git a/frontend-dapp/src/components/admin/BatchOperations.tsx b/frontend-dapp/src/components/admin/BatchOperations.tsx index 3a293af..ee332b1 100644 --- a/frontend-dapp/src/components/admin/BatchOperations.tsx +++ b/frontend-dapp/src/components/admin/BatchOperations.tsx @@ -12,21 +12,24 @@ import { useAdmin } from '../../contexts/AdminContext' import { TransactionRequestStatus } from '../../types/admin' import { generateSecureId } from '../../utils/security' import toast from 'react-hot-toast' +import type { Abi, AbiFunction } from 'viem' interface BatchAction { id: string contractAddress: string - functionName: string - args: any[] + functionName: 'pause' | 'unpause' | 'setAdmin' | 'anchorStateProof' | 'mirrorTransaction' | 'mirrorBatchTransactions' + args: unknown[] enabled: boolean } +type BatchFunctionName = BatchAction['functionName'] + export default function BatchOperations() { const { createAdminAction, addAuditLog } = useAdmin() const [actions, setActions] = useState([]) const { writeContract, isPending } = useWriteContract() - const addAction = (contractAddress: string, functionName: string, args: any[]) => { + const addAction = (contractAddress: string, functionName: BatchFunctionName, args: unknown[]) => { const newAction: BatchAction = { id: `action_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`, contractAddress, @@ -58,7 +61,7 @@ export default function BatchOperations() { for (const action of enabledActions) { try { - const abi = action.contractAddress === CONTRACT_ADDRESSES.mainnet.MAINNET_TETHER + const abi: Abi = action.contractAddress === CONTRACT_ADDRESSES.mainnet.MAINNET_TETHER ? MAINNET_TETHER_ABI : TRANSACTION_MIRROR_ABI @@ -66,14 +69,14 @@ export default function BatchOperations() { { address: action.contractAddress as `0x${string}`, abi, - functionName: action.functionName as any, - args: action.args as any, + functionName: action.functionName as AbiFunction['name'], + args: action.args, chainId: mainnet.id, }, { onSuccess: (hash) => { createAdminAction({ - type: action.functionName as any, + type: action.functionName, contractAddress: action.contractAddress, functionName: action.functionName, args: action.args, @@ -84,26 +87,28 @@ export default function BatchOperations() { }) toast.success(`Action ${action.functionName} executed`) }, - onError: (error: any) => { + onError: (error: unknown) => { + const message = error instanceof Error ? error.message : 'Transaction failed' createAdminAction({ - type: action.functionName as any, + type: action.functionName, contractAddress: action.contractAddress, functionName: action.functionName, args: action.args, status: TransactionRequestStatus.FAILED, - error: error.message, + error: message, createdAt: Date.now(), id: generateSecureId(), }) - toast.error(`Action ${action.functionName} failed: ${error.message}`) + toast.error(`Action ${action.functionName} failed: ${message}`) }, } ) // Small delay between actions await new Promise((resolve) => setTimeout(resolve, 1000)) - } catch (error: any) { - toast.error(`Error executing ${action.functionName}: ${error.message}`) + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Unexpected error' + toast.error(`Error executing ${action.functionName}: ${message}`) } } diff --git a/frontend-dapp/src/components/admin/EmergencyControls.tsx b/frontend-dapp/src/components/admin/EmergencyControls.tsx index af41fad..4d59b48 100644 --- a/frontend-dapp/src/components/admin/EmergencyControls.tsx +++ b/frontend-dapp/src/components/admin/EmergencyControls.tsx @@ -47,8 +47,8 @@ export default function EmergencyControls() { }) setConfirmText('') }, - onError: (error: any) => { - toast.error(`Error: ${error.message}`) + onError: (error: unknown) => { + toast.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`) }, } ) @@ -75,8 +75,8 @@ export default function EmergencyControls() {
-
diff --git a/frontend-dapp/src/components/admin/ImpersonationMode.tsx b/frontend-dapp/src/components/admin/ImpersonationMode.tsx index 99a7120..0b148cc 100644 --- a/frontend-dapp/src/components/admin/ImpersonationMode.tsx +++ b/frontend-dapp/src/components/admin/ImpersonationMode.tsx @@ -121,9 +121,9 @@ export default function ImpersonationMode() {
-

- 💡 This allows you to test admin functions from any address without needing that address's private key. -

+

+ 💡 This allows you to test admin functions from any address without needing that address's private key. +

)} diff --git a/frontend-dapp/src/components/admin/MainnetTetherAdmin.tsx b/frontend-dapp/src/components/admin/MainnetTetherAdmin.tsx index 7628351..1a6af52 100644 --- a/frontend-dapp/src/components/admin/MainnetTetherAdmin.tsx +++ b/frontend-dapp/src/components/admin/MainnetTetherAdmin.tsx @@ -5,6 +5,9 @@ import { CONTRACT_ADDRESSES } from '../../config/contracts' import { MAINNET_TETHER_ABI } from '../../abis/MainnetTether' import toast from 'react-hot-toast' +const getErrorMessage = (error: unknown): string => + error instanceof Error ? error.message : 'Transaction failed' + export default function MainnetTetherAdmin() { const [blockNumber, setBlockNumber] = useState('') const [newAdmin, setNewAdmin] = useState('') @@ -52,8 +55,8 @@ export default function MainnetTetherAdmin() { toast.success('Pause transaction submitted') refetchPaused() }, - onError: (error: any) => { - toast.error(`Error: ${error.message || 'Transaction failed'}`) + onError: (error: unknown) => { + toast.error(`Error: ${getErrorMessage(error)}`) }, } ) @@ -72,8 +75,8 @@ export default function MainnetTetherAdmin() { toast.success('Unpause transaction submitted') refetchPaused() }, - onError: (error: any) => { - toast.error(`Error: ${error.message || 'Transaction failed'}`) + onError: (error: unknown) => { + toast.error(`Error: ${getErrorMessage(error)}`) }, } ) @@ -99,8 +102,8 @@ export default function MainnetTetherAdmin() { setNewAdmin('') refetchAdmin() }, - onError: (error: any) => { - toast.error(`Error: ${error.message || 'Transaction failed'}`) + onError: (error: unknown) => { + toast.error(`Error: ${getErrorMessage(error)}`) }, } ) diff --git a/frontend-dapp/src/components/admin/MultiSigAdmin.tsx b/frontend-dapp/src/components/admin/MultiSigAdmin.tsx index c6d3afb..eed2606 100644 --- a/frontend-dapp/src/components/admin/MultiSigAdmin.tsx +++ b/frontend-dapp/src/components/admin/MultiSigAdmin.tsx @@ -14,7 +14,7 @@ interface MultiSigProposal { id: string contractAddress: string functionName: string - args: any[] + args: unknown[] description: string approvals: string[] requiredApprovals: number @@ -42,7 +42,7 @@ export default function MultiSigAdmin() { localStorage.setItem('multisig_proposals', JSON.stringify(proposals)) }, [proposals]) - const createProposal = (functionName: string, args: any[], description: string) => { + const createProposal = (functionName: string, args: unknown[], description: string) => { if (!address) { toast.error('Please connect your wallet') return @@ -65,7 +65,7 @@ export default function MultiSigAdmin() { // Create admin action createAdminAction({ - type: functionName as any, + type: functionName, contractAddress: proposal.contractAddress, functionName, args, @@ -160,8 +160,9 @@ export default function MultiSigAdmin() { }) toast.success('Proposal executed successfully') - } catch (error: any) { - toast.error(`Execution failed: ${error.message}`) + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Execution failed' + toast.error(`Execution failed: ${message}`) console.error('Proposal execution error:', error) } } diff --git a/frontend-dapp/src/components/admin/RealtimeMonitor.tsx b/frontend-dapp/src/components/admin/RealtimeMonitor.tsx index c581159..956a169 100644 --- a/frontend-dapp/src/components/admin/RealtimeMonitor.tsx +++ b/frontend-dapp/src/components/admin/RealtimeMonitor.tsx @@ -15,6 +15,8 @@ interface ContractState { lastUpdate: number } +type PartialContractState = Partial> + export default function RealtimeMonitor() { const publicClient = usePublicClient() const chainId = useChainId() @@ -40,8 +42,10 @@ export default function RealtimeMonitor() { ...prev, [CONTRACT_ADDRESSES.mainnet.MAINNET_TETHER]: { contractAddress: CONTRACT_ADDRESSES.mainnet.MAINNET_TETHER, - ...state, + ...(state as PartialContractState), lastUpdate: Date.now(), + paused: Boolean((state as PartialContractState).paused), + admin: String((state as PartialContractState).admin ?? ''), }, })) } @@ -61,8 +65,10 @@ export default function RealtimeMonitor() { ...prev, [CONTRACT_ADDRESSES.mainnet.TRANSACTION_MIRROR]: { contractAddress: CONTRACT_ADDRESSES.mainnet.TRANSACTION_MIRROR, - ...state, + ...(state as PartialContractState), lastUpdate: Date.now(), + paused: Boolean((state as PartialContractState).paused), + admin: String((state as PartialContractState).admin ?? ''), }, })) } @@ -78,7 +84,11 @@ export default function RealtimeMonitor() { monitor.subscribeToBlocks(chainId, (event) => { setEvents((prev) => [event, ...prev.slice(0, 49)]) // Keep last 50 events if (event.type === 'block') { - toast(`New block: ${event.data.number}`, { icon: '🔔', duration: 2000 }) + const blockNumber = + typeof event.data === 'object' && event.data !== null && 'number' in event.data + ? String((event.data as { number?: unknown }).number ?? '') + : 'unknown' + toast(`New block: ${blockNumber}`, { icon: '🔔', duration: 2000 }) } }).then((unsub) => { unsubscribeBlocks = unsub diff --git a/frontend-dapp/src/components/admin/RoleBasedAccess.tsx b/frontend-dapp/src/components/admin/RoleBasedAccess.tsx index a7e78c5..34276d5 100644 --- a/frontend-dapp/src/components/admin/RoleBasedAccess.tsx +++ b/frontend-dapp/src/components/admin/RoleBasedAccess.tsx @@ -7,7 +7,7 @@ import { useAccount } from 'wagmi' import { useAdmin } from '../../contexts/AdminContext' import toast from 'react-hot-toast' -export enum AdminRole { +enum AdminRole { SUPER_ADMIN = 'SUPER_ADMIN', OPERATOR = 'OPERATOR', VIEWER = 'VIEWER', diff --git a/frontend-dapp/src/components/admin/ScheduledActions.tsx b/frontend-dapp/src/components/admin/ScheduledActions.tsx index ef89f77..12eb850 100644 --- a/frontend-dapp/src/components/admin/ScheduledActions.tsx +++ b/frontend-dapp/src/components/admin/ScheduledActions.tsx @@ -14,7 +14,7 @@ interface ScheduledAction { name: string contractAddress: `0x${string}` | string functionName: string - args: any[] + args: unknown[] schedule: string // Cron-like expression (simplified: "daily", "weekly", "custom") nextExecution: number enabled: boolean @@ -48,10 +48,10 @@ export default function ScheduledActions() { if (action.enabled && action.nextExecution <= now) { // Execute action createAdminAction({ - type: action.functionName as any, + type: action.functionName, contractAddress: action.contractAddress as `0x${string}`, functionName: action.functionName, - args: (action.args && typeof action.args === 'string' ? JSON.parse(action.args) : action.args) || [], + args: Array.isArray(action.args) ? action.args : [], status: TransactionRequestStatus.PENDING, createdAt: Date.now(), id: generateSecureId(), @@ -109,7 +109,7 @@ export default function ScheduledActions() { name: newAction.name, contractAddress: newAction.contractAddress as `0x${string}`, functionName: newAction.functionName, - args: JSON.parse(newAction.args || '[]'), + args: JSON.parse(newAction.args || '[]') as unknown[], schedule: newAction.schedule, nextExecution: calculateNextExecution(newAction.schedule, Date.now()), enabled: true, diff --git a/frontend-dapp/src/components/admin/TimeLockedActions.tsx b/frontend-dapp/src/components/admin/TimeLockedActions.tsx index 710027c..ae1c4aa 100644 --- a/frontend-dapp/src/components/admin/TimeLockedActions.tsx +++ b/frontend-dapp/src/components/admin/TimeLockedActions.tsx @@ -39,7 +39,7 @@ export default function TimeLockedActions() { if (action.status === 'pending' && action.scheduledTime <= now) { // Execute action createAdminAction({ - type: action.action as any, + type: action.action, contractAddress: action.contractAddress, functionName: action.action, args: [], diff --git a/frontend-dapp/src/components/admin/TransactionMirrorAdmin.tsx b/frontend-dapp/src/components/admin/TransactionMirrorAdmin.tsx index d70ddf7..27a0140 100644 --- a/frontend-dapp/src/components/admin/TransactionMirrorAdmin.tsx +++ b/frontend-dapp/src/components/admin/TransactionMirrorAdmin.tsx @@ -5,6 +5,9 @@ import { CONTRACT_ADDRESSES } from '../../config/contracts' import { TRANSACTION_MIRROR_ABI } from '../../abis/TransactionMirror' import toast from 'react-hot-toast' +const getErrorMessage = (error: unknown): string => + error instanceof Error ? error.message : 'Transaction failed' + export default function TransactionMirrorAdmin() { const publicClient = usePublicClient({ chainId: mainnet.id }) const [newAdmin, setNewAdmin] = useState('') @@ -60,8 +63,8 @@ export default function TransactionMirrorAdmin() { toast.success('Pause transaction submitted') refetchPaused() }, - onError: (error: any) => { - toast.error(`Error: ${error.message || 'Transaction failed'}`) + onError: (error: unknown) => { + toast.error(`Error: ${getErrorMessage(error)}`) }, } ) @@ -80,8 +83,8 @@ export default function TransactionMirrorAdmin() { toast.success('Unpause transaction submitted') refetchPaused() }, - onError: (error: any) => { - toast.error(`Error: ${error.message || 'Transaction failed'}`) + onError: (error: unknown) => { + toast.error(`Error: ${getErrorMessage(error)}`) }, } ) @@ -107,8 +110,8 @@ export default function TransactionMirrorAdmin() { setNewAdmin('') refetchAdmin() }, - onError: (error: any) => { - toast.error(`Error: ${error.message || 'Transaction failed'}`) + onError: (error: unknown) => { + toast.error(`Error: ${getErrorMessage(error)}`) }, } ) @@ -149,16 +152,17 @@ export default function TransactionMirrorAdmin() { } else { toast('Transaction is not mirrored yet', { icon: 'ℹ️' }) } - } catch (error: any) { + } catch (error: unknown) { // Contract function may not exist or transaction not mirrored yet console.error('Error checking mirror status:', error) } } - } catch (error: any) { - if (error.message?.includes('Transaction receipt not found')) { + } catch (error: unknown) { + const message = getErrorMessage(error) + if (message.includes('Transaction receipt not found')) { toast.error('Transaction not found or not yet mined') } else { - toast.error(`Error checking transaction: ${error.message}`) + toast.error(`Error checking transaction: ${message}`) } } } diff --git a/frontend-dapp/src/components/admin/TransactionPreview.tsx b/frontend-dapp/src/components/admin/TransactionPreview.tsx index e78e0fb..4af3cb2 100644 --- a/frontend-dapp/src/components/admin/TransactionPreview.tsx +++ b/frontend-dapp/src/components/admin/TransactionPreview.tsx @@ -4,21 +4,34 @@ import { useState } from 'react' import { usePublicClient } from 'wagmi' +import type { Abi, AbiFunction } from 'viem' import { CONTRACT_ADDRESSES } from '../../config/contracts' import { MAINNET_TETHER_ABI } from '../../abis/MainnetTether' import { TRANSACTION_MIRROR_ABI } from '../../abis/TransactionMirror' import { simulateFunctionCall, getSimulationStatusEmoji } from '../../utils/transactionSimulator' import toast from 'react-hot-toast' +interface PreviewResult { + contractAddress: string + functionName: string + args: unknown[] + gasEstimate: string + returnValue: unknown + success: boolean + error?: string + abi: AbiFunction + timestamp: number +} + export default function TransactionPreview() { const publicClient = usePublicClient() const [contractAddress, setContractAddress] = useState(CONTRACT_ADDRESSES.mainnet.MAINNET_TETHER) const [functionName, setFunctionName] = useState('pause') const [args, setArgs] = useState('[]') - const [preview, setPreview] = useState(null) + const [preview, setPreview] = useState(null) const [isSimulating, setIsSimulating] = useState(false) - const getABI = () => { + const getABI = (): Abi => { return contractAddress === CONTRACT_ADDRESSES.mainnet.MAINNET_TETHER ? MAINNET_TETHER_ABI : TRANSACTION_MIRROR_ABI @@ -36,7 +49,7 @@ export default function TransactionPreview() { const abi = getABI() // Get function from ABI - const func = abi.find((item: any) => item.name === functionName && item.type === 'function') + const func = abi.find((item): item is AbiFunction => item.type === 'function' && item.name === functionName) if (!func) { toast.error('Function not found in ABI') setIsSimulating(false) @@ -69,8 +82,8 @@ export default function TransactionPreview() { } else { toast.error(`Simulation failed: ${simulation.error}`, { icon: '❌' }) } - } catch (error: any) { - toast.error(`Preview failed: ${error.message}`) + } catch (error: unknown) { + toast.error(`Preview failed: ${error instanceof Error ? error.message : 'Preview failed'}`) } setIsSimulating(false) } @@ -174,7 +187,7 @@ export default function TransactionPreview() {

Function Signature

- {preview.abi.name}({preview.abi.inputs?.map((i: any) => i.type).join(', ') || ''}) + {preview.abi.name}({preview.abi.inputs?.map((input) => input.type).join(', ') || ''})

)} diff --git a/frontend-dapp/src/components/admin/TransactionQueuePriority.tsx b/frontend-dapp/src/components/admin/TransactionQueuePriority.tsx index 0b52e38..0c4f395 100644 --- a/frontend-dapp/src/components/admin/TransactionQueuePriority.tsx +++ b/frontend-dapp/src/components/admin/TransactionQueuePriority.tsx @@ -7,7 +7,7 @@ import { useAdmin } from '../../contexts/AdminContext' import { TransactionRequestStatus } from '../../types/admin' import toast from 'react-hot-toast' -export enum QueuePriority { +enum QueuePriority { LOW = 'LOW', NORMAL = 'NORMAL', HIGH = 'HIGH', diff --git a/frontend-dapp/src/components/admin/TransactionRetry.tsx b/frontend-dapp/src/components/admin/TransactionRetry.tsx index 5acaa98..5210872 100644 --- a/frontend-dapp/src/components/admin/TransactionRetry.tsx +++ b/frontend-dapp/src/components/admin/TransactionRetry.tsx @@ -10,6 +10,10 @@ import { TransactionRequestStatus } from '../../types/admin' import { MAINNET_TETHER_ABI } from '../../abis/MainnetTether' import { TRANSACTION_MIRROR_ABI } from '../../abis/TransactionMirror' import toast from 'react-hot-toast' +import type { AdminAction } from '../../types/admin' +import type { Abi, AbiFunction } from 'viem' + +const MAINNET_TETHER_ADDRESS = '0x15DF1D5BFDD8Aa4b380445D4e3E9B38d34283619' as const export default function TransactionRetry() { const { adminActions, updateAdminAction } = useAdmin() @@ -22,22 +26,22 @@ export default function TransactionRetry() { ) const getABI = (contractAddress: string) => { - if (contractAddress === '0x15DF1D5BFDD8Aa4b380445D4e3E9B38d34283619') { + if (contractAddress === MAINNET_TETHER_ADDRESS) { return MAINNET_TETHER_ABI } return TRANSACTION_MIRROR_ABI } - const handleRetry = (action: any) => { + const handleRetry = (action: AdminAction) => { setRetryingId(action.id) - const abi = getABI(action.contractAddress) + const abi = getABI(action.contractAddress) as Abi writeContract( { address: action.contractAddress as `0x${string}`, abi, - functionName: action.functionName as any, + functionName: action.functionName as AbiFunction['name'], args: action.args || [], chainId: mainnet.id, }, @@ -49,12 +53,13 @@ export default function TransactionRetry() { }) toast.success('Transaction retried') }, - onError: (error: any) => { + onError: (error: unknown) => { + const message = error instanceof Error ? error.message : 'Retry failed' updateAdminAction(action.id, { status: TransactionRequestStatus.FAILED, - error: error.message, + error: message, }) - toast.error(`Retry failed: ${error.message}`) + toast.error(`Retry failed: ${message}`) setRetryingId(null) }, } diff --git a/frontend-dapp/src/components/admin/TransactionTemplates.tsx b/frontend-dapp/src/components/admin/TransactionTemplates.tsx index 6db825b..da9a8dc 100644 --- a/frontend-dapp/src/components/admin/TransactionTemplates.tsx +++ b/frontend-dapp/src/components/admin/TransactionTemplates.tsx @@ -14,10 +14,19 @@ interface Template { description: string contractAddress: string functionName: string - args: any[] + args: unknown[] category: 'maintenance' | 'emergency' | 'configuration' } +interface CustomTemplateDraft { + name: string + description: string + contractAddress: string + functionName: string + args: string + category: Template['category'] +} + const defaultTemplates: Template[] = [ { id: 'pause_all', @@ -42,13 +51,13 @@ const defaultTemplates: Template[] = [ export default function TransactionTemplates() { const { createAdminAction } = useAdmin() const [templates, setTemplates] = useState(defaultTemplates) - const [customTemplate, setCustomTemplate] = useState({ + const [customTemplate, setCustomTemplate] = useState({ name: '', description: '', contractAddress: '', functionName: '', args: '', - category: 'maintenance' as const, + category: 'maintenance', }) useEffect(() => { @@ -93,14 +102,14 @@ export default function TransactionTemplates() { args: '', category: 'maintenance', }) - } catch (error) { + } catch { toast.error('Invalid JSON in args field') } } const executeTemplate = (template: Template) => { createAdminAction({ - type: template.functionName as any, + type: template.functionName, contractAddress: template.contractAddress, functionName: template.functionName, args: template.args, @@ -230,7 +239,12 @@ export default function TransactionTemplates() { setWalletAddress(e.target.value)} + className="w-full mb-3 rounded-lg border border-white/10 bg-black/40 px-3 py-2.5 text-white outline-none focus:border-emerald-500" + placeholder="0x..." + /> + + +
+ + setUsername(e.target.value)} + className="w-full mb-4 rounded-lg border border-white/10 bg-black/40 px-3 py-2.5 text-white outline-none focus:border-emerald-500" + required + /> + + setPassword(e.target.value)} + className="w-full mb-4 rounded-lg border border-white/10 bg-black/40 px-3 py-2.5 text-white outline-none focus:border-emerald-500" + required + /> + {error ? ( +

+ {error} +

+ ) : null} + +
+ + + ); + } + + return ( +
+
+
+ Session active · {sessionMinutesLeft} min remaining +
+ +
+ {children} +
+ ); +} diff --git a/frontend-dapp/src/features/owner-auth/ownerAuth.test.ts b/frontend-dapp/src/features/owner-auth/ownerAuth.test.ts new file mode 100644 index 0000000..bd82078 --- /dev/null +++ b/frontend-dapp/src/features/owner-auth/ownerAuth.test.ts @@ -0,0 +1,161 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { + OWNER_SESSION_KEY, + clearOwnerSession, + decodeJwtExp, + fetchWeb3Challenge, + isOwnerRole, + isOwnerSessionValid, + loginOwner, + loginOwnerWithWeb3, + logoutOwner, + readOwnerSession, + writeOwnerSession, +} from './ownerAuth'; + +function jwtWithExp(exp: number): string { + const header = btoa(JSON.stringify({ alg: 'none', typ: 'JWT' })); + const payload = btoa(JSON.stringify({ exp, userId: 1, username: 'pc.walker', role: 'super_admin' })); + return `${header}.${payload}.sig`; +} + +describe('ownerAuth', () => { + beforeEach(() => { + sessionStorage.clear(); + vi.restoreAllMocks(); + }); + + afterEach(() => { + sessionStorage.clear(); + }); + + it('accepts only super_admin', () => { + expect(isOwnerRole('super_admin')).toBe(true); + expect(isOwnerRole('admin')).toBe(false); + expect(isOwnerRole('operator')).toBe(false); + }); + + it('decodes JWT exp and rejects expired sessions', () => { + const future = Math.floor(Date.now() / 1000) + 3600; + const past = Math.floor(Date.now() / 1000) - 10; + expect(decodeJwtExp(jwtWithExp(future))).toBe(future); + expect( + isOwnerSessionValid({ + token: 'x', + username: 'pc.walker', + role: 'super_admin', + exp: past, + }), + ).toBe(false); + }); + + it('clears invalid stored sessions', () => { + writeOwnerSession({ + token: 'x', + username: 'pc.walker', + role: 'super_admin', + exp: Math.floor(Date.now() / 1000) - 5, + }); + expect(readOwnerSession()).toBeNull(); + expect(sessionStorage.getItem(OWNER_SESSION_KEY)).toBeNull(); + }); + + it('stores a valid owner session after login', async () => { + const exp = Math.floor(Date.now() / 1000) + 3600; + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({ + token: jwtWithExp(exp), + user: { username: 'pc.walker', role: 'super_admin' }, + }), + }), + ); + + const session = await loginOwner('pc.walker', 'secret'); + expect(session.username).toBe('pc.walker'); + expect(readOwnerSession()?.username).toBe('pc.walker'); + clearOwnerSession(); + expect(readOwnerSession()).toBeNull(); + }); + + it('logs out through the API when a token is present', async () => { + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({ success: true }), + }); + vi.stubGlobal('fetch', fetchMock); + + writeOwnerSession({ + token: 'token-123', + username: 'pc.walker', + role: 'super_admin', + exp: Math.floor(Date.now() / 1000) + 3600, + }); + + await logoutOwner('token-123'); + + expect(fetchMock).toHaveBeenCalledWith( + '/api/v1/admin/auth/logout', + expect.objectContaining({ + method: 'POST', + headers: expect.objectContaining({ + Authorization: 'Bearer token-123', + }), + }), + ); + expect(readOwnerSession()).toBeNull(); + }); + + it('rejects non-owner roles even with a token', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({ + token: jwtWithExp(Math.floor(Date.now() / 1000) + 3600), + user: { username: 'clerk', role: 'operator' }, + }), + }), + ); + + await expect(loginOwner('clerk', 'secret')).rejects.toThrow('not authorized'); + expect(readOwnerSession()).toBeNull(); + }); + + it('fetches a web3 challenge and stores a web3 session after signature verification', async () => { + const exp = Math.floor(Date.now() / 1000) + 3600; + const fetchMock = vi.fn() + .mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({ + address: '0xabc123', + nonce: 'nonce-1', + message: 'DBIS central-bank Web3 login\nDomain: secure.d-bis.org\nWallet: 0xabc123\nNonce: nonce-1', + expiresInSeconds: 600, + }), + }) + .mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({ + token: jwtWithExp(exp), + user: { username: 'wallet:0xabc123', role: 'super_admin', walletAddress: '0xabc123', authMethod: 'web3' }, + }), + }); + vi.stubGlobal('fetch', fetchMock); + + const challenge = await fetchWeb3Challenge('0xabc123'); + expect(challenge.nonce).toBe('nonce-1'); + + const session = await loginOwnerWithWeb3('0xabc123', 'signed-message'); + expect(session.authMethod).toBe('web3'); + expect(session.walletAddress).toBe('0xabc123'); + expect(readOwnerSession()?.authMethod).toBe('web3'); + }); +}); diff --git a/frontend-dapp/src/features/owner-auth/ownerAuth.ts b/frontend-dapp/src/features/owner-auth/ownerAuth.ts new file mode 100644 index 0000000..db13df2 --- /dev/null +++ b/frontend-dapp/src/features/owner-auth/ownerAuth.ts @@ -0,0 +1,221 @@ +export const OWNER_SESSION_KEY = 'omnl_owner_session'; +export const OWNER_LOGIN_PATH = '/api/v1/admin/auth/login'; +export const OWNER_LOGOUT_PATH = '/api/v1/admin/auth/logout'; +export const OWNER_ROLES = ['super_admin'] as const; + +export type OwnerSession = { + token: string; + username: string; + role: string; + exp: number; + authMethod?: 'password' | 'web3'; + walletAddress?: string; +}; + +type LoginResponse = { + token?: string; + error?: string; + user?: { + username?: string; + role?: string; + walletAddress?: string; + authMethod?: 'password' | 'web3'; + }; + expiresInSeconds?: number; +}; + +export class OwnerAuthError extends Error { + readonly status: number; + + constructor(message: string, status = 401) { + super(message); + this.name = 'OwnerAuthError'; + this.status = status; + } +} + +export function decodeJwtExp(token: string): number | null { + try { + const payload = token.split('.')[1]; + if (!payload) return null; + const normalized = payload.replace(/-/g, '+').replace(/_/g, '/'); + const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '='); + const json = JSON.parse(atob(padded)) as { exp?: unknown }; + return typeof json.exp === 'number' ? json.exp : null; + } catch { + return null; + } +} + +export function isOwnerRole(role: string | undefined): boolean { + return OWNER_ROLES.includes((role ?? '') as (typeof OWNER_ROLES)[number]); +} + +export function isOwnerSessionValid(session: OwnerSession | null, nowSec = Math.floor(Date.now() / 1000)): boolean { + if (!session?.token || !session.username || !isOwnerRole(session.role)) return false; + return session.exp > nowSec + 30; +} + +export function readOwnerSession(): OwnerSession | null { + try { + const raw = sessionStorage.getItem(OWNER_SESSION_KEY); + if (!raw) return null; + const parsed = JSON.parse(raw) as OwnerSession; + if (!isOwnerSessionValid(parsed)) { + sessionStorage.removeItem(OWNER_SESSION_KEY); + return null; + } + return parsed; + } catch { + sessionStorage.removeItem(OWNER_SESSION_KEY); + return null; + } +} + +export function writeOwnerSession(session: OwnerSession): void { + sessionStorage.setItem(OWNER_SESSION_KEY, JSON.stringify(session)); +} + +export function clearOwnerSession(): void { + sessionStorage.removeItem(OWNER_SESSION_KEY); +} + +export async function loginOwner(username: string, password: string): Promise { + const user = username.trim(); + if (!user || !password) { + throw new OwnerAuthError('Username and password are required', 400); + } + + let res: Response; + try { + res = await fetch(OWNER_LOGIN_PATH, { + method: 'POST', + credentials: 'same-origin', + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: JSON.stringify({ username: user, password }), + }); + } catch { + throw new OwnerAuthError('Owner login service unreachable', 503); + } + + let body: LoginResponse = {}; + try { + body = (await res.json()) as LoginResponse; + } catch { + if (res.status === 401) { + throw new OwnerAuthError('Portal access denied'); + } + throw new OwnerAuthError(res.ok ? 'Invalid login response' : 'Owner login failed', res.status); + } + + if (!res.ok || !body.token) { + throw new OwnerAuthError(body.error || 'Invalid credentials', res.status || 401); + } + + const role = body.user?.role ?? ''; + if (!isOwnerRole(role)) { + throw new OwnerAuthError('This account is not authorized for the online bank', 403); + } + + const exp = decodeJwtExp(body.token) ?? Math.floor(Date.now() / 1000) + 24 * 60 * 60; + const session: OwnerSession = { + token: body.token, + username: body.user?.username || user, + role, + exp, + authMethod: body.user?.authMethod ?? 'password', + }; + writeOwnerSession(session); + return session; +} + +type Web3Challenge = { + address?: string; + nonce?: string; + message?: string; + expiresInSeconds?: number; + error?: string; +}; + +export async function loginOwnerWithWeb3(address: string, signature: string): Promise { + const wallet = address.trim(); + if (!wallet || !signature) { + throw new OwnerAuthError('Wallet address and signature are required', 400); + } + + let res: Response; + try { + res = await fetch('/api/v1/admin/auth/web3/login', { + method: 'POST', + credentials: 'same-origin', + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: JSON.stringify({ address: wallet, signature }), + }); + } catch { + throw new OwnerAuthError('Web3 login service unreachable', 503); + } + + let body: LoginResponse = {}; + try { + body = (await res.json()) as LoginResponse; + } catch { + throw new OwnerAuthError(res.ok ? 'Invalid web3 login response' : 'Web3 login failed', res.status); + } + + if (!res.ok || !body.token) { + throw new OwnerAuthError(body.error || 'Web3 login failed', res.status || 401); + } + + const role = body.user?.role ?? ''; + if (!isOwnerRole(role)) { + throw new OwnerAuthError('This wallet is not authorized for the online bank', 403); + } + + const exp = decodeJwtExp(body.token) ?? Math.floor(Date.now() / 1000) + 4 * 60 * 60; + const session: OwnerSession = { + token: body.token, + username: body.user?.username || wallet, + role, + exp, + authMethod: body.user?.authMethod ?? 'web3', + walletAddress: body.user?.walletAddress ?? wallet, + }; + writeOwnerSession(session); + return session; +} + +export async function logoutOwner(token?: string): Promise { + if (!token) { + clearOwnerSession(); + return; + } + + try { + await fetch(OWNER_LOGOUT_PATH, { + method: 'POST', + credentials: 'same-origin', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}`, + }, + }); + } catch { + // Local logout must still complete even if the API is unavailable. + } finally { + clearOwnerSession(); + } +} + +export async function fetchWeb3Challenge(address: string): Promise { + const wallet = address.trim(); + if (!wallet) throw new OwnerAuthError('Wallet address is required', 400); + const res = await fetch(`/api/v1/admin/auth/web3/challenge?address=${encodeURIComponent(wallet)}`, { + credentials: 'same-origin', + headers: { Accept: 'application/json' }, + }); + const body = (await res.json().catch(() => ({}))) as Web3Challenge; + if (!res.ok) { + throw new OwnerAuthError(body.error || 'Unable to create Web3 challenge', res.status); + } + return body; +} diff --git a/frontend-dapp/src/features/public-content/PublicInstitutionContent.tsx b/frontend-dapp/src/features/public-content/PublicInstitutionContent.tsx new file mode 100644 index 0000000..b3e9f06 --- /dev/null +++ b/frontend-dapp/src/features/public-content/PublicInstitutionContent.tsx @@ -0,0 +1,545 @@ +import { Link } from 'react-router-dom'; + +import type { ReactNode } from 'react'; + +import DashboardSection from '../omnl-dashboard/DashboardSection'; + +import DashboardStatCard from '../omnl-dashboard/DashboardStatCard'; + +import { formatUsd } from '../../utils/formatMoney'; + +import type { MoneySupply } from '../central-bank/useCentralBank'; + +type OfficeSummary = { + officeId?: number; + name?: string; + externalId?: string; +}; + +type Props = { + office: OfficeSummary | null; + moneySupply: MoneySupply | null; + tokenCount: number; + loading: boolean; +}; + +type PublicationLink = { + label: string; + description: string; + href: string; + external?: boolean; + provenance: 'Live' | 'Modelled' | 'Reference' | 'Published'; +}; + +type MetadataRow = { + label: string; + value: string; + note: string; +}; + +const POLICY_ROWS = [ + { + topic: 'Trust and governance', + source: '/.well-known/trust.json', + status: 'Published', + provenance: 'Reference' as const, + note: 'Trust artifact and public boundary statements.', + }, + { + topic: 'Operating governance', + source: '/governance.json', + status: 'Published', + provenance: 'Reference' as const, + note: 'Operator and institutional guardrails for the public stack.', + }, + { + topic: 'Settlement access', + source: '/settlement/chains', + status: 'Live', + provenance: 'Live' as const, + note: 'Chain registry and capability flags for settlement routing.', + }, + { + topic: 'Office 24 opening policy', + source: '/office-24', + status: 'Published', + provenance: 'Published' as const, + note: 'Office-specific operating view and registry anchor.', + }, + { + topic: 'Public data API', + source: 'https://data.d-bis.org', + status: 'Live', + provenance: 'Live' as const, + note: 'Official data and metrics surface used by the portal.', + }, +] satisfies Array<{ + topic: string; + source: string; + status: string; + provenance: PublicationLink['provenance']; + note: string; +}>; + +const PUBLICATION_LINKS: PublicationLink[] = [ + { + label: 'Public content audit', + description: 'Strict peer-bank benchmark and remediation tracker.', + href: '/docs', + provenance: 'Published', + }, + { + label: 'Chain registry paths', + description: 'Public registry map and API source notes.', + href: '/office-24#chain-registry', + provenance: 'Reference', + }, + { + label: 'Infrastructure inventory', + description: 'Domains, systems, explorers, RPC and status monitoring.', + href: '/docs', + provenance: 'Published', + }, + { + label: 'Live dashboard', + description: 'Public metrics and workspace summary.', + href: '/hub', + provenance: 'Live', + }, + { + label: 'Public publications page', + description: 'Mandate, policy, statistics, and service directory.', + href: '/publications', + provenance: 'Published', + }, +]; + +const SERVICE_LINKS: PublicationLink[] = [ + { + label: 'Public dashboard', + description: 'Entry point for public metrics and institutional workspace.', + href: '/hub', + provenance: 'Live', + }, + { + label: 'Central bank surface', + description: 'Operational balances, payments, and settlement views.', + href: '/central-bank', + provenance: 'Live', + }, + { + label: 'Office 24', + description: 'Corridor registry, office opening, and settlement anchors.', + href: '/office-24', + provenance: 'Published', + }, + { + label: 'Status monitoring', + description: 'System health and service availability.', + href: 'https://status.d-bis.org/', + external: true, + provenance: 'Live', + }, + { + label: 'Developer reference', + description: 'Same-origin route map and integration notes.', + href: '/docs', + provenance: 'Reference', + }, + { + label: 'Public infrastructure', + description: 'Domain inventory and technical transparency.', + href: '/docs', + provenance: 'Published', + }, +]; + +const GOVERNANCE_ROWS: MetadataRow[] = [ + { + label: 'Institutional purpose', + value: 'Public operating portal', + note: 'Exposes public information, governed service routes, and operator surfaces with explicit boundaries.', + }, + { + label: 'Public source of truth', + value: 'This portal plus operator manifests', + note: 'Public pages are curated from the live portal; operator manifests define the canonical operational record.', + }, + { + label: 'Auth boundaries', + value: 'Public / authenticated / owner / operator', + note: 'Access tiers are separated so users can tell what is open, logged-in, restricted, or operator-only.', + }, + { + label: 'Data status labels', + value: 'Live / Modelled / Reference / Published', + note: 'Public claims carry explicit provenance to reduce ambiguity for auditors and external reviewers.', + }, +]; + +const CONTACT_ROWS: MetadataRow[] = [ + { + label: 'Editorial owner', + value: 'DBIS portal operators', + note: 'Responsible for page structure, publication metadata, and route integrity.', + }, + { + label: 'Operational owner', + value: 'Authorized super-admin / owner session', + note: 'Owner-only surfaces are gated and logout revocation is explicit on the API side.', + }, + { + label: 'Public contact', + value: 'Public support and institutional inboxes', + note: 'Use the published domain/contact channels for public requests instead of operator-only paths.', + }, + { + label: 'Change control', + value: 'Versioned commits and deploys', + note: 'Major changes should remain reviewable as separate commit slices with validation evidence.', + }, +]; + +const LEGAL_ROWS: MetadataRow[] = [ + { + label: 'Publication intent', + value: 'Informational institutional disclosure', + note: 'The page describes the public operating model and does not substitute for legal advice or formal notices.', + }, + { + label: 'Jurisdiction', + value: 'Host-jurisdiction publication context', + note: 'External publication obligations should be confirmed by legal counsel in the relevant deployment jurisdiction.', + }, + { + label: 'Scope', + value: 'Public portals and controlled access surfaces', + note: 'Operator-only tooling, secrets, and privileged actions remain outside the public disclosure set.', + }, + { + label: 'Revision practice', + value: 'Versioned and reviewable', + note: 'Material public claims should be traceable to a commit, a publication page, or a signed operational artifact.', + }, +]; + +function Badge({ children, tone }: { children: ReactNode; tone: 'live' | 'published' | 'reference' | 'modelled' }) { + const classes: Record = { + live: 'border-[#3dff8a]/30 text-[#3dff8a] bg-[#3dff8a]/10', + published: 'border-[#00d4ff]/25 text-[#00d4ff] bg-[#00d4ff]/10', + reference: 'border-[#ffb347]/25 text-[#ffb347] bg-[#ffb347]/10', + modelled: 'border-[#7a9bb8]/25 text-[#7a9bb8] bg-[#7a9bb8]/10', + }; + + return {children}; +} + +function ExternalOrInternalLink({ href, external, children }: { href: string; external?: boolean; children: ReactNode }) { + if (external) { + return ( + + {children} + + ); + } + + return ( + + {children} + + ); +} + +export default function PublicInstitutionContent({ office, moneySupply, tokenCount, loading }: Props) { + return ( +
+ +
+
+

Public portal

+

DBIS publishes the institutional portal, live dashboard, and public infrastructure inventory.

+
+
+

Settlement role

+

+ Office {office?.officeId ?? 24} remains the operational settlement view for balances, payments, and corridor status. +

+
+
+

Public services

+

Status, developer reference, and transparency surfaces are exposed as same-origin routes or public FQDNs.

+
+
+

+ DBIS is not claiming a sovereign monetary-policy mandate here. The public site documents the operating model, trust + boundaries, and publication rules that govern the portal and settlement surfaces. +

+
+ + +
+ + + + + + + + + + + + {POLICY_ROWS.map((row) => ( + + + + + + + + ))} + +
TopicSourceStatusProvenanceNote
{row.topic} + {row.source} + + {row.status} + + {row.provenance} + {row.note}
+
+
+ +
+ {loading ? 'refreshing' : 'live'}} + > +
+ + + + + + +
+

+ Series above are pulled from the settlement middleware where available and otherwise modelled from the live registry + and office ledger configuration. +

+
+
+ +
+ +
+ {PUBLICATION_LINKS.map((item) => ( +
+
+
+

{item.label}

+

{item.description}

+
+ {item.provenance} +
+ + Open + +
+ ))} +
+
+
+ +
+ +
+
+

Governance summary

+
+ {GOVERNANCE_ROWS.map((row) => ( +
+
{row.label}
+
{row.value}
+
{row.note}
+
+ ))} +
+
+
+

Disclosure posture

+
    +
  • Public pages are intentionally concise and direct; operator-only routes stay out of the public menu where possible.
  • +
  • Published metrics distinguish live values from modeled or reference values so external readers can judge confidence correctly.
  • +
  • Institutional claims should carry provenance, revision history, and a clear owner before they are treated as official.
  • +
  • Authentication state should be obvious, and logout must revoke access both locally and at the API boundary.
  • +
+
+
+
+
+ +
+ +
+ {SERVICE_LINKS.map((item) => ( +
+
+
+

{item.label}

+

{item.description}

+
+ {item.provenance} +
+ + {item.href} + +
+ ))} +
+
+
+ +
+ +
+
+

Ownership and control

+
+ {CONTACT_ROWS.map((row) => ( +
+
{row.label}
+
{row.value}
+
{row.note}
+
+ ))} +
+
+
+

Recommended review checklist

+
    +
  • Check that every public page has a purpose, an owner, and a known source of truth.
  • +
  • Confirm that login-gated pages make the access tier obvious before credentials are entered.
  • +
  • Keep public navigation limited to routes that are meant to be discoverable by external readers.
  • +
  • Use publication pages for policy, disclosures, and explanation rather than operator controls.
  • +
+
+
+
+
+ + + +
+ +
+
+

Provenance legend

+
    +
  • Live Fetched from a current endpoint or live service.
  • +
  • Modelled Derived from live data, registry state, or computed series.
  • +
  • Reference Canonical repo or trust artifact.
  • +
  • Published Public page intended for broad institutional readership.
  • +
+
+
+

Recommended publication metadata

+
+{`Title
+Date
+Audience
+Owner
+Source of truth
+Provenance
+Revision status`}
+              
+
+
+
+
+
+ ); +} diff --git a/frontend-dapp/src/features/swap/DBISSwapPanel.tsx b/frontend-dapp/src/features/swap/DBISSwapPanel.tsx index 5cbdee9..a54e21f 100644 --- a/frontend-dapp/src/features/swap/DBISSwapPanel.tsx +++ b/frontend-dapp/src/features/swap/DBISSwapPanel.tsx @@ -58,14 +58,15 @@ function SidePanel({ export default function DBISSwapPanel() { const swap = useDbisSwap(); + const { loadMonitor, loadLedgers, isConnected, address } = swap; useEffect(() => { - swap.loadMonitor(); - }, [swap.loadMonitor]); + loadMonitor(); + }, [loadMonitor]); useEffect(() => { - if (swap.isConnected) swap.loadLedgers(); - }, [swap.isConnected, swap.address, swap.loadLedgers]); + if (isConnected) loadLedgers(); + }, [isConnected, address, loadLedgers]); const outDisplay = swap.plan?.amountOut && swap.tokenOut diff --git a/frontend-dapp/src/features/trade/BinanceTradePanel.tsx b/frontend-dapp/src/features/trade/BinanceTradePanel.tsx index 432cb0d..ceceeec 100644 --- a/frontend-dapp/src/features/trade/BinanceTradePanel.tsx +++ b/frontend-dapp/src/features/trade/BinanceTradePanel.tsx @@ -69,17 +69,18 @@ function OrderBookMock({ price }: { price: string }) { export default function BinanceTradePanel() { const swap = useDbisSwap(); + const { loadMonitor, loadLedgers, isConnected, address } = swap; const [side, setSide] = useState('buy'); const [marketSearch, setMarketSearch] = useState(''); const [orderType, setOrderType] = useState<'market' | 'limit'>('market'); useEffect(() => { - swap.loadMonitor(); - }, [swap.loadMonitor]); + loadMonitor(); + }, [loadMonitor]); useEffect(() => { - if (swap.isConnected) swap.loadLedgers(); - }, [swap.isConnected, swap.address, swap.loadLedgers]); + if (isConnected) loadLedgers(); + }, [isConnected, address, loadLedgers]); const quoteSymbol = 'cUSDT'; const quoteToken = swap.tokens.find((t) => t.symbol === quoteSymbol) ?? swap.tokens.find((t) => t.symbol.includes('USDT')); diff --git a/frontend-dapp/src/helpers/admin/safeHelpers.ts b/frontend-dapp/src/helpers/admin/safeHelpers.ts index 7711d3d..94b862f 100644 --- a/frontend-dapp/src/helpers/admin/safeHelpers.ts +++ b/frontend-dapp/src/helpers/admin/safeHelpers.ts @@ -2,7 +2,7 @@ * Safe SDK Helper Utilities - Utilities for Safe wallet operations */ -import type { Address } from 'viem' +import type { Abi, Address } from 'viem' export interface SafeConfig { owners: Address[] @@ -30,7 +30,7 @@ export function encodeSafeTransaction( _contractAddress: Address, _functionName: string, _args: unknown[], - _abi: any[] + _abi: Abi ): `0x${string}` { // This would use viem's encodeFunctionData in production // For now, return placeholder diff --git a/frontend-dapp/src/index.css b/frontend-dapp/src/index.css index 8df5e1b..98dc4f7 100644 --- a/frontend-dapp/src/index.css +++ b/frontend-dapp/src/index.css @@ -4,6 +4,22 @@ @tailwind components; @tailwind utilities; +:root { + color-scheme: dark; + --bg: #0d1210; + --bg-elevated: #151c19; + --bg-surface: #1a2320; + --bg-surface-2: #202925; + --border: rgba(255, 255, 255, 0.12); + --border-strong: rgba(255, 255, 255, 0.22); + --text: #f2f6f4; + --text-muted: #b9c3be; + --text-soft: #8f9b95; + --accent: #4ade80; + --accent-strong: #22c55e; + --accent-warm: #f0b90b; +} + body { margin: 0; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', @@ -11,7 +27,11 @@ body { sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - background: #141a16; + background: + radial-gradient(circle at top left, rgba(74, 222, 128, 0.1), transparent 32%), + radial-gradient(circle at top right, rgba(15, 118, 110, 0.12), transparent 28%), + var(--bg); + color: var(--text); min-height: 100vh; } @@ -84,30 +104,30 @@ body { } ::-webkit-scrollbar-track { - background: #f1f1f1; + background: #111714; border-radius: 10px; } ::-webkit-scrollbar-thumb { - background: linear-gradient(to bottom, #22c55e, #16a34a); + background: linear-gradient(to bottom, #3ddc84, #15803d); border-radius: 10px; } ::-webkit-scrollbar-thumb:hover { - background: linear-gradient(to bottom, #16a34a, #15803d); + background: linear-gradient(to bottom, #22c55e, #166534); } /* Glass morphism effect */ .glass { - background: rgba(255, 255, 255, 0.7); + background: rgba(14, 20, 18, 0.82); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.18); + border: 1px solid var(--border); } /* Gradient text */ .gradient-text { - background: linear-gradient(135deg, #22c55e 0%, #059669 100%); + background: linear-gradient(135deg, #4ade80 0%, #0ea5a4 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; @@ -115,13 +135,13 @@ body { /* Button hover effects */ .btn-gradient { - background: linear-gradient(135deg, #22c55e 0%, #059669 100%); + background: linear-gradient(135deg, #22c55e 0%, #0f766e 100%); transition: all 0.3s ease; } .btn-gradient:hover { transform: translateY(-2px); - box-shadow: 0 10px 25px rgba(34, 197, 94, 0.4); + box-shadow: 0 12px 30px rgba(34, 197, 94, 0.28); } /* Card hover effect */ @@ -131,7 +151,7 @@ body { .card-hover:hover { transform: translateY(-5px); - box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); } /* Additional animations */ @@ -193,7 +213,7 @@ body { /* Focus styles for accessibility */ *:focus-visible { - outline: 2px solid #22c55e; + outline: 2px solid #4ade80; outline-offset: 2px; border-radius: 4px; } @@ -223,8 +243,8 @@ textarea:focus { /* Loading spinner */ .spinner { - border: 3px solid rgba(59, 130, 246, 0.1); - border-top-color: #22c55e; + border: 3px solid rgba(255, 255, 255, 0.1); + border-top-color: #4ade80; border-radius: 50%; animation: spin 0.8s linear infinite; } diff --git a/frontend-dapp/src/pages/AdminConsole.tsx b/frontend-dapp/src/pages/AdminConsole.tsx index 7725119..0463164 100644 --- a/frontend-dapp/src/pages/AdminConsole.tsx +++ b/frontend-dapp/src/pages/AdminConsole.tsx @@ -69,8 +69,8 @@ export default function AdminConsole() { } else { throw new Error('Pause failed'); } - } catch (error: any) { - alert(`Error: ${error.message}`); + } catch (error: unknown) { + alert(`Error: ${error instanceof Error ? error.message : 'Pause failed'}`); } }; @@ -90,8 +90,8 @@ export default function AdminConsole() { } else { throw new Error('Refund failed'); } - } catch (error: any) { - alert(`Error: ${error.message}`); + } catch (error: unknown) { + alert(`Error: ${error instanceof Error ? error.message : 'Refund failed'}`); } }; diff --git a/frontend-dapp/src/pages/CentralBankPage.tsx b/frontend-dapp/src/pages/CentralBankPage.tsx index 22eb0a9..ab5e05c 100644 --- a/frontend-dapp/src/pages/CentralBankPage.tsx +++ b/frontend-dapp/src/pages/CentralBankPage.tsx @@ -1,6 +1,7 @@ import { useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import OnlineBankApp from '../features/online-bank/OnlineBankApp'; +import OwnerGate from '../features/owner-auth/OwnerGate'; export default function CentralBankPage() { const { hash } = useLocation(); @@ -12,5 +13,9 @@ export default function CentralBankPage() { el?.scrollIntoView({ behavior: 'smooth', block: 'start' }); }, [hash]); - return ; + return ( + + + + ); } diff --git a/frontend-dapp/src/pages/DocsPage.tsx b/frontend-dapp/src/pages/DocsPage.tsx index 6ea95ad..ff226c4 100644 --- a/frontend-dapp/src/pages/DocsPage.tsx +++ b/frontend-dapp/src/pages/DocsPage.tsx @@ -31,6 +31,36 @@ export default function DocsPage() { localhost, or 127.0.0.1. Institutional share URLs live in operator manifests.

+
+

Public content

+
    +
  • + + Public content and publications + {' '} + - mandate, policy, statistics, publications, and directory. +
  • +
  • + + Central Bank + {' '} + - live money supply and treasury view. +
  • +
  • + + Office 24 + {' '} + - corridor registry and settlement surface. +
  • +
  • + + Governance and disclosures + {' '} + - public controls, audience boundaries, and review posture. +
  • +
+
+

Policy

    diff --git a/frontend-dapp/src/pages/HubPage.tsx b/frontend-dapp/src/pages/HubPage.tsx index 7fa9102..db3a77b 100644 --- a/frontend-dapp/src/pages/HubPage.tsx +++ b/frontend-dapp/src/pages/HubPage.tsx @@ -18,6 +18,7 @@ const CHAIN138: { path: string; label: string; icon: NasaIconName }[] = [ { path: '/history', label: 'History', icon: 'chart' }, { path: '/wallets', label: 'Wallets', icon: 'globe' }, { path: '/docs', label: 'Developers / API docs', icon: 'link' }, + { path: '/publications', label: 'Public content', icon: 'ledger' }, { path: '/admin', label: 'Admin', icon: 'satellite' }, ]; diff --git a/frontend-dapp/src/pages/PublicationsPage.tsx b/frontend-dapp/src/pages/PublicationsPage.tsx new file mode 100644 index 0000000..6c6715b --- /dev/null +++ b/frontend-dapp/src/pages/PublicationsPage.tsx @@ -0,0 +1,44 @@ +import { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; + +import NasaPageHeader from '../features/omnl-dashboard/NasaPageHeader'; + +import { useCentralBank } from '../features/central-bank/useCentralBank'; + +import PublicInstitutionContent from '../features/public-content/PublicInstitutionContent'; + +export default function PublicationsPage() { + const { hash } = useLocation(); + const cb = useCentralBank(); + + useEffect(() => { + if (!hash) return; + const id = hash.replace(/^#/, ''); + document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }, [hash]); + + return ( +
    + + Published + Public + Live metrics + + } + /> + + +
    + ); +} diff --git a/frontend-dapp/src/pages/WalletsDemoPage.tsx b/frontend-dapp/src/pages/WalletsDemoPage.tsx index cb07f75..e3b5e77 100644 --- a/frontend-dapp/src/pages/WalletsDemoPage.tsx +++ b/frontend-dapp/src/pages/WalletsDemoPage.tsx @@ -56,17 +56,25 @@ const wallets = [ }), ] +const fallbackWalletAvatarUrl = '/favicon.ico' +const evmAddressPattern = /^0x[a-fA-F0-9]{40}$/ + +function getValidEvmAddress(address: string | undefined) { + return address && evmAddressPattern.test(address) ? address : undefined +} + function WalletsDemoContent() { const account = useActiveAccount() + const accountAddress = getValidEvmAddress(account?.address) const { data: balance, isLoading: balanceLoading } = useWalletBalance({ client, chain: defaultThirdwebChain, - address: account?.address, + address: accountAddress, }) const { data: shivaBalance, isLoading: shivaBalanceLoading } = useWalletBalance({ client, chain: shivaThirdwebChain, - address: account?.address, + address: accountAddress, }) return ( @@ -95,11 +103,13 @@ function WalletsDemoContent() { padding: '0.75rem 1.5rem', }, }} + detailsButton={{ connectedAccountAvatarUrl: fallbackWalletAvatarUrl }} + detailsModal={{ connectedAccountAvatarUrl: fallbackWalletAvatarUrl }} /> - {account && ( + {accountAddress && (

    - Address: {account.address} + Address: {accountAddress}

    {balanceLoading ? (

    Loading balance…

    @@ -123,17 +133,19 @@ function WalletsDemoContent() { chain={shivaThirdwebChain} wallets={wallets} theme="dark" - connectButton={{ - label: `Connect ${onexShiva.brand.wallet}`, - style: { + connectButton={{ + label: `Connect ${onexShiva.brand.wallet}`, + style: { backgroundColor: '#7c3aed', color: 'white', borderRadius: '0.75rem', - padding: '0.75rem 1.5rem', - }, - }} - /> - {account && ( + padding: '0.75rem 1.5rem', + }, + }} + detailsButton={{ connectedAccountAvatarUrl: fallbackWalletAvatarUrl }} + detailsModal={{ connectedAccountAvatarUrl: fallbackWalletAvatarUrl }} + /> + {accountAddress && (
    {shivaBalanceLoading ? (

    Loading Shiva balance…

    diff --git a/frontend-dapp/src/pages/ZWalletPage.tsx b/frontend-dapp/src/pages/ZWalletPage.tsx index 135aee7..a3fbdb9 100644 --- a/frontend-dapp/src/pages/ZWalletPage.tsx +++ b/frontend-dapp/src/pages/ZWalletPage.tsx @@ -40,12 +40,20 @@ const wallets = [ }), ]; +const fallbackWalletAvatarUrl = '/favicon.ico'; +const evmAddressPattern = /^0x[a-fA-F0-9]{40}$/; + +function getValidEvmAddress(address: string | undefined) { + return address && evmAddressPattern.test(address) ? address : undefined; +} + function ZWalletContent() { const account = useActiveAccount(); + const accountAddress = getValidEvmAddress(account?.address); const { data: balance, isLoading: balanceLoading } = useWalletBalance({ client, chain: zThirdwebChain, - address: account?.address, + address: accountAddress, }); if (!zWalletEnabled) { @@ -87,11 +95,13 @@ function ZWalletContent() { padding: '0.75rem 1.5rem', }, }} + detailsButton={{ connectedAccountAvatarUrl: fallbackWalletAvatarUrl }} + detailsModal={{ connectedAccountAvatarUrl: fallbackWalletAvatarUrl }} /> - {account && ( + {accountAddress && (

    - Address: {account.address} + Address: {accountAddress}

    {balanceLoading ? (

    Loading balance…

    @@ -110,9 +120,9 @@ function ZWalletContent() { )}
    - + - +
    ); } diff --git a/frontend-dapp/src/setupTests.ts b/frontend-dapp/src/setupTests.ts index 8ec7afc..8179670 100644 --- a/frontend-dapp/src/setupTests.ts +++ b/frontend-dapp/src/setupTests.ts @@ -24,7 +24,7 @@ const localStorageMock = { removeItem: vi.fn(), clear: vi.fn(), } -global.localStorage = localStorageMock as any +global.localStorage = localStorageMock as unknown as Storage // Mock matchMedia Object.defineProperty(window, 'matchMedia', { diff --git a/frontend-dapp/src/styles/app-dashboard.css b/frontend-dapp/src/styles/app-dashboard.css index 24842f5..5fc72d1 100644 --- a/frontend-dapp/src/styles/app-dashboard.css +++ b/frontend-dapp/src/styles/app-dashboard.css @@ -379,8 +379,8 @@ html:has(.omnl-app) body { .omnl-app-header { background: #1a241c !important; - border-color: rgba(34, 197, 94, 0.2) !important; - box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); + border-color: rgba(74, 222, 128, 0.32) !important; + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.28); } .nasa-nav-brand { @@ -401,7 +401,7 @@ html:has(.omnl-app) body { font-weight: 500; letter-spacing: normal; text-transform: none; - color: var(--nasa-muted); + color: #c7d0c9; border-radius: 8px; border: 1px solid transparent; transition: color 0.15s, background 0.15s; @@ -409,7 +409,7 @@ html:has(.omnl-app) body { .nasa-nav-link:hover { color: var(--nasa-text); - background: rgba(255, 255, 255, 0.05); + background: rgba(255, 255, 255, 0.08); } .nasa-nav-link--active { diff --git a/frontend-dapp/src/styles/dbis-dashboard.css b/frontend-dapp/src/styles/dbis-dashboard.css index 644a414..abeaf5c 100644 --- a/frontend-dapp/src/styles/dbis-dashboard.css +++ b/frontend-dapp/src/styles/dbis-dashboard.css @@ -321,7 +321,7 @@ min-height: 3.5rem; padding-bottom: env(safe-area-inset-bottom, 0); background: rgba(26, 36, 28, 0.96); - border-top: 1px solid rgba(34, 197, 94, 0.2); + border-top: 1px solid rgba(74, 222, 128, 0.3); backdrop-filter: blur(12px); } @@ -342,7 +342,7 @@ min-height: 3.5rem; font-size: 0.6rem; font-weight: 500; - color: #848e9c; + color: #c7d0c9; text-decoration: none; border: none; background: transparent; @@ -351,11 +351,11 @@ } .dbis-bottom-nav__item--active { - color: #4ade80; + color: #bbf7d0; } .dbis-bottom-nav__item:active { - background: rgba(255, 255, 255, 0.05); + background: rgba(255, 255, 255, 0.08); } .dbis-has-bottom-nav { diff --git a/frontend-dapp/src/styles/omnl-apps.css b/frontend-dapp/src/styles/omnl-apps.css index 7a1e223..cc1f42d 100644 --- a/frontend-dapp/src/styles/omnl-apps.css +++ b/frontend-dapp/src/styles/omnl-apps.css @@ -9,22 +9,23 @@ .cb-card, .o24-card, .dash-card { - background: #1e2329; - border: 1px solid #2b3139; - border-radius: 8px; + background: linear-gradient(180deg, rgba(26, 35, 32, 0.96), rgba(17, 23, 20, 0.96)); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 14px; padding: 1.25rem; + box-shadow: 0 12px 28px rgba(0, 0, 0, 0.24); } .dash-page-title { font-size: 1.875rem; font-weight: 700; - color: #eaecef; + color: #f4f7f5; line-height: 1.2; } .dash-page-subtitle { margin-top: 0.35rem; - color: #848e9c; + color: #b8c2bc; font-size: 0.95rem; max-width: 42rem; } @@ -55,21 +56,21 @@ } .dash-banner { - border-radius: 8px; + border-radius: 14px; padding: 1rem 1.15rem; - border: 1px solid #2b3139; + border: 1px solid rgba(255, 255, 255, 0.12); } .dash-banner__title { font-size: 0.95rem; font-weight: 600; - color: #eaecef; + color: #f4f7f5; margin-bottom: 0.25rem; } .dash-banner__detail { font-size: 0.85rem; - color: #848e9c; + color: #b8c2bc; line-height: 1.45; } @@ -101,12 +102,12 @@ } .dash-banner--loading { - background: #1e2329; + background: rgba(26, 35, 32, 0.7); } .dash-stat-label { font-size: 0.75rem; - color: #848e9c; + color: #b8c2bc; font-weight: 500; margin-bottom: 0.35rem; } @@ -114,26 +115,26 @@ .dash-stat-value { font-size: 1.65rem; font-weight: 700; - color: #eaecef; + color: #f4f7f5; font-variant-numeric: tabular-nums; } .dash-stat-hint { margin-top: 0.35rem; font-size: 0.75rem; - color: #848e9c; + color: #a4afaa; } .dash-section-title { font-size: 1.05rem; font-weight: 600; - color: #eaecef; + color: #f4f7f5; } .dash-section-subtitle { margin-top: 0.2rem; font-size: 0.8rem; - color: #848e9c; + color: #b8c2bc; } .dash-kv { @@ -142,7 +143,7 @@ gap: 1rem; padding: 0.45rem 0; font-size: 0.875rem; - border-bottom: 1px solid #2b3139; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); } .dash-kv:last-child { @@ -150,12 +151,12 @@ } .dash-kv dt { - color: #848e9c; + color: #b8c2bc; flex-shrink: 0; } .dash-kv dd { - color: #eaecef; + color: #f4f7f5; text-align: right; word-break: break-word; } @@ -168,7 +169,7 @@ .dash-quick-links__label { font-size: 0.75rem; font-weight: 500; - color: #848e9c; + color: #b8c2bc; margin-bottom: 0.5rem; } @@ -178,16 +179,16 @@ border-radius: 999px; font-size: 0.75rem; font-weight: 500; - color: #eaecef; - background: #2b3139; - border: 1px solid #474d57; + color: #f4f7f5; + background: rgba(43, 49, 57, 0.95); + border: 1px solid rgba(255, 255, 255, 0.14); transition: background 0.15s, border-color 0.15s; } .dash-quick-links__chip:hover { - background: #474d57; - border-color: #22c55e; - color: #4ade80; + background: rgba(58, 68, 62, 0.95); + border-color: #4ade80; + color: #d8ffe8; } .dash-btn { @@ -196,13 +197,13 @@ border-radius: 6px; font-size: 0.875rem; font-weight: 500; - background: #2b3139; - color: #eaecef; + background: rgba(43, 49, 57, 0.95); + color: #f4f7f5; transition: background 0.15s; } .dash-btn:hover { - background: #474d57; + background: rgba(58, 68, 62, 0.95); } .dash-btn:disabled { @@ -216,7 +217,7 @@ grid-template-columns: 240px 1fr; grid-template-rows: 1fr; min-height: calc(100vh - 56px); - background: #0b0e11; + background: linear-gradient(180deg, #090c0e 0%, #111614 100%); } @media (max-width: 1023px) { @@ -237,12 +238,12 @@ } .trade-market-row:hover { - background: #2b3139; + background: #202925; } .trade-market-row--active { - background: #2b3139; - border-left: 2px solid #22c55e; + background: #202925; + border-left: 2px solid #4ade80; } .trade-orderbook { @@ -251,5 +252,73 @@ } .omnl-app-header { - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28); +} + +.omnl-app { + background: + radial-gradient(circle at top left, rgba(74, 222, 128, 0.08), transparent 24%), + radial-gradient(circle at top right, rgba(245, 158, 11, 0.07), transparent 28%), + #0d1210; +} + +.nasa-header { + border: 1px solid rgba(255, 255, 255, 0.12); + background: linear-gradient(180deg, rgba(26, 35, 32, 0.92), rgba(17, 23, 20, 0.92)); + border-radius: 18px; + padding: 1.25rem 1.25rem 1rem; + box-shadow: 0 12px 30px rgba(0, 0, 0, 0.24); +} + +.nasa-header__title-row { + display: flex; + align-items: center; + gap: 0.9rem; +} + +.nasa-header__icon-wrap { + width: 2.75rem; + height: 2.75rem; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 0.9rem; + background: rgba(74, 222, 128, 0.14); + border: 1px solid rgba(74, 222, 128, 0.22); +} + +.nasa-title { + margin: 0; + color: #f4f7f5; + font-size: 1.65rem; + line-height: 1.15; + letter-spacing: -0.02em; +} + +.nasa-header--centered .nasa-header__body { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; +} + +.nasa-header__body { + display: flex; + flex-direction: column; + gap: 0.9rem; +} + +.dbis-bottom-nav { + background: rgba(13, 18, 16, 0.96); + border-top: 1px solid rgba(255, 255, 255, 0.12); + backdrop-filter: blur(14px); +} + +.dbis-bottom-nav__item { + color: #b8c2bc; +} + +.dbis-bottom-nav__item--active { + color: #f4f7f5; + background: rgba(74, 222, 128, 0.14); } diff --git a/frontend-dapp/src/styles/online-bank.css b/frontend-dapp/src/styles/online-bank.css index 5f2f42d..08ab86d 100644 --- a/frontend-dapp/src/styles/online-bank.css +++ b/frontend-dapp/src/styles/online-bank.css @@ -31,7 +31,7 @@ .omnl-banking-theme .omnl-app-header { background: #ffffff !important; - border-bottom: 1px solid var(--rb-border) !important; + border-bottom: 1px solid var(--rb-border-strong) !important; box-shadow: none !important; } @@ -42,7 +42,7 @@ } .omnl-banking-theme .omnl-app-header nav a { - color: var(--rb-muted) !important; + color: var(--rb-text-body) !important; } .omnl-banking-theme .omnl-app-header nav a.bg-green-600 { @@ -59,7 +59,7 @@ } .omnl-banking-theme .omnl-app-header .hover\:bg-white\/10:hover { - background: var(--rb-bg) !important; + background: var(--rb-accent-soft) !important; } .revolut-bank__inner { diff --git a/frontend-dapp/src/styles/z-ecosystem.css b/frontend-dapp/src/styles/z-ecosystem.css index 48c7fc0..f3567ab 100644 --- a/frontend-dapp/src/styles/z-ecosystem.css +++ b/frontend-dapp/src/styles/z-ecosystem.css @@ -14,7 +14,7 @@ min-height: 3.5rem; padding-bottom: env(safe-area-inset-bottom, 0); background: rgba(26, 22, 36, 0.96); - border-top: 1px solid rgba(147, 51, 234, 0.35); + border-top: 1px solid rgba(196, 181, 253, 0.42); backdrop-filter: blur(12px); } @@ -35,7 +35,7 @@ min-height: 3.5rem; font-size: 0.6rem; font-weight: 500; - color: #9ca3af; + color: #d1d5db; text-decoration: none; border: none; background: transparent; @@ -44,11 +44,11 @@ } .z-ecosystem-theme .z-bottom-nav__item--active { - color: #c084fc; + color: #e9d5ff; } .z-ecosystem-theme .z-bottom-nav__item:active { - background: rgba(255, 255, 255, 0.05); + background: rgba(255, 255, 255, 0.08); } .z-ecosystem-theme.omnl-banking-theme .omnl-app-header nav a.bg-purple-600 { diff --git a/frontend-dapp/src/types/admin.ts b/frontend-dapp/src/types/admin.ts index 515b922..404c2eb 100644 --- a/frontend-dapp/src/types/admin.ts +++ b/frontend-dapp/src/types/admin.ts @@ -61,7 +61,7 @@ export interface TransactionRequest { error?: string; contractAddress?: string; // For admin actions functionName?: string; // For admin actions - args?: any[]; // For admin actions + args?: unknown[]; // For admin actions } export interface MultiSigApproval { @@ -93,7 +93,7 @@ export interface GasEstimate { export interface WalletBalance { native: string; nativeFormatted: string; - tokens: any[]; + tokens: TokenBalance[]; totalUsdValue?: string; } @@ -110,10 +110,10 @@ export interface TokenBalance { export interface AdminAction { id: string; - type: 'pause' | 'unpause' | 'setAdmin' | 'anchorStateProof' | 'mirrorTransaction' | 'custom'; + type: string; contractAddress: string; functionName: string; - args: any[]; + args: unknown[]; status: TransactionRequestStatus; createdAt: number; executedAt?: number; @@ -129,7 +129,7 @@ export interface AuditLog { resourceType: string; resourceId: string; ipAddress?: string; - details?: any; + details?: Record | unknown; status: 'success' | 'failure'; } diff --git a/frontend-dapp/src/utils/contractEvents.ts b/frontend-dapp/src/utils/contractEvents.ts index f2ed5c3..4fa08d6 100644 --- a/frontend-dapp/src/utils/contractEvents.ts +++ b/frontend-dapp/src/utils/contractEvents.ts @@ -3,12 +3,12 @@ */ import type { Address } from 'viem' -import type { Abi } from 'viem' +import type { Abi, AbiEvent, PublicClient } from 'viem' export interface ContractEvent { address: Address eventName: string - args: any + args: unknown blockNumber: bigint transactionHash: `0x${string}` logIndex: number @@ -21,7 +21,7 @@ export type EventCallback = (event: ContractEvent) => void * Subscribe to contract events */ export async function subscribeToContractEvents( - publicClient: any, + publicClient: Pick, contractAddress: Address, abi: Abi, eventName: string, @@ -43,7 +43,7 @@ export async function subscribeToContractEvents( event: { type: 'event', name: eventName, - } as any, + } as AbiEvent, fromBlock: lastBlock, toBlock: currentBlock, }) @@ -91,7 +91,7 @@ export async function subscribeToContractEvents( * Listen to specific event once */ export async function waitForEvent( - publicClient: any, + publicClient: Pick, contractAddress: Address, abi: Abi, eventName: string, @@ -118,7 +118,7 @@ export async function waitForEvent( * Get recent events for a contract */ export async function getRecentEvents( - publicClient: any, + publicClient: Pick, contractAddress: Address, abi: Abi, eventName: string, @@ -133,7 +133,7 @@ export async function getRecentEvents( event: { type: 'event', name: eventName, - } as any, + } as AbiEvent, fromBlock, toBlock: currentBlock, }) diff --git a/frontend-dapp/src/utils/realtimeMonitor.ts b/frontend-dapp/src/utils/realtimeMonitor.ts index e856890..61faa00 100644 --- a/frontend-dapp/src/utils/realtimeMonitor.ts +++ b/frontend-dapp/src/utils/realtimeMonitor.ts @@ -7,7 +7,13 @@ import type { Address } from 'viem' export interface MonitorEvent { type: 'block' | 'transaction' | 'contract_event' | 'state_change' timestamp: number - data: any + data: unknown +} + +interface RealtimeMessage { + type?: string + key?: string + event?: MonitorEvent } export type EventCallback = (event: MonitorEvent) => void @@ -153,11 +159,18 @@ class RealtimeMonitor { /** * Handle incoming WebSocket message */ - private handleMessage(data: any): void { - const { type, key, event } = data + private handleMessage(data: unknown): void { + if (typeof data !== 'object' || data === null) { + return + } + + const message = data as RealtimeMessage + const { type, key, event } = message if (type === 'event' && key) { - this.notifyListeners(key, event) + if (event) { + this.notifyListeners(key, event) + } } } @@ -180,7 +193,7 @@ class RealtimeMonitor { /** * Send message to WebSocket server */ - send(message: any): void { + send(message: unknown): void { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(message)) } else { @@ -215,12 +228,12 @@ export function getRealtimeMonitor(wsUrl?: string): RealtimeMonitor { */ export async function monitorContractState( _contractAddress: Address, - _publicClient: any, - onStateChange: (state: any) => void, + _publicClient: unknown, + onStateChange: (state: Record) => void, pollInterval = 10000 ): Promise<() => void> { let isMonitoring = true - let lastState: any = null + let lastState: Record | null = null const checkState = async () => { if (!isMonitoring) return diff --git a/frontend-dapp/src/utils/security.ts b/frontend-dapp/src/utils/security.ts index 98cbd2d..cafd493 100644 --- a/frontend-dapp/src/utils/security.ts +++ b/frontend-dapp/src/utils/security.ts @@ -4,6 +4,7 @@ */ import { isAddress, getAddress } from 'viem'; +import type { PublicClient } from 'viem'; import { ERROR_MESSAGES, VALIDATION, SECURITY } from './constants'; /** @@ -29,8 +30,11 @@ export function validateAddress(address: string): { try { const checksummed = getAddress(address); return { valid: true, checksummed }; - } catch (error: any) { - return { valid: false, error: error.message || 'Address validation failed' }; + } catch (error: unknown) { + return { + valid: false, + error: error instanceof Error ? error.message : 'Address validation failed', + }; } } @@ -39,7 +43,7 @@ export function validateAddress(address: string): { */ export async function isContractAddress( address: string, - publicClient: any // viem PublicClient + publicClient: Pick // viem PublicClient ): Promise { try { const code = await publicClient.getBytecode({ address: address as `0x${string}` }); diff --git a/frontend-dapp/src/utils/transactionSimulator.ts b/frontend-dapp/src/utils/transactionSimulator.ts index 9d9e700..a7d7313 100644 --- a/frontend-dapp/src/utils/transactionSimulator.ts +++ b/frontend-dapp/src/utils/transactionSimulator.ts @@ -3,15 +3,16 @@ */ import type { Address } from 'viem' -import type { Abi } from 'viem' +import { formatEther } from 'viem' +import type { Abi, AbiFunction, Hex, PublicClient } from 'viem' export interface SimulationResult { success: boolean gasEstimate: bigint - returnValue?: any + returnValue?: unknown error?: string - traces?: any[] - logs?: any[] + traces?: unknown[] + logs?: unknown[] } export interface SimulatedCall { @@ -26,7 +27,7 @@ export interface SimulatedCall { * Simulate a contract call */ export async function simulateCall( - publicClient: any, + publicClient: Pick, call: SimulatedCall ): Promise { try { @@ -38,15 +39,14 @@ export async function simulateCall( return { success: true, - gasEstimate: result.gasUsed || 21000n, + gasEstimate: 21000n, returnValue: result.data, - logs: result.logs, } - } catch (error: any) { + } catch (error: unknown) { return { success: false, gasEstimate: 0n, - error: error.message || 'Simulation failed', + error: error instanceof Error ? error.message : 'Simulation failed', } } } @@ -55,7 +55,7 @@ export async function simulateCall( * Simulate contract function call */ export async function simulateFunctionCall( - publicClient: any, + publicClient: Pick, contractAddress: Address, abi: Abi, functionName: string, @@ -67,7 +67,7 @@ export async function simulateFunctionCall( const { encodeFunctionData } = await import('viem') const data = encodeFunctionData({ abi, - functionName: functionName as any, + functionName: functionName as never, args, }) @@ -86,16 +86,14 @@ export async function simulateFunctionCall( }) // Decode return value if possible - let returnValue: any = null + let returnValue: unknown = null try { const { decodeFunctionResult } = await import('viem') - const functionAbi = abi.find( - (item: any) => item.type === 'function' && item.name === functionName - ) as any + const functionAbi = abi.find((item): item is AbiFunction => item.type === 'function' && item.name === functionName) if (functionAbi && functionAbi.outputs && result.data && result.data !== '0x') { returnValue = decodeFunctionResult({ abi, - functionName: functionName as any, + functionName: functionAbi.name as never, data: result.data, }) } @@ -108,15 +106,15 @@ export async function simulateFunctionCall( success: true, gasEstimate, returnValue, - logs: result.logs, } - } catch (error: any) { + } catch (error: unknown) { // Try to extract revert reason - let errorMessage = error.message || 'Simulation failed' - if (error.data || error.cause?.data) { + const errorRecord = error as { data?: unknown; cause?: { data?: unknown }; message?: string } + let errorMessage = errorRecord.message || 'Simulation failed' + if (errorRecord.data || errorRecord.cause?.data) { try { const { decodeErrorResult } = await import('viem') - const errorData = error.data || error.cause?.data + const errorData = (errorRecord.data || errorRecord.cause?.data) as Hex const decoded = decodeErrorResult({ abi, data: errorData, @@ -139,7 +137,7 @@ export async function simulateFunctionCall( * Simulate batch of calls */ export async function simulateBatch( - publicClient: any, + publicClient: Pick, calls: SimulatedCall[] ): Promise { const results: SimulationResult[] = [] @@ -163,7 +161,6 @@ export function estimateGasCost( gasPrice: bigint ): string { const cost = gasEstimate * gasPrice - const { formatEther } = require('viem') return formatEther(cost) } diff --git a/frontend-dapp/src/vite-env.d.ts b/frontend-dapp/src/vite-env.d.ts index 911df7e..a873f4d 100644 --- a/frontend-dapp/src/vite-env.d.ts +++ b/frontend-dapp/src/vite-env.d.ts @@ -18,15 +18,9 @@ declare global { Buffer: typeof import('buffer').Buffer EventEmitter: typeof import('events').EventEmitter } - - // eslint-disable-next-line @typescript-eslint/no-empty-object-type - interface GlobalThis { - Buffer: typeof import('buffer').Buffer - EventEmitter: typeof import('events').EventEmitter - } - - var Buffer: typeof import('buffer').Buffer - var EventEmitter: typeof import('events').EventEmitter } +declare const Buffer: typeof import('buffer').Buffer +declare const EventEmitter: typeof import('events').EventEmitter + export {} diff --git a/frontend-dapp/vite.config.ts b/frontend-dapp/vite.config.ts index 5493c0f..e860d95 100644 --- a/frontend-dapp/vite.config.ts +++ b/frontend-dapp/vite.config.ts @@ -24,80 +24,84 @@ export default defineConfig(({ mode }) => { const isZ = env.VITE_ECOSYSTEM === 'z' return { - plugins: [ - react(), - ecosystemHtmlPlugin(isZ), - nodePolyfills({ - // Enable polyfills for specific modules - globals: { - Buffer: true, - global: true, - process: true, - }, - // Include specific polyfills - include: ['buffer', 'events', 'stream', 'util', 'crypto', 'vm'], - // Exclude Node.js built-ins that shouldn't be polyfilled - exclude: ['https', 'http', 'url', 'path', 'fs', 'os', 'net', 'tls', 'zlib'], - }), - ], - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - }, - // Dedupe to avoid "multiple instances" warnings from transitive deps (e.g. @emotion/react, Lit) - dedupe: [ - 'react', - 'react-dom', - '@emotion/react', - '@emotion/styled', - 'lit', - 'lit-html', - 'lit-element', + plugins: [ + react(), + ecosystemHtmlPlugin(isZ), + nodePolyfills({ + globals: { + Buffer: true, + global: true, + process: true, + }, + include: ['buffer', 'events', 'stream', 'util', 'crypto', 'vm'], + exclude: ['https', 'http', 'url', 'path', 'fs', 'os', 'net', 'tls', 'zlib'], + }), ], - }, - server: { - port: 3002, - proxy: { - '/api': { target: 'http://localhost:3000', changeOrigin: true }, - '/settlement': { target: 'https://secure.omdnl.org', changeOrigin: true }, - '/exchange': { target: 'https://exchange.omdnl.org', changeOrigin: true }, - '/omnl': { - target: 'http://localhost:3000', - changeOrigin: true, - bypass(req) { - const url = req.url ?? ''; - if (/^\/omnl\/(compliance|dashboard|terminal)\/?(\?.*)?$/.test(url)) return url; - if (url.startsWith('/omnl/static/')) return url; + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + // Dedupe to avoid "multiple instances" warnings from transitive deps (e.g. @emotion/react, Lit) + dedupe: [ + 'react', + 'react-dom', + '@emotion/react', + '@emotion/styled', + 'lit', + 'lit-html', + 'lit-element', + ], + }, + server: { + port: 3002, + proxy: { + '/api': { target: 'http://localhost:3000', changeOrigin: true }, + '/settlement': { target: 'https://secure.omdnl.org', changeOrigin: true }, + '/exchange': { target: 'https://exchange.omdnl.org', changeOrigin: true }, + '/omnl': { + target: 'http://localhost:3000', + changeOrigin: true, + bypass(req) { + const url = req.url ?? ''; + if (/^\/omnl\/(compliance|dashboard|terminal)\/?(\?.*)?$/.test(url)) return url; + if (url.startsWith('/omnl/static/')) return url; + }, + }, + '/reserve': { target: 'http://localhost:3000', changeOrigin: true }, + }, + }, + optimizeDeps: { + // Exclude problematic packages from optimization + exclude: ['https', 'http', 'url', 'stream', 'util', 'crypto', 'path', 'fs', 'os', 'net', 'tls', 'zlib'], + include: ['@safe-global/protocol-kit'], + }, + define: { + global: 'globalThis', + 'process.env': {}, + }, + build: { + rollupOptions: { + external: (id) => { + // Do NOT externalise crypto, buffer, stream, util, events - vite-plugin-node-polyfills provides them for the browser. + // Externalise only Node built-ins we don't polyfill (bundling would fail or break in browser). + const nodeBuiltIns = ['path', 'fs', 'os', 'net', 'tls', 'zlib', 'https', 'http', 'url'] + return nodeBuiltIns.includes(id) + }, + onwarn(warning, warn) { + const message = warning.message || '' + if ( + message.includes('contains an annotation that Rollup cannot interpret') || + message.includes('Use of eval in') + ) { + return + } + warn(warning) }, }, - '/reserve': { target: 'http://localhost:3000', changeOrigin: true }, - }, - }, - optimizeDeps: { - // Exclude problematic packages from optimization - exclude: ['https', 'http', 'url', 'stream', 'util', 'crypto', 'path', 'fs', 'os', 'net', 'tls', 'zlib'], - include: ['@safe-global/protocol-kit'], - }, - define: { - global: 'globalThis', - 'process.env': {}, - }, - build: { - rollupOptions: { - output: { - // Add Content Security Policy meta tag via HTML plugin if needed - }, - external: (id) => { - // Do NOT externalise crypto, buffer, stream, util, events - vite-plugin-node-polyfills provides them for the browser. - // Externalise only Node built-ins we don't polyfill (bundling would fail or break in browser). - const nodeBuiltIns = ['path', 'fs', 'os', 'net', 'tls', 'zlib', 'https', 'http', 'url'] - return nodeBuiltIns.includes(id) + commonjsOptions: { + transformMixedEsModules: true, }, + chunkSizeWarningLimit: 50000, }, - commonjsOptions: { - transformMixedEsModules: true, - }, - chunkSizeWarningLimit: 1000, - }, } }) diff --git a/scripts/deployment/print-omnl-dns-records.sh b/scripts/deployment/print-omnl-dns-records.sh index 60368df..7d911cb 100644 --- a/scripts/deployment/print-omnl-dns-records.sh +++ b/scripts/deployment/print-omnl-dns-records.sh @@ -2,6 +2,7 @@ # Print Cloudflare/registrar DNS records for OMNL production domains. # Origin server: dev-bis-ali @ 76.53.10.34 (nginx vhosts already deployed) +set -euo pipefail ORIGIN_IP="${OMNL_ORIGIN_IP:-76.53.10.34}" PROXY="${OMNL_DNS_PROXY:-proxied}" @@ -21,17 +22,25 @@ Zone: omdnl.org Zone: omdnl.org (Office 24) Type Name Content Proxy Dashboard A office24 $ORIGIN_IP yes Office 24 (/office-24) + A terminal $ORIGIN_IP yes OMNL terminal (/omnl/terminal) Zone: d-bis.org Type Name Content Proxy Notes A exchange $ORIGIN_IP yes DBIS Trade (/trade) A secure $ORIGIN_IP yes UPDATE — currently 76.53.10.36 A forex $ORIGIN_IP yes Forex terminal (/trade) + A terminal $ORIGIN_IP yes OMNL terminal alias (/omnl/terminal) + +Zone: novabank.uk + Type Name Content Proxy Dashboard + A terminal $ORIGIN_IP yes NovaBank terminal (/novabank/terminal) # Cloudflare SSL/TLS mode: Full (origin is HTTP on :80) # After DNS propagates (1–15 min), verify: # curl -sI https://exchange.omdnl.org/ | head -3 # curl -sI https://office24.omdnl.org/ | head -3 +# curl -sI https://hft-rail.omdnl.org/ | head -3 +# curl -sI https://terminal.novabank.uk/ | head -3 # curl -sI https://secure.d-bis.org/ | head -3 Live URLs once DNS is active: @@ -44,6 +53,9 @@ Live URLs once DNS is active: https://zblockchainsystem.com/z-wallet https://zblockchainsystem.com/swap https://office24.omdnl.org/office-24 + https://hft-rail.omdnl.org/ + https://terminal.d-bis.org/ + https://terminal.novabank.uk/ https://exchange.d-bis.org/trade https://secure.d-bis.org/central-bank https://forex.d-bis.org/trade