Rename OMNL DNS helper to hft-rail

This commit is contained in:
defiQUG
2026-08-30 17:27:16 -07:00
parent ee824cd1c8
commit 1f21d84e56
77 changed files with 2669 additions and 727 deletions
+5 -5
View File
@@ -7,7 +7,7 @@
<meta name="theme-color" content="#141a16" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<title>DBIS — Chain 138</title>
<title>OMNL / DBIS — Secure Portal</title>
<style>
html, body { margin: 0; min-height: 100%; background: #141a16; color: #eaecef; }
#root { min-height: 100vh; }
@@ -21,15 +21,15 @@
font-family: Inter, system-ui, sans-serif;
background: #141a16;
}
.omnl-boot__title { font-size: 1.125rem; font-weight: 600; color: #eaecef; }
.omnl-boot__hint { color: #848e9c; font-size: 0.875rem; }
.omnl-boot__title { font-size: 1.125rem; font-weight: 600; color: #eef2f0; }
.omnl-boot__hint { color: #9aa6a0; font-size: 0.875rem; }
</style>
</head>
<body>
<div id="root">
<div class="omnl-boot" role="status" aria-live="polite">
<div class="omnl-boot__title">DBIS</div>
<div class="omnl-boot__hint">Loading…</div>
<div class="omnl-boot__title">OMNL / DBIS</div>
<div class="omnl-boot__hint">Loading secure portal…</div>
</div>
</div>
<script type="module" src="/src/main.tsx"></script>
@@ -0,0 +1,78 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="omnl-api-base" content="auto" />
<title>NovaBank Core Ledger System</title>
<link rel="stylesheet" href="/omnl/static/omnl-settlement-terminal.css" />
</head>
<body class="theme-black" data-tenant="novabank" data-default-theme="black" data-brand-name="NovaBank Core Ledger System">
<div class="layout">
<header class="topbar">
<div>
<h1>NovaBank Core Ledger System</h1>
<p class="sub">Fineract SoR · Alchemy on-chain · internal document API</p>
</div>
<div class="toolbar">
<button type="button" id="btn-mode-black" class="active">Black / Green</button>
<button type="button" id="btn-mode-color">Gray / Black</button>
<button type="button" id="btn-refresh">Refresh</button>
<a class="btn" href="/omnl/compliance">Compliance</a>
<a class="btn" href="/api/v1/omnl/terminal/status">Status JSON</a>
</div>
</header>
<section class="params card">
<label>Connection
<select id="inp-connection">
<option value="">(none — legacy defaults)</option>
<option value="mk-albert-gate-limited">MK Albert Gate Limited</option>
</select>
</label>
<label>Settlement ref <input id="inp-settlement-ref" type="text" value="NOVA-POF-001" /></label>
<label>Office ID <input id="inp-office-id" type="number" value="30" min="1" /></label>
<label>Value date <input id="inp-value-date" type="date" value="2026-07-26" /></label>
<label>Amount USD <input id="inp-amount" type="text" value="200000000000.00" /></label>
<label>API token <input id="inp-token" type="password" placeholder="OMNL_API_KEY (optional)" autocomplete="off" /></label>
<label>User <input id="inp-user" type="text" value="novabank_operator" /></label>
<label>Officer <input id="inp-officer" type="text" value="nova_compliance_officer" /></label>
</section>
<div class="main">
<section class="screen-wrap card">
<pre id="terminal-screen" class="terminal" aria-live="polite">Loading…</pre>
</section>
<aside class="side card">
<h2>Commands</h2>
<div class="cmd-grid">
<button type="button" data-cmd="help">help</button>
<button type="button" data-cmd="status">status</button>
<button type="button" data-cmd="package">package</button>
<button type="button" data-cmd="pof">proof of funds</button>
<button type="button" data-cmd="debit">debit-note</button>
<button type="button" data-cmd="remittance">remittance</button>
<button type="button" data-cmd="token">token-san</button>
<button type="button" data-cmd="swift">swift-copy</button>
<button type="button" data-cmd="screen">screen</button>
<button type="button" data-cmd="alchemy-balance">alchemy balance</button>
</div>
<label>Alchemy address
<input id="inp-alchemy-addr" type="text" value="0xb3B416BdE671c256aAbFB56358baD91D46BB9c39" />
</label>
<h2>Output</h2>
<pre id="json-out" class="json-out">{}</pre>
</aside>
</div>
<footer>
<p>
API: <code>/api/v1/omnl/terminal/*</code> · Documents bound to Fineract core ledger — not SWIFT NET transmission.
Pass <code>?access_token=</code> or Bearer when <code>OMNL_API_KEY</code> is set.
</p>
</footer>
</div>
<script src="/omnl/static/omnl-api-base.js"></script>
<script src="/omnl/static/omnl-settlement-terminal.js"></script>
</body>
</html>
@@ -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 {
@@ -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();
})();
+16 -34
View File
@@ -4,22 +4,15 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="omnl-api-base" content="auto" />
<title>OMNL / HYBX Settlement Terminal</title>
<title>OMNL Core Ledger System</title>
<link rel="stylesheet" href="/omnl/static/omnl-settlement-terminal.css" />
</head>
<body class="theme-black">
<body class="theme-color" data-tenant="omnl" data-default-theme="color" data-brand-name="OMNL Core Ledger System">
<div class="layout">
<header class="topbar">
<div>
<h1>OMNL / HYBX Settlement Terminal</h1>
<p class="sub">Fineract SoR · Alchemy on-chain · institutional document API</p>
</div>
<div class="toolbar">
<button type="button" id="btn-mode-black" class="active">Black / Green</button>
<button type="button" id="btn-mode-color">Gray / Black</button>
<button type="button" id="btn-refresh">Refresh</button>
<a class="btn" href="/omnl/compliance">Compliance</a>
<a class="btn" href="/api/v1/omnl/terminal/status">Status JSON</a>
<h1>OMNL Core Ledger System</h1>
<p class="sub">Institutional appendix view · Fineract SoR · OMNL terminal API</p>
</div>
</header>
@@ -35,38 +28,27 @@
<label>Value date <input id="inp-value-date" type="date" value="2026-03-17" /></label>
<label>Amount USD <input id="inp-amount" type="text" value="1000000000.00" /></label>
<label>API token <input id="inp-token" type="password" placeholder="OMNL_API_KEY (optional)" autocomplete="off" /></label>
<label>User <input id="inp-user" type="text" value="omnl_operator" /></label>
<label>Officer <input id="inp-officer" type="text" value="compliance_officer" /></label>
</section>
<div class="main">
<section class="screen-wrap card">
<section class="screen-wrap card terminal-shell">
<div class="terminal-band" id="terminal-band">OMNL CORE LEDGER SYSTEM | HOST SESSION | READY</div>
<pre id="terminal-screen" class="terminal" aria-live="polite">Loading…</pre>
</section>
<aside class="side card">
<h2>Commands</h2>
<div class="cmd-grid">
<button type="button" data-cmd="help">help</button>
<button type="button" data-cmd="status">status</button>
<button type="button" data-cmd="package">package</button>
<button type="button" data-cmd="pof">pof</button>
<button type="button" data-cmd="debit">debit-note</button>
<button type="button" data-cmd="remittance">remittance</button>
<button type="button" data-cmd="token">token-san</button>
<button type="button" data-cmd="swift">swift-copy</button>
<button type="button" data-cmd="screen">screen</button>
<button type="button" data-cmd="alchemy-balance">alchemy balance</button>
</div>
<label>Alchemy address
<input id="inp-alchemy-addr" type="text" value="0xb3B416BdE671c256aAbFB56358baD91D46BB9c39" />
<label class="command-line">
<span class="prompt">===&gt;</span>
<input id="inp-command" type="text" value="status" autocomplete="off" spellcheck="false" aria-label="Command line" />
</label>
<h2>Output</h2>
<pre id="json-out" class="json-out">{}</pre>
</aside>
<div class="terminal-status" id="terminal-status">READY</div>
<pre id="json-out" class="json-out" aria-hidden="true">{}</pre>
</section>
</div>
<footer>
<p>
API: <code>/api/v1/omnl/terminal/*</code> · Documents bound to Fineract — not SWIFT NET transmission.
Pass <code>?access_token=</code> or Bearer when <code>OMNL_API_KEY</code> is set.
Document control · read-only appendix screen · Fineract core ledger.
Use <code>?access_token=</code> or Bearer when an OMNL dashboard or API token is set.
</p>
</footer>
</div>
+53 -6
View File
@@ -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 <div className="app-page-shell p-6 text-center text-[#d1d8d3]">Loading secure portal…</div>
}
function RouteDocumentTitle() {
const { pathname } = useLocation()
useEffect(() => {
const titleMap: Record<string, string> = {
'/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 (
<Routes>
@@ -58,7 +89,14 @@ function ZRoutes() {
}
>
<Route path="/z" element={<ZEcosystemHubPage />} />
<Route path="/z-wallet" element={<ZWalletPage />} />
<Route
path="/z-wallet"
element={
<Suspense fallback={<RouteLoading />}>
<ZWalletPage />
</Suspense>
}
/>
<Route path="/swap" element={<SwapPage />} />
<Route path="/central-bank" element={<CentralBankPage />} />
</Route>
@@ -75,6 +113,7 @@ function OmnlRoutes() {
<Route path="/hub" element={<HubPage />} />
<Route path="/central-bank" element={<CentralBankPage />} />
<Route path="/office-24" element={<Office24Page />} />
<Route path="/publications" element={<PublicationsPage />} />
<Route path="/trade" element={<TradePage />} />
<Route path="/swap" element={<SwapPage />} />
</Route>
@@ -90,7 +129,14 @@ function OmnlRoutes() {
<Route path="/history" element={<HistoryPage />} />
<Route path="/admin" element={<AdminPanel />} />
<Route path="/docs" element={<DocsPage />} />
<Route path="/wallets" element={<WalletsDemoPage />} />
<Route
path="/wallets"
element={
<Suspense fallback={<RouteLoading />}>
<WalletsDemoPage />
</Suspense>
}
/>
</Route>
<Route path="/z" element={<ExternalRedirect to="https://zblockchainsystem.com/z" />} />
<Route path="/z-wallet" element={<ExternalRedirect to="https://zblockchainsystem.com/z-wallet" />} />
@@ -110,6 +156,7 @@ function App() {
v7_relativeSplatPath: true,
}}
>
<RouteDocumentTitle />
<ToastProvider />
<AdminProvider>
{isZEcosystem ? <ZRoutes /> : isOmnlEcosystem ? <OmnlRoutes /> : <OmnlRoutes />}
@@ -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() {
</span>
</div>
<p className="text-white/60 text-xs">
User: <span className="font-mono">{log.user.slice(0, 10)}...</span> |{' '}
{log.resourceType} | {log.resourceId.slice(0, 10)}...
User: <span className="font-mono">{toText(log.user).slice(0, 10)}...</span> |{' '}
{toText(log.resourceType)} | {toText(log.resourceId).slice(0, 10)}...
</p>
{log.details && (
{Boolean(log.details) && (
<p className="text-white/50 text-xs mt-1">
{JSON.stringify(log.details).slice(0, 100)}...
{String(JSON.stringify(log.details)).slice(0, 100)}...
</p>
)}
</div>
@@ -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<BatchAction[]>([])
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}`)
}
}
@@ -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() {
<div className="space-y-4">
<div>
<label className="block text-red-200 text-sm mb-2">
Type "EMERGENCY" to enable emergency controls:
<label className="block text-red-200 text-sm mb-2">
Type &quot;EMERGENCY&quot; to enable emergency controls:
</label>
<input
type="text"
@@ -2,7 +2,7 @@
* GasOptimizer Component - Gas estimation and optimization
*/
import { useState, useEffect } from 'react'
import { useState, useEffect, useCallback } from 'react'
import { fetchGasPrices, getRecommendedGasPrice, type GasPriceRecommendation } from '../../helpers/admin/gasOracle'
import { formatEther } from 'viem'
@@ -22,7 +22,7 @@ export default function GasOptimizer() {
return () => clearInterval(interval)
}, [])
const calculateCost = () => {
const calculateCost = useCallback(() => {
if (!gasRecommendations || !estimatedGas) return
const recommendation = getRecommendedGasPrice(gasRecommendations, urgency)
@@ -30,11 +30,11 @@ export default function GasOptimizer() {
const maxFee = BigInt(recommendation.maxFeePerGas)
const cost = gasLimit * maxFee
setEstimatedCost(formatEther(cost))
}
}, [estimatedGas, gasRecommendations, urgency])
useEffect(() => {
calculateCost()
}, [estimatedGas, urgency, gasRecommendations])
}, [calculateCost])
if (!gasRecommendations) {
return (
@@ -58,8 +58,8 @@ export default function HardwareWalletSupport() {
toast.success(`Connected to ${connector.name}`)
}
} catch (error: any) {
toast.error(`Connection failed: ${error.message}`)
} catch (error: unknown) {
toast.error(`Connection failed: ${error instanceof Error ? error.message : 'Unknown error'}`)
console.error('Hardware wallet connection error:', error)
}
}
@@ -183,7 +183,7 @@ export default function HardwareWalletSupport() {
<li>Install Ledger Live application</li>
<li>Connect your Ledger device via USB</li>
<li>Unlock your device and open Ethereum app</li>
<li>Enable "Contract Data" in Ethereum app settings</li>
<li>Enable &quot;Contract Data&quot; in Ethereum app settings</li>
<li>Click Connect above</li>
</ul>
</div>
@@ -121,9 +121,9 @@ export default function ImpersonationMode() {
</button>
</div>
</div>
<p className="text-white/60 text-xs">
💡 This allows you to test admin functions from any address without needing that address's private key.
</p>
<p className="text-white/60 text-xs">
💡 This allows you to test admin functions from any address without needing that address&apos;s private key.
</p>
</div>
)}
</div>
@@ -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)}`)
},
}
)
@@ -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)
}
}
@@ -15,6 +15,8 @@ interface ContractState {
lastUpdate: number
}
type PartialContractState = Partial<Pick<ContractState, 'paused' | 'admin'>>
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
@@ -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',
@@ -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,
@@ -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: [],
@@ -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}`)
}
}
}
@@ -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<any>(null)
const [preview, setPreview] = useState<PreviewResult | null>(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() {
<div className="bg-white/5 rounded-lg p-4">
<p className="text-white/70 text-sm mb-1">Function Signature</p>
<p className="text-white text-xs font-mono">
{preview.abi.name}({preview.abi.inputs?.map((i: any) => i.type).join(', ') || ''})
{preview.abi.name}({preview.abi.inputs?.map((input) => input.type).join(', ') || ''})
</p>
</div>
)}
@@ -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',
@@ -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)
},
}
@@ -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<Template[]>(defaultTemplates)
const [customTemplate, setCustomTemplate] = useState({
const [customTemplate, setCustomTemplate] = useState<CustomTemplateDraft>({
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() {
<label className="block text-white/70 text-sm mb-2">Category</label>
<select
value={customTemplate.category}
onChange={(e) => setCustomTemplate({ ...customTemplate, category: e.target.value as any })}
onChange={(e) =>
setCustomTemplate({
...customTemplate,
category: e.target.value as CustomTemplateDraft['category'],
})
}
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-green-500"
>
<option value="maintenance">Maintenance</option>
@@ -5,6 +5,9 @@ import { CONTRACT_ADDRESSES } from '../../config/contracts'
import { TWOWAY_TOKEN_BRIDGE_L1_ABI } from '../../abis/TwoWayTokenBridge'
import toast from 'react-hot-toast'
const getErrorMessage = (error: unknown): string =>
error instanceof Error ? error.message : 'Transaction failed'
export default function TwoWayBridgeAdmin() {
const [newAdmin, setNewAdmin] = useState('')
const [chainSelector, setChainSelector] = useState('')
@@ -94,8 +97,8 @@ export default function TwoWayBridgeAdmin() {
setNewAdmin('')
refetchAdmin()
},
onError: (error: any) => {
toast.error(`Error: ${error.message || 'Transaction failed'}`)
onError: (error: unknown) => {
toast.error(`Error: ${getErrorMessage(error)}`)
},
}
)
@@ -121,8 +124,8 @@ export default function TwoWayBridgeAdmin() {
setChainSelector('')
setL2Bridge('')
},
onError: (error: any) => {
toast.error(`Error: ${error.message || 'Transaction failed'}`)
onError: (error: unknown) => {
toast.error(`Error: ${getErrorMessage(error)}`)
},
}
)
@@ -147,8 +150,8 @@ export default function TwoWayBridgeAdmin() {
toast.success('Fee token updated')
setFeeToken('')
},
onError: (error: any) => {
toast.error(`Error: ${error.message || 'Transaction failed'}`)
onError: (error: unknown) => {
toast.error(`Error: ${getErrorMessage(error)}`)
},
}
)
@@ -4,7 +4,7 @@
import { useState } from 'react'
import { useAdmin } from '../../contexts/AdminContext'
import { SecureStorage } from '../../utils/encryption'
import { decryptData, encryptData, SecureStorage } from '../../utils/encryption'
import { STORAGE_KEYS } from '../../utils/constants'
import toast from 'react-hot-toast'
@@ -30,8 +30,7 @@ export default function WalletBackup() {
}
// Encrypt backup
const { encryptData: encryptDataFn } = await import('../../utils/encryption')
const encrypted = await encryptDataFn(JSON.stringify(backup), backupPassword)
const encrypted = await encryptData(JSON.stringify(backup), backupPassword)
// Create download
const blob = new Blob([encrypted], { type: 'application/json' })
@@ -44,8 +43,8 @@ export default function WalletBackup() {
toast.success('Backup exported successfully')
setBackupPassword('')
} catch (error: any) {
toast.error(`Export failed: ${error.message}`)
} catch (error: unknown) {
toast.error(`Export failed: ${error instanceof Error ? error.message : 'Export failed'}`)
}
}
@@ -57,8 +56,7 @@ export default function WalletBackup() {
try {
// Decrypt backup
const { decryptData: decryptDataFn } = await import('../../utils/encryption')
const decrypted = await decryptDataFn(importData, backupPassword)
const decrypted = await decryptData(importData, backupPassword)
const backup = JSON.parse(decrypted)
// Validate backup
@@ -72,8 +70,8 @@ export default function WalletBackup() {
console.log('Backup data:', backup)
setImportData('')
setBackupPassword('')
} catch (error: any) {
toast.error(`Import failed: ${error.message}`)
} catch (error: unknown) {
toast.error(`Import failed: ${error instanceof Error ? error.message : 'Import failed'}`)
}
}
@@ -114,8 +114,8 @@ export default function WalletDeploymentEnhanced() {
*/
setIsDeploying(false)
} catch (error: any) {
toast.error(`Deployment failed: ${error.message}`)
} catch (error: unknown) {
toast.error(`Deployment failed: ${error instanceof Error ? error.message : 'Deployment failed'}`)
setIsDeploying(false)
}
}
@@ -53,8 +53,8 @@ export default function TransferTracking({ transferId: initialTransferId }: Tran
} else {
setIsPolling(false);
}
} catch (err: any) {
setError(err.message || 'Failed to fetch transfer');
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Failed to fetch transfer');
setTransfer(null);
setIsPolling(false);
} finally {
@@ -10,6 +10,12 @@ import CopyButton from '../ui/CopyButton';
import Tooltip from '../ui/Tooltip';
import ConfirmationModal from '../ui/ConfirmationModal';
type XRPLQuote = {
fee: string | number;
minReceived: string | number;
estimatedTime: string | number;
};
interface XRPLBridgeFormProps {
onBridge: (data: XRPLBridgeData) => Promise<void>;
}
@@ -29,7 +35,7 @@ export default function XRPLBridgeForm({ onBridge }: XRPLBridgeFormProps) {
const [token, setToken] = useState('native'); // 'native' or token address
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [quote, setQuote] = useState<any>(null);
const [quote, setQuote] = useState<XRPLQuote | null>(null);
const [showConfirmModal, setShowConfirmModal] = useState(false);
const [amountError, setAmountError] = useState<string>('');
const [addressError, setAddressError] = useState<string>('');
@@ -93,8 +99,8 @@ export default function XRPLBridgeForm({ onBridge }: XRPLBridgeFormProps) {
const quoteData = await response.json();
setQuote(quoteData);
toast.success('Quote received successfully!', { id: toastId });
} catch (err: any) {
const errorMsg = err.message || 'Failed to get quote';
} catch (err: unknown) {
const errorMsg = err instanceof Error ? err.message : 'Failed to get quote';
setError(errorMsg);
toast.error(errorMsg);
}
@@ -141,8 +147,8 @@ export default function XRPLBridgeForm({ onBridge }: XRPLBridgeFormProps) {
setDestinationAddress('');
setDestinationTag('');
setQuote(null);
} catch (err: any) {
const errorMsg = err.message || 'Bridge failed';
} catch (err: unknown) {
const errorMsg = err instanceof Error ? err.message : 'Bridge failed';
setError(errorMsg);
toast.error(errorMsg, { id: toastId });
} finally {
@@ -253,7 +259,7 @@ export default function XRPLBridgeForm({ onBridge }: XRPLBridgeFormProps) {
</p>
)}
<p className="text-xs text-gray-500 mt-1">
Enter a valid XRPL address (starts with 'r')
Enter a valid XRPL address (starts with &apos;r&apos;)
</p>
</div>
@@ -327,7 +333,7 @@ export default function XRPLBridgeForm({ onBridge }: XRPLBridgeFormProps) {
<span className="font-bold text-gray-900">{quote.fee} {token === 'native' ? 'ETH' : 'tokens'}</span>
</div>
<div className="flex justify-between items-center p-2 bg-white/60 rounded-lg">
<span className="font-medium text-gray-600">You'll receive:</span>
<span className="font-medium text-gray-600">You&apos;ll receive:</span>
<span className="font-bold text-green-600">{quote.minReceived} XRP</span>
</div>
<div className="flex justify-between items-center p-2 bg-white/60 rounded-lg">
+23 -11
View File
@@ -26,9 +26,9 @@ export default function Layout() {
]
return (
<div className="min-h-screen relative bg-[#141a16] flex flex-col dbis-has-bottom-nav">
<div className="min-h-screen relative flex flex-col dbis-has-bottom-nav">
<WalletDisconnectNotice userInitiatedDisconnectRef={userInitiatedDisconnectRef} />
<nav className="mobile-nav-bar bg-[#1a241c] border-b border-green-500/20 sticky top-0 z-50 safe-area-top">
<nav className="mobile-nav-bar sticky top-0 z-50 safe-area-top bg-[#121815]/95 backdrop-blur-md border-b border-white/12">
<div className="container mx-auto px-3 sm:px-4 max-w-7xl">
<div className="flex justify-between items-center h-14 gap-2">
<div className="flex items-center gap-2 min-w-0 flex-1">
@@ -42,7 +42,7 @@ export default function Layout() {
☰
</button>
<Link to="/hub" className="text-lg sm:text-xl font-semibold text-white whitespace-nowrap flex items-center gap-2">
<span className="text-green-400 font-bold">DBIS</span>
<span className="text-emerald-300 font-bold">DBIS</span>
</Link>
<div className="hidden sm:flex gap-1 ml-2 overflow-x-auto">
{navItems.filter((i) => i.to && !['/central-bank', '/office-24', '/trade', '/docs', '/admin', '/hub'].includes(i.to)).map((item) => (
@@ -50,7 +50,7 @@ export default function Layout() {
key={item.to}
to={item.to!}
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap min-h-[44px] flex items-center ${
item.active ? 'bg-green-600 text-white' : 'text-[#A0A0A0] hover:text-white hover:bg-white/5'
item.active ? 'bg-emerald-500 text-white shadow-[0_0_0_1px_rgba(255,255,255,0.08)]' : 'text-[#d3ddd8] hover:text-white hover:bg-white/10'
}`}
>
{item.label}
@@ -58,7 +58,7 @@ export default function Layout() {
))}
<a
href="/omnl/gru-vault-controls"
className="px-3 py-2 rounded-lg text-sm font-medium text-[#A0A0A0] hover:text-white hover:bg-white/5 transition-colors whitespace-nowrap min-h-[44px] flex items-center"
className="px-3 py-2 rounded-lg text-sm font-medium text-[#d3ddd8] hover:text-white hover:bg-white/10 transition-colors whitespace-nowrap min-h-[44px] flex items-center"
>
Vault Controls
</a>
@@ -66,7 +66,7 @@ export default function Layout() {
href={defaultFrontendExplorerUrl}
target="_blank"
rel="noopener noreferrer"
className="px-3 py-2 rounded-lg text-sm font-medium text-[#A0A0A0] hover:text-white hover:bg-white/5 transition-colors whitespace-nowrap min-h-[44px] flex items-center"
className="px-3 py-2 rounded-lg text-sm font-medium text-[#d3ddd8] hover:text-white hover:bg-white/10 transition-colors whitespace-nowrap min-h-[44px] flex items-center"
>
Explorer
</a>
@@ -85,15 +85,27 @@ export default function Layout() {
<main className="flex-1 w-full min-w-0 overflow-x-hidden">
<Outlet />
</main>
<footer className="border-t border-white/10 py-6 mt-auto safe-area-bottom">
<footer className="border-t border-white/12 py-6 mt-auto safe-area-bottom bg-[#0f1412]/95">
<div className="container mx-auto px-4 max-w-7xl">
<div className="flex flex-wrap justify-center items-center gap-x-4 gap-y-2 text-xs sm:text-sm text-[#A0A0A0]">
<div className="flex flex-col items-center gap-3 text-center text-xs sm:text-sm text-[#d3ddd8]">
<div className="flex flex-wrap justify-center items-center gap-x-4 gap-y-2">
<span>✔ Contracts verified</span>
<span>✔ Powered by CCIP</span>
<span className="hidden sm:inline">✔ Secure cross-chain messaging</span>
<Link to="/hub" className="hover:text-green-400 transition-colors min-h-[44px] flex items-center">DBIS Dashboard</Link>
<Link to="/wallets" className="hover:text-green-400 transition-colors min-h-[44px] flex items-center">Wallets</Link>
<Link to="/docs" className="hover:text-green-400 transition-colors min-h-[44px] flex items-center">Developers</Link>
<Link to="/publications#governance" className="hover:text-emerald-300 transition-colors min-h-[44px] flex items-center">Governance</Link>
<Link to="/publications#contact" className="hover:text-emerald-300 transition-colors min-h-[44px] flex items-center">Disclosures</Link>
<Link to="/hub" className="hover:text-emerald-300 transition-colors min-h-[44px] flex items-center">DBIS Dashboard</Link>
<Link to="/wallets" className="hover:text-emerald-300 transition-colors min-h-[44px] flex items-center">Wallets</Link>
<Link to="/docs" className="hover:text-emerald-300 transition-colors min-h-[44px] flex items-center">Developers</Link>
</div>
<p className="max-w-4xl text-[11px] sm:text-xs leading-relaxed text-[#a8b3ac]">
Public use only. This portal documents institutional operations, public publications, and controlled access
surfaces. Owner- and operator-only routes require authenticated access and are not part of the public disclosure set.
</p>
<p className="text-[11px] sm:text-xs text-[#7e8a83]">
Jurisdiction: operational disclosure intended for public review in the applicable host jurisdiction; legal counsel
should confirm any external publication requirements.
</p>
</div>
</div>
</footer>
@@ -7,6 +7,7 @@ type Props = {
const TABS = [
{ to: '/hub', label: 'Home', icon: 'dashboard' as const },
{ to: '/publications', label: 'Public', icon: 'ledger' as const },
{ to: '/bridge', label: 'Bridge', icon: 'bridge' as const },
{ to: '/trade', label: 'Trade', icon: 'trade' as const },
{ to: '/central-bank', label: 'Z Bank', icon: 'central-bank' as const },
@@ -17,6 +18,7 @@ export default function MobileBottomNav({ onOpenMenu }: Props) {
const isActive = (path: string) => {
if (path === '/hub') return location.pathname === '/hub' || location.pathname === '/';
if (path === '/publications') return location.pathname === '/publications' || location.pathname.startsWith('/publications');
if (path === '/bridge') return location.pathname === '/bridge' || location.pathname.startsWith('/bridge');
if (path === '/') return location.pathname === '/';
return location.pathname.startsWith(path);
@@ -36,13 +36,13 @@ export default function MobileNavDrawer({ open, onClose, title, items }: Props)
aria-label="Close menu"
onClick={onClose}
/>
<nav className="absolute top-0 left-0 bottom-0 w-[min(100vw,20rem)] bg-[#141a16] border-r border-green-500/20 flex flex-col shadow-2xl safe-area-pad">
<div className="flex items-center justify-between px-4 py-3 border-b border-white/10 min-h-[56px]">
<span className="text-white font-semibold text-sm">{title}</span>
<nav className="absolute top-0 left-0 bottom-0 w-[min(100vw,20rem)] bg-[#101612] border-r border-white/12 flex flex-col shadow-2xl safe-area-pad">
<div className="flex items-center justify-between px-4 py-3 border-b border-white/12 min-h-[56px]">
<span className="text-white font-semibold text-sm tracking-wide">{title}</span>
<button
type="button"
onClick={onClose}
className="text-white/80 hover:text-white p-2 min-h-[44px] min-w-[44px] flex items-center justify-center rounded-lg"
className="text-white/90 hover:text-white p-2 min-h-[44px] min-w-[44px] flex items-center justify-center rounded-lg hover:bg-white/10"
aria-label="Close"
>
✕
@@ -51,7 +51,7 @@ export default function MobileNavDrawer({ open, onClose, title, items }: Props)
<ul className="flex-1 overflow-y-auto py-2">
{items.map((item) => {
const className = `block w-full text-left px-4 py-3 text-sm font-medium min-h-[44px] ${
item.active ? 'bg-green-600/20 text-green-300 border-l-2 border-green-400' : 'text-[#A0A0A0] hover:text-white hover:bg-white/5'
item.active ? 'bg-emerald-500/18 text-emerald-100 border-l-2 border-emerald-300' : 'text-[#d3ddd8] hover:text-white hover:bg-white/10'
}`;
if (item.href) {
return (
@@ -10,6 +10,8 @@ const PRODUCTS: { path: string; label: string; icon: NasaIconName }[] = [
{ path: '/hub', label: 'DBIS Dashboard', icon: 'dashboard' },
{ path: '/central-bank', label: 'Central Bank', icon: 'central-bank' },
{ path: '/office-24', label: 'O24 Central Bank', icon: 'office' },
{ path: '/office-24#chain-registry', label: 'Chain registry', icon: 'network' },
{ path: '/publications', label: 'Public content', icon: 'ledger' },
{ path: '/trade', label: 'Trade', icon: 'trade' },
{ path: '/swap', label: 'Swap', icon: 'swap' },
];
@@ -19,8 +21,13 @@ export default function OmnlProductLayout() {
const userInitiatedDisconnectRef = useRef(false);
const [menuOpen, setMenuOpen] = useState(false);
const isActive = (path: string) =>
path === '/hub' ? location.pathname === '/hub' : location.pathname.startsWith(path);
const isActive = (path: string) => {
const basePath = path.split('#')[0];
return basePath === '/hub'
? location.pathname === '/hub'
: location.pathname === basePath || location.pathname.startsWith(`${basePath}/`);
};
const drawerItems = [
...PRODUCTS.map((p) => ({ to: p.path, label: p.label, active: isActive(p.path) })),
@@ -35,7 +42,7 @@ export default function OmnlProductLayout() {
return (
<div className={`omnl-app min-h-screen flex flex-col dbis-has-bottom-nav${isBanking ? ' omnl-banking-theme' : ''}`}>
<WalletDisconnectNotice userInitiatedDisconnectRef={userInitiatedDisconnectRef} />
<header className="omnl-app-header mobile-nav-bar sticky top-0 z-50 border-b border-green-500/20 safe-area-top bg-[#1a241c]">
<header className="omnl-app-header mobile-nav-bar sticky top-0 z-50 border-b border-white/12 safe-area-top bg-[#121815]/95 backdrop-blur-md">
<div className="max-w-[1600px] mx-auto px-3 sm:px-4 h-14 flex items-center gap-2 sm:gap-4">
<button
type="button"
@@ -47,7 +54,7 @@ export default function OmnlProductLayout() {
☰
</button>
<Link to="/hub" className="flex items-center gap-2 shrink-0 min-w-0">
<span className="w-8 h-8 rounded-lg bg-green-600 text-white font-bold flex items-center justify-center text-sm shrink-0">
<span className="w-8 h-8 rounded-lg bg-emerald-500 text-white font-bold flex items-center justify-center text-sm shrink-0">
<NasaIcon name="central-bank" size={16} />
</span>
<span className="font-semibold hidden sm:inline text-sm truncate text-white">DBIS OMNL</span>
@@ -59,7 +66,7 @@ export default function OmnlProductLayout() {
key={path}
to={path}
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap min-h-[40px] flex items-center gap-1.5 ${
isActive(path) ? 'bg-green-600 text-white' : 'text-[#848e9c] hover:text-white hover:bg-white/5'
isActive(path) ? 'bg-emerald-500 text-white shadow-[0_0_0_1px_rgba(255,255,255,0.08)]' : 'text-[#d3ddd8] hover:text-white hover:bg-white/10'
}`}
>
<NasaIcon name={icon} size={14} />
@@ -71,14 +78,14 @@ export default function OmnlProductLayout() {
<Link
to="/bridge"
className="hidden lg:inline-flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium text-[#848e9c] hover:text-white hover:bg-white/5 min-h-[40px]"
className="hidden lg:inline-flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium text-[#d3ddd8] hover:text-white hover:bg-white/10 min-h-[40px]"
>
<NasaIcon name="bridge" size={14} />
Bridge
</Link>
<a
href="/omnl/gru-vault-controls"
className="hidden xl:inline-flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium text-[#848e9c] hover:text-white hover:bg-white/5 min-h-[40px]"
className="hidden xl:inline-flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium text-[#d3ddd8] hover:text-white hover:bg-white/10 min-h-[40px]"
>
<NasaIcon name="admin" size={14} />
Vault Controls
@@ -30,7 +30,7 @@ export default function ZProductLayout() {
className={`omnl-app min-h-screen flex flex-col dbis-has-bottom-nav z-ecosystem-theme${isBanking ? ' omnl-banking-theme' : ''}`}
>
<WalletDisconnectNotice userInitiatedDisconnectRef={userInitiatedDisconnectRef} />
<header className="omnl-app-header mobile-nav-bar sticky top-0 z-50 border-b border-purple-500/30 safe-area-top bg-[#1a1624]">
<header className="omnl-app-header mobile-nav-bar sticky top-0 z-50 border-b border-white/12 safe-area-top bg-[#12131a]/95 backdrop-blur-md">
<div className="max-w-[1600px] mx-auto px-3 sm:px-4 h-14 flex items-center gap-2 sm:gap-4">
<button
type="button"
@@ -42,7 +42,7 @@ export default function ZProductLayout() {
☰
</button>
<Link to="/z" className="flex items-center gap-2 shrink-0 min-w-0">
<span className="w-8 h-8 rounded-lg bg-purple-600 text-white font-bold flex items-center justify-center text-sm shrink-0">
<span className="w-8 h-8 rounded-lg bg-violet-500 text-white font-bold flex items-center justify-center text-sm shrink-0">
Z
</span>
<span className="font-semibold hidden sm:inline text-sm truncate text-white">Z Blockchain System</span>
@@ -54,7 +54,7 @@ export default function ZProductLayout() {
key={path}
to={path}
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap min-h-[40px] flex items-center gap-1.5 ${
isActive(path) ? 'bg-purple-600 text-white' : 'text-[#848e9c] hover:text-white hover:bg-white/5'
isActive(path) ? 'bg-violet-500 text-white shadow-[0_0_0_1px_rgba(255,255,255,0.08)]' : 'text-[#d4d7e2] hover:text-white hover:bg-white/10'
}`}
>
<NasaIcon name={icon} size={14} />
@@ -76,8 +76,17 @@ export default function ZProductLayout() {
<Outlet />
</main>
<footer className="text-center text-xs text-[#6b7280] py-3 border-t border-white/5">
{Z_ECOSYSTEM_PUBLIC_URL.replace(/^https?:\/\//, '')}
<footer className="text-center text-xs text-[#d4d7e2] py-3 border-t border-white/12 bg-[#0e1016]/95">
<div className="flex flex-col items-center gap-1 px-4">
<span>{Z_ECOSYSTEM_PUBLIC_URL.replace(/^https?:\/\//, '')}</span>
<span className="max-w-4xl text-[11px] leading-relaxed text-[#9ca7bc]">
Public-facing Z surfaces are separate from owner or operator functions. Review the{' '}
<Link to="/publications#governance" className="underline underline-offset-2 hover:text-white">
governance and disclosures
</Link>{' '}
page for scope and audience boundaries.
</span>
</div>
</footer>
</div>
);
@@ -91,7 +91,7 @@ export default function WalletConnect({ onBeforeDisconnect }: WalletConnectProps
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setShowDropdown(!showDropdown)}
className="flex items-center gap-2 px-4 py-2.5 bg-[#1a1d24] rounded-xl border border-white/20 hover:border-white/30 transition-colors min-h-[44px] w-full md:w-auto"
className="flex items-center gap-2 px-4 py-2.5 bg-[#111714] rounded-xl border border-white/12 hover:border-white/24 transition-colors min-h-[44px] w-full md:w-auto"
aria-expanded={showDropdown}
aria-haspopup="true"
>
@@ -104,7 +104,7 @@ export default function WalletConnect({ onBeforeDisconnect }: WalletConnectProps
</svg>
</button>
{showDropdown && (
<div className="absolute right-0 mt-1 py-1 w-48 bg-[#252830] rounded-xl border border-white/10 shadow-xl z-50">
<div className="absolute right-0 mt-1 py-1 w-48 bg-[#151b18] rounded-xl border border-white/12 shadow-xl z-50">
<button onClick={copyAddress} className="w-full px-4 py-2.5 text-left text-sm text-white hover:bg-white/5 transition-colors">
Copy address
</button>
@@ -128,17 +128,17 @@ export default function WalletConnect({ onBeforeDisconnect }: WalletConnectProps
<>
<button
onClick={() => setShowConnectModal(true)}
className="px-6 py-3 bg-green-600 text-white font-medium rounded-xl hover:bg-green-500 transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 focus:ring-offset-[#252830] min-h-[44px] w-full md:w-auto"
className="px-6 py-3 bg-emerald-600 text-white font-medium rounded-xl hover:bg-emerald-500 transition-colors focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2 focus:ring-offset-[#151b18] min-h-[44px] w-full md:w-auto"
>
Connect Wallet
</button>
{showConnectModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60" onClick={() => setShowConnectModal(false)}>
<div className="bg-[#252830] rounded-2xl border border-white/10 shadow-2xl w-full max-w-md p-6" onClick={e => e.stopPropagation()}>
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm" onClick={() => setShowConnectModal(false)}>
<div className="bg-[#111714] rounded-2xl border border-white/12 shadow-2xl w-full max-w-md p-6" onClick={e => e.stopPropagation()}>
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-semibold text-white">Connect Wallet</h2>
<button onClick={() => setShowConnectModal(false)} className="p-2 text-[#A0A0A0] hover:text-white rounded-lg hover:bg-white/5 transition-colors" aria-label="Close">
<button onClick={() => setShowConnectModal(false)} className="p-2 text-[#b8c2bc] hover:text-white rounded-lg hover:bg-white/5 transition-colors" aria-label="Close">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
@@ -148,7 +148,7 @@ export default function WalletConnect({ onBeforeDisconnect }: WalletConnectProps
key={connector.uid}
onClick={() => handleConnect(connector)}
disabled={isPending}
className="w-full px-4 py-3 rounded-xl bg-[#1a1d24] border border-white/10 text-white font-medium hover:bg-white/5 hover:border-white/20 disabled:opacity-50 transition-colors text-left flex items-center justify-between"
className="w-full px-4 py-3 rounded-xl bg-[#151b18] border border-white/10 text-white font-medium hover:bg-white/5 hover:border-white/20 disabled:opacity-50 transition-colors text-left flex items-center justify-between"
>
<span>{connector.name}</span>
{isPending && (
+2 -2
View File
@@ -14,12 +14,12 @@ export const TOKEN_AGGREGATION_URL = prodOrLocal(
);
export const DBIS_EXCHANGE_URL = prodOrLocal(
import.meta.env.VITE_DBIS_EXCHANGE_URL,
'https://exchange.omdnl.org/exchange',
'/exchange',
'http://localhost:3012',
);
export const SETTLEMENT_MIDDLEWARE_URL = prodOrLocal(
import.meta.env.VITE_SETTLEMENT_MIDDLEWARE_URL,
'https://secure.omdnl.org/settlement',
'/settlement',
'http://localhost:3011',
);
+4 -3
View File
@@ -18,7 +18,7 @@ export const IN_APP_LINKS: EcosystemLink[] = [
{ id: 'bridge', label: 'Bridge', description: 'Cross-chain bridge, CCIP, XRPL, trustless', icon: 'bridge', href: '/bridge', accent: 'teal' },
{ id: 'swap', label: 'Digital Swap', description: 'M2 load, token swap, settlement rails', icon: 'swap', href: '/swap', accent: 'blue' },
{ id: 'trade', label: 'DBIS Trade', description: 'Markets, order book, spot trading', icon: 'trade', href: '/trade', accent: 'gold' },
{ id: 'central-bank', label: 'Central Bank', description: 'OMNL balances & payments', icon: 'central-bank', href: '/central-bank', accent: 'green' },
{ id: 'central-bank', label: 'Central Bank', description: 'Owner-only balances & payments', icon: 'central-bank', href: '/central-bank', accent: 'green' },
{ id: 'office-24', label: 'Office 24 Central Bank', description: '128-bank corridors · IPSAS settlement', icon: 'office', href: '/office-24', accent: 'purple' },
{ id: 'reserve', label: 'Reserve', description: 'Coverage, attestation, peg status', icon: 'money', href: '/reserve', accent: 'green' },
{ id: 'history', label: 'History', description: 'Bridge transfer lookup & tracking', icon: 'ledger', href: '/history', accent: 'slate' },
@@ -49,8 +49,8 @@ export const OPERATOR_CONSOLE_LINKS: EcosystemLink[] = [
/** Public production portals (OMNL / DBIS) */
export const PUBLIC_PORTAL_LINKS: EcosystemLink[] = [
{ id: 'pub-cb', label: 'Central Bank', description: 'secure.omdnl.org', icon: 'central-bank', href: 'https://secure.omdnl.org/central-bank', external: true, accent: 'green' },
{ id: 'pub-online', label: 'Online banking', description: 'online.omdnl.org', icon: 'globe', href: 'https://online.omdnl.org/central-bank', external: true, accent: 'green' },
{ id: 'pub-cb', label: 'Central Bank', description: 'secure.omdnl.org · owner login', icon: 'central-bank', href: 'https://secure.omdnl.org/central-bank', external: true, accent: 'green' },
{ id: 'pub-online', label: 'Online banking', description: 'online.omdnl.org · owner login', icon: 'globe', href: 'https://online.omdnl.org/central-bank', external: true, accent: 'green' },
{ id: 'pub-o24', label: 'Office 24 Central Bank', description: 'office24.omdnl.org', icon: 'office', href: 'https://office24.omdnl.org/office-24', external: true, accent: 'purple' },
{ id: 'pub-trade', label: 'Exchange', description: 'exchange.omdnl.org', icon: 'trade', href: 'https://exchange.omdnl.org/trade', external: true, accent: 'gold' },
{ id: 'pub-forex', label: 'Forex', description: 'forex.omdnl.org', icon: 'trade', href: 'https://forex.omdnl.org/trade', external: true, accent: 'gold' },
@@ -61,6 +61,7 @@ export const PUBLIC_PORTAL_LINKS: EcosystemLink[] = [
export const Z_ECOSYSTEM_PORTAL_LINKS: EcosystemLink[] = [
{ id: 'z-hub', label: 'Z Ecosystem', description: 'zblockchainsystem.com', icon: 'globe', href: 'https://zblockchainsystem.com/z', external: true, accent: 'purple' },
{ id: 'z-wallet', label: 'International Z Wallet', description: 'Z Chain wallet + corridors', icon: 'wallet', href: 'https://zblockchainsystem.com/z-wallet', external: true, accent: 'blue' },
{ id: 'z-cryptohost', label: 'Z CryptoHost', description: 'World ledgers → real → Z Wallet', icon: 'ledger', href: 'https://zblockchainsystem.com/z-cryptohost', external: true, accent: 'gold' },
{ id: 'z-bot', label: 'Z Bot', description: 'In-app assistant', icon: 'terminal', href: 'https://zblockchainsystem.com/z-wallet#z-bot', external: true, accent: 'teal' },
{ id: 'z-swap', label: 'Digital Swap', description: 'zblockchainsystem.com', icon: 'swap', href: 'https://zblockchainsystem.com/swap', external: true, accent: 'teal' },
{ id: 'z-bank', label: 'Z Online Bank', description: 'M0–M4 settlement UI', icon: 'central-bank', href: 'https://zblockchainsystem.com/central-bank', external: true, accent: 'green' },
+112 -111
View File
@@ -1,111 +1,112 @@
/**
* Same-origin internal path map for hub /docs and in-portal navigation.
* SSOT: proxmox repo config/omnl-internal-paths.v1.json — keep in sync on edits.
*/
export type InternalPathEntry = {
label: string;
path: string;
method?: string;
auth?: string;
notes?: string;
};
export type InternalPathGroup = {
id: string;
label: string;
entries: InternalPathEntry[];
};
export const INTERNAL_PATH_POLICY = {
internalLinks:
'Same-origin paths only — no FQDNs, localhost, or 127.0.0.1 in portal navigation or /docs links.',
nginxRequired:
'API prefixes /settlement/, /exchange/, /api/v1/, /omnl/ require nginx in front of the SPA (production LXC or dev deploy-omnl-domains.sh).',
externalShare:
'Institutional share URLs (https://… FQDNs) are documented in operator manifests — not used as internal links.',
} as const;
export const INTERNAL_PATH_PREFIXES = [
{ label: 'Settlement', prefix: '/settlement', upstream: '/api/v1/settlement', port: 3011 },
{ label: 'Exchange', prefix: '/exchange', upstream: '/api/v1/exchange', port: 3012 },
{ label: 'OMNL API', prefix: '/api/v1', upstream: '/api/v1', port: 3000 },
{ label: 'OMNL HTML', prefix: '/omnl', upstream: '/omnl', port: 3000 },
] as const;
export const INTERNAL_PATH_FORBIDDEN = [
'/settlement/api/v1/settlement/',
'/exchange/api/v1/exchange/',
'z-bank/settle-m0-m4',
] as const;
export const INTERNAL_PATH_GROUPS: InternalPathGroup[] = [
{
id: 'spa',
label: 'React SPA',
entries: [
{ label: 'DBIS Hub', path: '/hub' },
{ label: 'Central Bank', path: '/central-bank' },
{ label: 'M0→M4 panel', path: '/central-bank#z-full-settlement' },
{ label: 'Settlement protocols', path: '/central-bank#z-settlement-protocols' },
{ label: 'Office 24', path: '/office-24' },
{ label: 'DBIS Trade', path: '/trade' },
{ label: 'Digital Swap', path: '/swap' },
{ label: 'Bridge', path: '/bridge' },
{ label: 'Reserve', path: '/reserve' },
{ label: 'History', path: '/history' },
{ label: 'Wallets', path: '/wallets' },
{ label: 'Admin', path: '/admin' },
{ label: 'API docs', path: '/docs' },
],
},
{
id: 'settlement',
label: 'Settlement API',
entries: [
{ method: 'GET', path: '/settlement/health', auth: 'none' },
{ method: 'GET', path: '/settlement/public/money-supply', auth: 'none' },
{ method: 'GET', path: '/settlement/chains', auth: 'none' },
{ method: 'POST', path: '/settlement/external-transfer', auth: 'super-admin', notes: '→ Fineract journals' },
{ method: 'POST', path: '/settlement/token-load', auth: 'super-admin' },
{ method: 'POST', path: '/settlement/transfer/external', auth: 'super-admin' },
],
},
{
id: 'exchange',
label: 'Exchange API',
entries: [
{ method: 'GET', path: '/exchange/health', auth: 'none' },
{ method: 'GET', path: '/exchange/tokens', auth: 'read' },
{ method: 'GET', path: '/exchange/quote', auth: 'read' },
{ method: 'POST', path: '/exchange/plan', auth: 'trade' },
{ method: 'POST', path: '/exchange/swap/settle', auth: 'admin' },
],
},
{
id: 'omnl-api',
label: 'OMNL API',
entries: [
{ method: 'GET', path: '/api/v1/omnl/catalog', auth: 'none' },
{ method: 'GET', path: '/api/v1/omnl/gazette/notices', auth: 'none', notes: 'GUOSMM official gazette registry' },
{ method: 'GET', path: '/api/v1/omnl/gazette/notices/:documentId', auth: 'none' },
{ method: 'GET', path: '/api/v1/omnl/openapi.json', auth: 'none' },
{ method: 'GET', path: '/api/v1/omnl/health', auth: 'omnl-key-in-production' },
{ method: 'GET', path: '/api/v1/omnl/ipsas/fineract-health', auth: 'omnl-key-in-production' },
],
},
{
id: 'omnl-html',
label: 'OMNL HTML consoles',
entries: [
{ path: '/omnl/compliance', label: 'Compliance' },
{ path: '/omnl/dashboard', label: 'Dashboard' },
{ path: '/omnl/terminal', label: 'Terminal' },
],
},
];
export const INTERNAL_STACK_FLOW = [
'POST /settlement/external-transfer → settlement-middleware → Fineract POST /journalentries',
'GET /settlement/public/money-supply ← Fineract GL (fineractConnected / fineractLive)',
'GET /api/v1/omnl/ipsas/fineract-health → probes Fineract /glaccounts?limit=1',
] as const;
/**
* Same-origin internal path map for hub /docs and in-portal navigation.
* SSOT: proxmox repo config/omnl-internal-paths.v1.json — keep in sync on edits.
*/
export type InternalPathEntry = {
label: string;
path: string;
method?: string;
auth?: string;
notes?: string;
};
export type InternalPathGroup = {
id: string;
label: string;
entries: InternalPathEntry[];
};
export const INTERNAL_PATH_POLICY = {
internalLinks:
'Same-origin paths only — no FQDNs, localhost, or 127.0.0.1 in portal navigation or /docs links.',
nginxRequired:
'API prefixes /settlement/, /exchange/, /api/v1/, /omnl/ require nginx in front of the SPA (production LXC or dev deploy-omnl-domains.sh).',
externalShare:
'Institutional share URLs (https://… FQDNs) are documented in operator manifests — not used as internal links.',
} as const;
export const INTERNAL_PATH_PREFIXES = [
{ label: 'Settlement', prefix: '/settlement', upstream: '/api/v1/settlement', port: 3011 },
{ label: 'Exchange', prefix: '/exchange', upstream: '/api/v1/exchange', port: 3012 },
{ label: 'OMNL API', prefix: '/api/v1', upstream: '/api/v1', port: 3000 },
{ label: 'OMNL HTML', prefix: '/omnl', upstream: '/omnl', port: 3000 },
] as const;
export const INTERNAL_PATH_FORBIDDEN = [
'/settlement/api/v1/settlement/',
'/exchange/api/v1/exchange/',
'z-bank/settle-m0-m4',
] as const;
export const INTERNAL_PATH_GROUPS: InternalPathGroup[] = [
{
id: 'spa',
label: 'React SPA',
entries: [
{ label: 'DBIS Hub', path: '/hub' },
{ label: 'Central Bank', path: '/central-bank', auth: 'owner', notes: 'nginx basic + super_admin login' },
{ label: 'M0→M4 panel', path: '/central-bank#z-full-settlement', auth: 'owner' },
{ label: 'Settlement protocols', path: '/central-bank#z-settlement-protocols', auth: 'owner' },
{ label: 'Office 24', path: '/office-24' },
{ label: 'DBIS Trade', path: '/trade' },
{ label: 'Digital Swap', path: '/swap' },
{ label: 'Bridge', path: '/bridge' },
{ label: 'Reserve', path: '/reserve' },
{ label: 'History', path: '/history' },
{ label: 'Wallets', path: '/wallets' },
{ label: 'Admin', path: '/admin' },
{ label: 'API docs', path: '/docs' },
],
},
{
id: 'settlement',
label: 'Settlement API',
entries: [
{ label: 'Health', method: 'GET', path: '/settlement/health', auth: 'none' },
{ label: 'Money supply', method: 'GET', path: '/settlement/public/money-supply', auth: 'none' },
{ label: 'Supported chains', method: 'GET', path: '/settlement/chains', auth: 'none' },
{ label: 'External transfer', method: 'POST', path: '/settlement/external-transfer', auth: 'super-admin', notes: '→ Fineract journals' },
{ label: 'Token load', method: 'POST', path: '/settlement/token-load', auth: 'super-admin' },
{ label: 'External transfer dispatch', method: 'POST', path: '/settlement/transfer/external', auth: 'super-admin' },
],
},
{
id: 'exchange',
label: 'Exchange API',
entries: [
{ label: 'Health', method: 'GET', path: '/exchange/health', auth: 'none' },
{ label: 'Tokens', method: 'GET', path: '/exchange/tokens', auth: 'read' },
{ label: 'Quote', method: 'GET', path: '/exchange/quote', auth: 'read' },
{ label: 'Trade plan', method: 'POST', path: '/exchange/plan', auth: 'trade' },
{ label: 'Settle swap', method: 'POST', path: '/exchange/swap/settle', auth: 'admin' },
],
},
{
id: 'omnl-api',
label: 'OMNL API',
entries: [
{ label: 'Catalog', method: 'GET', path: '/api/v1/omnl/catalog', auth: 'none' },
{ label: 'Gazette notices', method: 'GET', path: '/api/v1/omnl/gazette/notices', auth: 'none', notes: 'GUOSMM official gazette registry' },
{ label: 'Gazette notice', method: 'GET', path: '/api/v1/omnl/gazette/notices/:documentId', auth: 'none' },
{ label: 'OpenAPI spec', method: 'GET', path: '/api/v1/omnl/openapi.json', auth: 'none' },
{ label: 'Owner login', method: 'POST', path: '/api/v1/admin/auth/login', auth: 'owner-credentials' },
{ label: 'Health', method: 'GET', path: '/api/v1/omnl/health', auth: 'omnl-key-in-production' },
{ label: 'Fineract health', method: 'GET', path: '/api/v1/omnl/ipsas/fineract-health', auth: 'omnl-key-in-production' },
],
},
{
id: 'omnl-html',
label: 'OMNL HTML consoles',
entries: [
{ path: '/omnl/compliance', label: 'Compliance' },
{ path: '/omnl/dashboard', label: 'Dashboard' },
{ path: '/omnl/terminal', label: 'Terminal' },
],
},
];
export const INTERNAL_STACK_FLOW = [
'POST /settlement/external-transfer → settlement-middleware → Fineract POST /journalentries',
'GET /settlement/public/money-supply ← Fineract GL (fineractConnected / fineractLive)',
'GET /api/v1/omnl/ipsas/fineract-health → probes Fineract /glaccounts?limit=1',
] as const;
+3 -2
View File
@@ -1,5 +1,5 @@
import { createConfig, http } from 'wagmi'
import { metaMask, walletConnect, coinbaseWallet } from 'wagmi/connectors'
import { injected, walletConnect, coinbaseWallet } from 'wagmi/connectors'
import {
allNetworks,
chainRpcUrls,
@@ -16,8 +16,9 @@ const transports = Object.fromEntries(
export const config = createConfig({
chains: allNetworks,
multiInjectedProviderDiscovery: false,
connectors: [
metaMask(),
injected({ target: 'metaMask', shimDisconnect: false }),
walletConnect({ projectId }),
coinbaseWallet({ appName: 'Bridge DApp' }),
],
+9 -2
View File
@@ -4,7 +4,13 @@
import { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react'
import { useAccount, usePublicClient } from 'wagmi'
import { AdminAction, AuditLog, AdminPreferences, TransactionRequestStatus } from '../types/admin'
import {
AdminAction,
AuditLog,
AdminPreferences,
TransactionExecutionMethod,
TransactionRequestStatus,
} from '../types/admin'
import { SecureStorage } from '../utils/encryption'
import { STORAGE_KEYS, DEFAULTS } from '../utils/constants'
import { generateSecureId } from '../utils/security'
@@ -39,7 +45,7 @@ const AdminContext = createContext<AdminContextType | undefined>(undefined)
const secureStorage = new SecureStorage()
const defaultPreferences: AdminPreferences = {
defaultExecutionMethod: DEFAULTS.EXECUTION_METHOD as any,
defaultExecutionMethod: DEFAULTS.EXECUTION_METHOD as TransactionExecutionMethod,
showAdvancedOptions: false,
autoApprove: false,
theme: 'dark',
@@ -232,6 +238,7 @@ export function AdminProvider({ children }: { children: ReactNode }) {
)
}
// eslint-disable-next-line react-refresh/only-export-components
export function useAdmin() {
const context = useContext(AdminContext)
if (context === undefined) {
@@ -61,7 +61,7 @@ export default function CentralBankDashboard() {
title="Central Bank Operations"
subtitle={`Live money supply for ${office?.name ?? 'HOSPITALLERS — Zardasht (Iraq / Iran Operations)'} (Office ${office?.officeId ?? 24}) · settlement · SWIFT · HYBX · Chain 138`}
subtitle={`Live money supply for ${office?.name ?? 'zBank'} (Office ${office?.officeId ?? 24}) · settlement · SWIFT · HYBX · Chain 138`}
badges={
@@ -83,6 +83,34 @@ export default function CentralBankDashboard() {
<QuickPortalLinks />
<DashboardSection
title="Public content"
subtitle="Mandate, policy, statistics, publications, and service directory."
icon="ledger"
className="mb-6"
action={
<Link to="/publications" className="text-xs text-[#00d4ff] hover:underline inline-flex items-center gap-1">
<NasaIcon name="arrow-right" size={12} />
Open publications
</Link>
}
>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-3">
{[
['Mandate', '/publications#mandate'],
['Policy', '/publications#policy'],
['Statistics', '/publications#statistics'],
['Publications', '/publications#publications'],
['Directory', '/publications#directory'],
].map(([label, to]) => (
<Link key={label} to={to} className="dash-quick-links__chip inline-flex !justify-start gap-2 py-2.5 px-3">
<NasaIcon name="ledger" size={14} />
{label}
</Link>
))}
</div>
</DashboardSection>
<div className="flex gap-2 flex-wrap mb-6">
{([
['overview', 'Overview'],
@@ -211,7 +239,7 @@ export default function CentralBankDashboard() {
<dt>Office</dt>
<dd>{office?.officeId ?? 24} · {office?.externalId ?? 'HOSPITALLERS-ZARDASHT-IRAQ-IRAN'}</dd>
<dd>{office?.officeId ?? 24} · {office?.externalId ?? 'zBank'}</dd>
</div>
@@ -52,7 +52,7 @@ export default function ChainMatrixPanel() {
return (
<section className="dash-card cb-card lg:col-span-2">
<section id="chain-registry" className="dash-card cb-card lg:col-span-2">
<div className="flex flex-wrap items-center justify-between gap-3 mb-4">
@@ -211,4 +211,3 @@ export default function ChainMatrixPanel() {
);
}
@@ -57,9 +57,9 @@ export default function Office24Dashboard() {
<NasaPageHeader
icon="central-bank"
title={profile?.name ?? 'Office 24 Central Bank — HOSPITALLERS Zardasht'}
title={profile?.name ?? 'Office 24 Central Bank — zBank'}
subtitle={profile?.externalId ?? 'HOSPITALLERS-ZARDASHT-IRAQ-IRAN · IPSAS Office 24'}
subtitle={profile?.externalId ?? 'zBank · IPSAS Office 24'}
badges={
@@ -85,6 +85,18 @@ export default function Office24Dashboard() {
</Link>
<Link to="/office-24#chain-registry" className="nasa-btn inline-flex items-center gap-1">
<NasaIcon name="network" size={12} /> Chain registry
</Link>
<Link to="/publications" className="nasa-btn inline-flex items-center gap-1">
<NasaIcon name="ledger" size={12} /> Public content
</Link>
<Link to="/trade" className="nasa-btn nasa-btn--primary inline-flex items-center gap-1">
<NasaIcon name="trade" size={12} /> DBIS Trade
@@ -314,4 +326,3 @@ export default function Office24Dashboard() {
);
}
@@ -1,74 +1,39 @@
import type { ReactNode } from 'react';
import type { ReactNode } from 'react'
import type { NasaIconName } from '../../components/icons/NasaIcon';
import type { NasaIconName } from '../../components/icons/NasaIcon'
import NasaIcon from '../../components/icons/NasaIcon';
export default function DashboardSection({
title,
subtitle,
action,
children,
className = '',
icon,
}: {
title: string;
subtitle?: string;
action?: ReactNode;
children: ReactNode;
className?: string;
icon?: NasaIconName;
}) {
return (
<section className={`dash-card ${className}`}>
<div className="flex flex-col items-center text-center gap-3 mb-4">
<div>
<h2 className="dash-section-title flex items-center justify-center gap-2">
{icon && <NasaIcon name={icon} size={14} className="nasa-icon--telemetry" />}
{title}
</h2>
{subtitle && <p className="dash-section-subtitle">{subtitle}</p>}
</div>
{action}
</div>
<div className="dash-section-body text-left">
{children}
</div>
</section>
);
import NasaIcon from '../../components/icons/NasaIcon'
type Props = {
title: string
subtitle?: string
action?: ReactNode
children: ReactNode
className?: string
icon?: NasaIconName
}
export default function DashboardSection({
title,
subtitle,
action,
children,
className = '',
icon,
}: Props) {
return (
<section className={`dash-card ${className}`}>
<div className="flex flex-col items-center text-center gap-3 mb-4">
<div>
<h2 className="dash-section-title flex items-center justify-center gap-2">
{icon && <NasaIcon name={icon} size={14} className="nasa-icon--telemetry" />}
{title}
</h2>
{subtitle && <p className="dash-section-subtitle">{subtitle}</p>}
</div>
{action}
</div>
<div className="dash-section-body text-left">{children}</div>
</section>
)
}
@@ -4,13 +4,14 @@ import NasaIcon from '../../components/icons/NasaIcon';
type Props = {
icon: NasaIconName;
missionId?: string;
title: string;
subtitle?: string;
badges?: ReactNode;
centered?: boolean;
};
export default function NasaPageHeader({ icon, title, subtitle, badges, centered = true }: Props) {
export default function NasaPageHeader({ icon, missionId, title, subtitle, badges, centered = true }: Props) {
return (
<header className={`nasa-header mb-6${centered ? ' nasa-header--centered' : ''}`}>
<div className="nasa-header__body">
@@ -19,6 +20,7 @@ export default function NasaPageHeader({ icon, title, subtitle, badges, centered
<NasaIcon name={icon} size={22} className="nasa-icon--telemetry" />
</div>
<div>
{missionId && <p className="dash-section-subtitle uppercase tracking-[0.18em]">{missionId}</p>}
<h1 className="nasa-title">{title}</h1>
{subtitle && <p className="dash-page-subtitle !mt-1">{subtitle}</p>}
</div>
@@ -1,14 +1,16 @@
import { Link } from 'react-router-dom';
import NasaIcon from '../../components/icons/NasaIcon';
/** Same-origin portal surfaces ÔÇö no FQDNs (see /docs). */
const LINKS = [
{ label: 'Central Bank', to: '/central-bank', icon: 'central-bank' as const },
{ label: 'Office 24', to: '/office-24', icon: 'office' as const },
{ label: 'DBIS Trade', to: '/trade', icon: 'trade' as const },
{ label: 'Digital swap', to: '/swap', icon: 'swap' as const },
{ label: 'API paths', to: '/docs', icon: 'link' as const },
] as const;
/** Same-origin portal surfaces - no FQDNs (see /docs). */
const LINKS = [
{ label: 'Central Bank', to: '/central-bank', icon: 'central-bank' as const },
{ label: 'Office 24', to: '/office-24', icon: 'office' as const },
{ label: 'Chain registry', to: '/office-24#chain-registry', icon: 'network' as const },
{ label: 'Public content', to: '/publications', icon: 'ledger' as const },
{ label: 'DBIS Trade', to: '/trade', icon: 'trade' as const },
{ label: 'Digital swap', to: '/swap', icon: 'swap' as const },
{ label: 'API paths', to: '/docs', icon: 'link' as const },
] as const;
export default function QuickPortalLinks() {
return (
@@ -1,8 +1,7 @@
import { useEffect, useState } from 'react';
import { useAccount } from 'wagmi';
import { useZBankSettlement, type SettlementPhase, type ZBankSettlementApi } from './useZBankSettlement';
import type { SettlementPhase, ZBankSettlementApi } from './useZBankSettlement';
import { isZEcosystem } from '../../config/ecosystemMode';
import { formatUsd } from '../../utils/formatMoney';
const PHASE_LABEL: Record<SettlementPhase, string> = {
RECEIVED: 'Received',
@@ -86,11 +85,12 @@ export default function SettlementProtocolsPanel({
const [tokenSymbol, setTokenSymbol] = useState('cUSDC');
const [tokenAddress, setTokenAddress] = useState('0x71D6687F38b93CCad569Fa6352c876eea967201b');
const [lineId, setLineId] = useState('USD-M2');
const refreshSettlements = s.refreshSettlements;
useEffect(() => {
if (address) setRecipient(address);
s.refreshSettlements();
}, [address]);
refreshSettlements();
}, [address, refreshSettlements]);
useEffect(() => {
const first = PROTOCOLS[group][0]?.id;
@@ -43,11 +43,12 @@ export default function ZBankFullSettlementPanel({ settlement: s, onSuccess, onM
const [iban, setIban] = useState(DEFAULT_IBAN);
const [recipient, setRecipient] = useState('');
const [includeM3, setIncludeM3] = useState(true);
const refreshSettlements = s.refreshSettlements;
useEffect(() => {
if (address) setRecipient(address);
s.refreshSettlements();
}, [address]);
refreshSettlements();
}, [address, refreshSettlements]);
async function submit() {
try {
@@ -56,7 +57,7 @@ export default function ZBankFullSettlementPanel({ settlement: s, onSuccess, onM
creditorIban: iban,
recipientAddress: includeM3 ? recipient : undefined,
});
await s.refreshSettlements();
await refreshSettlements();
onSuccess?.();
if (includeM3 && recipient.startsWith('0x')) {
onMintComplete?.(recipient);
@@ -0,0 +1,128 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import OwnerGate from './OwnerGate';
import { OWNER_SESSION_KEY } from './ownerAuth';
function jwtWithExp(exp: number): string {
const header = btoa(JSON.stringify({ alg: 'none', typ: 'JWT' }));
const payload = btoa(JSON.stringify({ exp }));
return `${header}.${payload}.sig`;
}
describe('OwnerGate', () => {
beforeEach(() => {
sessionStorage.clear();
vi.restoreAllMocks();
});
it('hides bank children until owner credentials succeed', async () => {
const user = userEvent.setup();
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' },
}),
}),
);
render(
<OwnerGate>
<p>SECRET_BANK_BALANCES</p>
</OwnerGate>,
);
expect(screen.queryByText('SECRET_BANK_BALANCES')).not.toBeInTheDocument();
expect(screen.getByRole('heading', { name: 'Z Online Bank' })).toBeInTheDocument();
await user.type(screen.getByLabelText('Username'), 'pc.walker');
await user.type(screen.getByLabelText('Password'), 'correct-horse');
await user.click(screen.getByRole('button', { name: 'Unlock bank' }));
await waitFor(() => {
expect(screen.getByText('SECRET_BANK_BALANCES')).toBeInTheDocument();
});
expect(sessionStorage.getItem(OWNER_SESSION_KEY)).toBeTruthy();
});
it('keeps the bank hidden when credentials fail', async () => {
const user = userEvent.setup();
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({
ok: false,
status: 401,
json: async () => ({ error: 'Invalid credentials' }),
}),
);
render(
<OwnerGate>
<p>SECRET_BANK_BALANCES</p>
</OwnerGate>,
);
await user.type(screen.getByLabelText('Username'), 'stranger');
await user.type(screen.getByLabelText('Password'), 'nope');
await user.click(screen.getByRole('button', { name: 'Unlock bank' }));
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent('Invalid credentials');
});
expect(screen.queryByText('SECRET_BANK_BALANCES')).not.toBeInTheDocument();
});
it('calls logout before returning to the locked screen', async () => {
const user = userEvent.setup();
const exp = Math.floor(Date.now() / 1000) + 3600;
const fetchMock = vi.fn()
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
token: jwtWithExp(exp),
user: { username: 'pc.walker', role: 'super_admin' },
}),
})
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ success: true }),
});
vi.stubGlobal('fetch', fetchMock);
render(
<OwnerGate>
<p>SECRET_BANK_BALANCES</p>
</OwnerGate>,
);
await user.type(screen.getByLabelText('Username'), 'pc.walker');
await user.type(screen.getByLabelText('Password'), 'correct-horse');
await user.click(screen.getByRole('button', { name: 'Unlock bank' }));
await waitFor(() => {
expect(screen.getByText('SECRET_BANK_BALANCES')).toBeInTheDocument();
});
await user.click(screen.getByRole('button', { name: /Sign out pc\.walker/i }));
await waitFor(() => {
expect(screen.getByRole('heading', { name: 'Z Online Bank' })).toBeInTheDocument();
});
expect(fetchMock).toHaveBeenLastCalledWith(
'/api/v1/admin/auth/logout',
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
Authorization: expect.stringContaining('Bearer '),
}),
}),
);
});
});
@@ -0,0 +1,195 @@
import { FormEvent, useEffect, useMemo, useState } from 'react';
import NasaIcon from '../../components/icons/NasaIcon';
import {
clearOwnerSession,
fetchWeb3Challenge,
loginOwner,
loginOwnerWithWeb3,
OwnerAuthError,
readOwnerSession,
logoutOwner,
type OwnerSession,
} from './ownerAuth';
import { ethers } from 'ethers';
type Props = {
children: React.ReactNode;
};
export default function OwnerGate({ children }: Props) {
const [session, setSession] = useState<OwnerSession | null>(() => readOwnerSession());
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState<string | null>(null);
const [submitting, setSubmitting] = useState(false);
const [web3Submitting, setWeb3Submitting] = useState(false);
const [walletAddress, setWalletAddress] = useState('');
const locked = !session;
useEffect(() => {
if (!session) return undefined;
const remainingMs = Math.max(0, (session.exp * 1000) - Date.now());
const id = window.setTimeout(() => {
clearOwnerSession();
setSession(null);
}, remainingMs);
return () => window.clearTimeout(id);
}, [session]);
const onSubmit = async (event: FormEvent) => {
event.preventDefault();
setError(null);
setSubmitting(true);
try {
const next = await loginOwner(username, password);
setPassword('');
setSession(next);
} catch (err) {
const message = err instanceof OwnerAuthError ? err.message : 'Invalid credentials';
setError(message);
setSession(null);
} finally {
setSubmitting(false);
}
};
const onWeb3Submit = async (event: FormEvent) => {
event.preventDefault();
setError(null);
setWeb3Submitting(true);
try {
const challenge = await fetchWeb3Challenge(walletAddress);
const provider = (window as unknown as { ethereum?: unknown }).ethereum;
if (!provider) {
throw new OwnerAuthError('Wallet provider not found', 400);
}
const web3Provider = new ethers.providers.Web3Provider(provider as ethers.providers.ExternalProvider);
const signer = web3Provider.getSigner();
const signature = await signer.signMessage(challenge.message || '');
const next = await loginOwnerWithWeb3(walletAddress, signature);
setPassword('');
setSession(next);
} catch (err) {
const message = err instanceof OwnerAuthError ? err.message : 'Web3 login failed';
setError(message);
setSession(null);
} finally {
setWeb3Submitting(false);
}
};
const onLogout = async () => {
const token = session?.token;
await logoutOwner(token);
setSession(null);
setPassword('');
};
const signedInAs = useMemo(() => session?.username ?? '', [session]);
const sessionMinutesLeft = useMemo(() => {
if (!session) return 0;
return Math.max(0, Math.ceil((session.exp * 1000 - Date.now()) / 60000));
}, [session]);
if (locked) {
return (
<div className="owner-gate min-h-[70vh] flex items-center justify-center px-4 py-10">
<div className="w-full max-w-md rounded-2xl border border-emerald-500/25 bg-[#121a14] p-6 shadow-2xl">
<div className="flex items-center gap-3 mb-5">
<span className="w-10 h-10 rounded-xl bg-emerald-600 text-white flex items-center justify-center">
<NasaIcon name="central-bank" size={18} />
</span>
<div>
<p className="text-[11px] uppercase tracking-[0.18em] text-emerald-400/80">Restricted</p>
<h1 className="text-lg font-semibold text-white">Z Online Bank</h1>
</div>
</div>
<p className="text-sm text-white/70 mb-6">
Owner credentials or an approved wallet are required. This console is not public.
</p>
<form onSubmit={onWeb3Submit} className="mb-5 rounded-xl border border-white/10 bg-black/20 p-4" autoComplete="off">
<label className="block text-xs font-medium text-white/60 mb-1" htmlFor="wallet-address">
Web3 wallet address
</label>
<input
id="wallet-address"
name="walletAddress"
type="text"
autoComplete="off"
value={walletAddress}
onChange={(e) => 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..."
/>
<button
type="submit"
disabled={web3Submitting}
className="w-full rounded-lg border border-emerald-500/30 bg-emerald-500/10 hover:bg-emerald-500/20 disabled:opacity-60 text-emerald-200 font-medium py-2.5"
>
{web3Submitting ? 'Waiting for signature…' : 'Login with Web3'}
</button>
</form>
<form onSubmit={onSubmit} autoComplete="on">
<label className="block text-xs font-medium text-white/60 mb-1" htmlFor="owner-username">
Username
</label>
<input
id="owner-username"
name="username"
type="text"
autoComplete="username"
value={username}
onChange={(e) => 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
/>
<label className="block text-xs font-medium text-white/60 mb-1" htmlFor="owner-password">
Password
</label>
<input
id="owner-password"
name="password"
type="password"
autoComplete="current-password"
value={password}
onChange={(e) => 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 ? (
<p className="mb-4 text-sm text-red-400" role="alert">
{error}
</p>
) : null}
<button
type="submit"
disabled={submitting}
className="w-full rounded-lg bg-emerald-600 hover:bg-emerald-500 disabled:opacity-60 text-white font-medium py-2.5"
>
{submitting ? 'Verifying…' : 'Unlock bank'}
</button>
</form>
</div>
</div>
);
}
return (
<div className="owner-gate-session">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-end px-4 pt-3">
<div className="rounded-full border border-emerald-500/20 bg-emerald-500/10 px-3 py-1 text-[11px] text-emerald-200">
Session active · {sessionMinutesLeft} min remaining
</div>
<button
type="button"
onClick={onLogout}
className="text-xs text-white/50 hover:text-white underline-offset-2 hover:underline"
>
Sign out {signedInAs}
</button>
</div>
{children}
</div>
);
}
@@ -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');
});
});
@@ -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<OwnerSession> {
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<OwnerSession> {
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<void> {
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<Web3Challenge> {
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;
}
@@ -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<typeof tone, string> = {
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 <span className={`inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] font-mono uppercase tracking-wide ${classes[tone]}`}>{children}</span>;
}
function ExternalOrInternalLink({ href, external, children }: { href: string; external?: boolean; children: ReactNode }) {
if (external) {
return (
<a href={href} target="_blank" rel="noopener noreferrer" className="text-[#00d4ff] hover:underline">
{children}
</a>
);
}
return (
<Link to={href} className="text-[#00d4ff] hover:underline">
{children}
</Link>
);
}
export default function PublicInstitutionContent({ office, moneySupply, tokenCount, loading }: Props) {
return (
<div className="grid gap-6">
<DashboardSection
title="Mandate and role"
subtitle="Public institutional framing, operating responsibility, and the public entry points for DBIS."
icon="ledger"
>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
<div className="rounded-lg border border-white/10 bg-white/5 p-4">
<p className="text-xs font-mono text-[#7a9bb8] mb-1">Public portal</p>
<p className="text-sm text-[#e8f4ff]">DBIS publishes the institutional portal, live dashboard, and public infrastructure inventory.</p>
</div>
<div className="rounded-lg border border-white/10 bg-white/5 p-4">
<p className="text-xs font-mono text-[#7a9bb8] mb-1">Settlement role</p>
<p className="text-sm text-[#e8f4ff]">
Office {office?.officeId ?? 24} remains the operational settlement view for balances, payments, and corridor status.
</p>
</div>
<div className="rounded-lg border border-white/10 bg-white/5 p-4">
<p className="text-xs font-mono text-[#7a9bb8] mb-1">Public services</p>
<p className="text-sm text-[#e8f4ff]">Status, developer reference, and transparency surfaces are exposed as same-origin routes or public FQDNs.</p>
</div>
</div>
<p className="text-xs text-[#7a9bb8] mt-4 leading-relaxed">
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.
</p>
</DashboardSection>
<DashboardSection
title="Policy and decisions"
subtitle="Published operating rules, trust artifacts, and settlement access notes."
icon="shield"
>
<div className="overflow-x-auto">
<table className="w-full text-sm nasa-table">
<thead>
<tr className="text-left">
<th className="py-2 px-2">Topic</th>
<th className="py-2 px-2">Source</th>
<th className="py-2 px-2">Status</th>
<th className="py-2 px-2">Provenance</th>
<th className="py-2 px-2">Note</th>
</tr>
</thead>
<tbody>
{POLICY_ROWS.map((row) => (
<tr key={row.topic}>
<td className="py-2 px-2 text-[#e8f4ff]">{row.topic}</td>
<td className="py-2 px-2">
<code className="text-xs text-[#4ade80]">{row.source}</code>
</td>
<td className="py-2 px-2">
<Badge tone={row.provenance === 'Live' ? 'live' : row.provenance === 'Reference' ? 'reference' : 'published'}>{row.status}</Badge>
</td>
<td className="py-2 px-2">
<Badge tone={row.provenance === 'Live' ? 'live' : row.provenance === 'Reference' ? 'reference' : 'published'}>{row.provenance}</Badge>
</td>
<td className="py-2 px-2 text-xs text-[#7a9bb8]">{row.note}</td>
</tr>
))}
</tbody>
</table>
</div>
</DashboardSection>
<section id="statistics" className="scroll-mt-20">
<DashboardSection
title="Statistics hub"
subtitle="Live, modelled, and ledger-linked series with explicit provenance."
icon="chart"
action={<Badge tone={loading ? 'modelled' : 'live'}>{loading ? 'refreshing' : 'live'}</Badge>}
>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<DashboardStatCard
label="M0 reserve"
value={formatUsd(moneySupply?.M0?.gl1050)}
hint={`Live public balance · ${moneySupply?.M0?.backingRatio ?? 1.2}× backing`}
accent="blue"
icon="money"
/>
<DashboardStatCard
label="M1 circulating"
value={formatUsd(moneySupply?.M1?.circulating)}
hint="Live narrow-money series · GL 2100"
accent="green"
icon="money"
/>
<DashboardStatCard
label="M2 broad money"
value={formatUsd(moneySupply?.M2?.broadMoney)}
hint="Modelled load source · GL 2200"
accent="gold"
icon="rocket"
/>
<DashboardStatCard
label="M3 token liabilities"
value={formatUsd(moneySupply?.M3?.tokenLiabilities)}
hint="Policy series · GL 2300"
accent="gold"
icon="ledger"
/>
<DashboardStatCard
label="M4 near money"
value={formatUsd(moneySupply?.M4?.nearMoney)}
hint="Live suspense and in-flight settlement"
accent="blue"
icon="rail"
/>
<DashboardStatCard
label="Registered M2 tokens"
value={String(tokenCount)}
hint="Reference registry count"
accent="green"
icon="network"
/>
</div>
<p className="text-xs text-[#7a9bb8] mt-4 leading-relaxed">
Series above are pulled from the settlement middleware where available and otherwise modelled from the live registry
and office ledger configuration.
</p>
</DashboardSection>
</section>
<section id="publications" className="scroll-mt-20">
<DashboardSection
title="Publications index"
subtitle="Public pages and canonical repo notes that explain the site surface."
icon="ledger"
>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{PUBLICATION_LINKS.map((item) => (
<div key={item.label} className="rounded-lg border border-white/10 bg-white/5 p-4">
<div className="flex items-start justify-between gap-3 mb-3">
<div>
<p className="text-sm font-medium text-[#e8f4ff]">{item.label}</p>
<p className="text-xs text-[#7a9bb8] mt-1">{item.description}</p>
</div>
<Badge tone={item.provenance === 'Live' ? 'live' : item.provenance === 'Reference' ? 'reference' : 'published'}>{item.provenance}</Badge>
</div>
<ExternalOrInternalLink href={item.href} external={item.external}>
Open
</ExternalOrInternalLink>
</div>
))}
</div>
</DashboardSection>
</section>
<section id="governance" className="scroll-mt-20">
<DashboardSection
title="Governance and disclosures"
subtitle="Public operating rules, audience boundaries, and review posture."
icon="shield"
>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div className="rounded-lg border border-white/10 bg-white/5 p-4">
<h3 className="text-sm font-semibold text-[#e8f4ff] mb-3">Governance summary</h3>
<dl className="space-y-3">
{GOVERNANCE_ROWS.map((row) => (
<div key={row.label} className="grid gap-1">
<dt className="text-xs uppercase tracking-[0.18em] text-[#7a9bb8]">{row.label}</dt>
<dd className="text-sm text-[#e8f4ff]">{row.value}</dd>
<dd className="text-xs text-[#7a9bb8]">{row.note}</dd>
</div>
))}
</dl>
</div>
<div className="rounded-lg border border-white/10 bg-white/5 p-4">
<h3 className="text-sm font-semibold text-[#e8f4ff] mb-3">Disclosure posture</h3>
<ul className="space-y-2 text-sm text-[#a0b3c8]">
<li>Public pages are intentionally concise and direct; operator-only routes stay out of the public menu where possible.</li>
<li>Published metrics distinguish live values from modeled or reference values so external readers can judge confidence correctly.</li>
<li>Institutional claims should carry provenance, revision history, and a clear owner before they are treated as official.</li>
<li>Authentication state should be obvious, and logout must revoke access both locally and at the API boundary.</li>
</ul>
</div>
</div>
</DashboardSection>
</section>
<section id="directory" className="scroll-mt-20">
<DashboardSection
title="Public service directory"
subtitle="Audience-specific entry points, with an explicit contact-style surface for each audience."
icon="globe"
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{SERVICE_LINKS.map((item) => (
<div key={item.label} className="rounded-lg border border-white/10 bg-white/5 p-4">
<div className="flex items-start justify-between gap-3 mb-2">
<div>
<p className="text-sm font-medium text-[#e8f4ff]">{item.label}</p>
<p className="text-xs text-[#7a9bb8] mt-1">{item.description}</p>
</div>
<Badge tone={item.provenance === 'Live' ? 'live' : item.provenance === 'Reference' ? 'reference' : 'published'}>{item.provenance}</Badge>
</div>
<ExternalOrInternalLink href={item.href} external={item.external}>
{item.href}
</ExternalOrInternalLink>
</div>
))}
</div>
</DashboardSection>
</section>
<section id="contact" className="scroll-mt-20">
<DashboardSection
title="Editorial contacts and control"
subtitle="Who owns the content, who controls the session boundary, and how changes are governed."
icon="admin"
>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div className="rounded-lg border border-white/10 bg-white/5 p-4">
<h3 className="text-sm font-semibold text-[#e8f4ff] mb-3">Ownership and control</h3>
<dl className="space-y-3">
{CONTACT_ROWS.map((row) => (
<div key={row.label} className="grid gap-1">
<dt className="text-xs uppercase tracking-[0.18em] text-[#7a9bb8]">{row.label}</dt>
<dd className="text-sm text-[#e8f4ff]">{row.value}</dd>
<dd className="text-xs text-[#7a9bb8]">{row.note}</dd>
</div>
))}
</dl>
</div>
<div className="rounded-lg border border-white/10 bg-white/5 p-4">
<h3 className="text-sm font-semibold text-[#e8f4ff] mb-3">Recommended review checklist</h3>
<ul className="space-y-2 text-sm text-[#a0b3c8]">
<li>Check that every public page has a purpose, an owner, and a known source of truth.</li>
<li>Confirm that login-gated pages make the access tier obvious before credentials are entered.</li>
<li>Keep public navigation limited to routes that are meant to be discoverable by external readers.</li>
<li>Use publication pages for policy, disclosures, and explanation rather than operator controls.</li>
</ul>
</div>
</div>
</DashboardSection>
</section>
<section id="legal" className="scroll-mt-20">
<DashboardSection
title="Legal basis and publication scope"
subtitle="Plain-language disclosure of publication intent, jurisdiction, and review scope."
icon="shield"
>
<div className="rounded-lg border border-white/10 bg-white/5 p-4">
<dl className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{LEGAL_ROWS.map((row) => (
<div key={row.label} className="grid gap-1">
<dt className="text-xs uppercase tracking-[0.18em] text-[#7a9bb8]">{row.label}</dt>
<dd className="text-sm text-[#e8f4ff]">{row.value}</dd>
<dd className="text-xs text-[#7a9bb8]">{row.note}</dd>
</div>
))}
</dl>
</div>
</DashboardSection>
</section>
<section id="provenance" className="scroll-mt-20">
<DashboardSection
title="Publication standard"
subtitle="Each public item should carry a date, owner, source of truth, and provenance label."
icon="satellite"
>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div className="rounded-lg border border-white/10 bg-white/5 p-4">
<h3 className="text-sm font-semibold text-[#e8f4ff] mb-2">Provenance legend</h3>
<ul className="space-y-2 text-sm text-[#a0b3c8]">
<li className="flex items-center gap-2"><Badge tone="live">Live</Badge> Fetched from a current endpoint or live service.</li>
<li className="flex items-center gap-2"><Badge tone="modelled">Modelled</Badge> Derived from live data, registry state, or computed series.</li>
<li className="flex items-center gap-2"><Badge tone="reference">Reference</Badge> Canonical repo or trust artifact.</li>
<li className="flex items-center gap-2"><Badge tone="published">Published</Badge> Public page intended for broad institutional readership.</li>
</ul>
</div>
<div className="rounded-lg border border-white/10 bg-white/5 p-4">
<h3 className="text-sm font-semibold text-[#e8f4ff] mb-2">Recommended publication metadata</h3>
<pre className="overflow-x-auto text-xs text-[#7a9bb8] leading-6 whitespace-pre-wrap">
{`Title
Date
Audience
Owner
Source of truth
Provenance
Revision status`}
</pre>
</div>
</div>
</DashboardSection>
</section>
</div>
);
}
@@ -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
@@ -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<Side>('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'));
@@ -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
+33 -13
View File
@@ -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;
}
+4 -4
View File
@@ -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'}`);
}
};
+6 -1
View File
@@ -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 <OnlineBankApp />;
return (
<OwnerGate>
<OnlineBankApp />
</OwnerGate>
);
}
+30
View File
@@ -31,6 +31,36 @@ export default function DocsPage() {
localhost, or 127.0.0.1. Institutional share URLs live in operator manifests.
</p>
<section className="mb-10 rounded-lg border border-[#2a2f3a] bg-[#1a1d24] p-5">
<h2 className="text-lg font-semibold text-white mb-3">Public content</h2>
<ul className="text-sm text-[#A0A0A0] space-y-2 list-disc pl-5">
<li>
<Link to="/publications" className="text-teal-400 hover:underline">
Public content and publications
</Link>{' '}
- mandate, policy, statistics, publications, and directory.
</li>
<li>
<Link to="/central-bank" className="text-teal-400 hover:underline">
Central Bank
</Link>{' '}
- live money supply and treasury view.
</li>
<li>
<Link to="/office-24" className="text-teal-400 hover:underline">
Office 24
</Link>{' '}
- corridor registry and settlement surface.
</li>
<li>
<Link to="/publications#governance" className="text-teal-400 hover:underline">
Governance and disclosures
</Link>{' '}
- public controls, audience boundaries, and review posture.
</li>
</ul>
</section>
<section className="mb-10 rounded-lg border border-[#2a2f3a] bg-[#1a1d24] p-5">
<h2 className="text-lg font-semibold text-white mb-3">Policy</h2>
<ul className="text-sm text-[#A0A0A0] space-y-2 list-disc pl-5">
+1
View File
@@ -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' },
];
@@ -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 (
<div className="dash-page cb-page max-w-[1400px] mx-auto px-4 py-6">
<NasaPageHeader
icon="ledger"
missionId="PUB-138"
title="Public content and publications"
subtitle="Mandate, policy, statistics, publications, and service directory for the DBIS public surface."
badges={
<>
<span className="nasa-badge nasa-badge--telemetry">Published</span>
<span className="nasa-badge nasa-badge--nominal">Public</span>
<span className="nasa-badge nasa-badge--caution">Live metrics</span>
</>
}
/>
<PublicInstitutionContent
office={cb.office as { officeId?: number; name?: string; externalId?: string } | null}
moneySupply={cb.moneySupply}
tokenCount={cb.tokenCount}
loading={cb.loading}
/>
</div>
);
}
+24 -12
View File
@@ -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 && (
<div className="w-full space-y-2 text-sm">
<p className="text-[#A0A0A0]">
Address: <span className="font-mono text-white">{account.address}</span>
Address: <span className="font-mono text-white">{accountAddress}</span>
</p>
{balanceLoading ? (
<p className="text-[#A0A0A0]">Loading balance…</p>
@@ -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 && (
<div className="w-full space-y-2 text-sm">
{shivaBalanceLoading ? (
<p className="text-[#A0A0A0]">Loading Shiva balance…</p>
+15 -5
View File
@@ -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 && (
<div className="w-full space-y-2 text-sm text-center">
<p className="text-[#A0A0A0]">
Address: <span className="font-mono text-white">{account.address}</span>
Address: <span className="font-mono text-white">{accountAddress}</span>
</p>
{balanceLoading ? (
<p className="text-[#A0A0A0]">Loading balance…</p>
@@ -110,9 +120,9 @@ function ZWalletContent() {
)}
</div>
<InternationalTransferPanel recipientAddress={account?.address} />
<InternationalTransferPanel recipientAddress={accountAddress} />
<ZBotPanel walletAddress={account?.address} chainId={zChainId} />
<ZBotPanel walletAddress={accountAddress} chainId={zChainId} />
</div>
);
}
+1 -1
View File
@@ -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', {
+4 -4
View File
@@ -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 {
+4 -4
View File
@@ -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 {
+102 -33
View File
@@ -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);
}
+3 -3
View File
@@ -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 {
+4 -4
View File
@@ -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 {
+5 -5
View File
@@ -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<string, unknown> | unknown;
status: 'success' | 'failure';
}
+7 -7
View File
@@ -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<PublicClient, 'getBlockNumber' | 'getLogs'>,
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<PublicClient, 'getBlockNumber' | 'getLogs'>,
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<PublicClient, 'getBlockNumber' | 'getLogs'>,
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,
})
+21 -8
View File
@@ -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<string, unknown>) => void,
pollInterval = 10000
): Promise<() => void> {
let isMonitoring = true
let lastState: any = null
let lastState: Record<string, unknown> | null = null
const checkState = async () => {
if (!isMonitoring) return
+7 -3
View File
@@ -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<PublicClient, 'getBytecode'> // viem PublicClient
): Promise<boolean> {
try {
const code = await publicClient.getBytecode({ address: address as `0x${string}` });
+20 -23
View File
@@ -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<PublicClient, 'call'>,
call: SimulatedCall
): Promise<SimulationResult> {
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<PublicClient, 'call' | 'estimateGas'>,
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<PublicClient, 'call'>,
calls: SimulatedCall[]
): Promise<SimulationResult[]> {
const results: SimulationResult[] = []
@@ -163,7 +161,6 @@ export function estimateGasCost(
gasPrice: bigint
): string {
const cost = gasEstimate * gasPrice
const { formatEther } = require('viem')
return formatEther(cost)
}
+3 -9
View File
@@ -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 {}
+74 -70
View File
@@ -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,
},
}
})
@@ -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