e.stopPropagation()}>
Connect Wallet
-
@@ -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"
>
{connector.name}
{isPending && (
diff --git a/frontend-dapp/src/config/dex.ts b/frontend-dapp/src/config/dex.ts
index 3bb3369..f809dbc 100644
--- a/frontend-dapp/src/config/dex.ts
+++ b/frontend-dapp/src/config/dex.ts
@@ -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',
);
diff --git a/frontend-dapp/src/config/ecosystemLinks.ts b/frontend-dapp/src/config/ecosystemLinks.ts
index 1db5b3f..453f9cb 100644
--- a/frontend-dapp/src/config/ecosystemLinks.ts
+++ b/frontend-dapp/src/config/ecosystemLinks.ts
@@ -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' },
diff --git a/frontend-dapp/src/config/omnlInternalPaths.ts b/frontend-dapp/src/config/omnlInternalPaths.ts
index 617b663..acaddfe 100644
--- a/frontend-dapp/src/config/omnlInternalPaths.ts
+++ b/frontend-dapp/src/config/omnlInternalPaths.ts
@@ -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;
diff --git a/frontend-dapp/src/config/wagmi.ts b/frontend-dapp/src/config/wagmi.ts
index e8db9f2..e6b89e1 100644
--- a/frontend-dapp/src/config/wagmi.ts
+++ b/frontend-dapp/src/config/wagmi.ts
@@ -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' }),
],
diff --git a/frontend-dapp/src/contexts/AdminContext.tsx b/frontend-dapp/src/contexts/AdminContext.tsx
index d1cd253..6884301 100644
--- a/frontend-dapp/src/contexts/AdminContext.tsx
+++ b/frontend-dapp/src/contexts/AdminContext.tsx
@@ -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
(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) {
diff --git a/frontend-dapp/src/features/central-bank/CentralBankDashboard.tsx b/frontend-dapp/src/features/central-bank/CentralBankDashboard.tsx
index 2629546..cc82334 100644
--- a/frontend-dapp/src/features/central-bank/CentralBankDashboard.tsx
+++ b/frontend-dapp/src/features/central-bank/CentralBankDashboard.tsx
@@ -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() {
+
+
+ Open publications
+
+ }
+ >
+
+ {[
+ ['Mandate', '/publications#mandate'],
+ ['Policy', '/publications#policy'],
+ ['Statistics', '/publications#statistics'],
+ ['Publications', '/publications#publications'],
+ ['Directory', '/publications#directory'],
+ ].map(([label, to]) => (
+
+
+ {label}
+
+ ))}
+
+
+
{([
['overview', 'Overview'],
@@ -211,7 +239,7 @@ export default function CentralBankDashboard() {
Office
- {office?.officeId ?? 24} · {office?.externalId ?? 'HOSPITALLERS-ZARDASHT-IRAQ-IRAN'}
+ {office?.officeId ?? 24} · {office?.externalId ?? 'zBank'}
diff --git a/frontend-dapp/src/features/central-bank/ChainMatrixPanel.tsx b/frontend-dapp/src/features/central-bank/ChainMatrixPanel.tsx
index 15db1cf..b9935be 100644
--- a/frontend-dapp/src/features/central-bank/ChainMatrixPanel.tsx
+++ b/frontend-dapp/src/features/central-bank/ChainMatrixPanel.tsx
@@ -52,7 +52,7 @@ export default function ChainMatrixPanel() {
return (
-
+
@@ -211,4 +211,3 @@ export default function ChainMatrixPanel() {
);
}
-
diff --git a/frontend-dapp/src/features/office24/Office24Dashboard.tsx b/frontend-dapp/src/features/office24/Office24Dashboard.tsx
index a1afec0..99d27db 100644
--- a/frontend-dapp/src/features/office24/Office24Dashboard.tsx
+++ b/frontend-dapp/src/features/office24/Office24Dashboard.tsx
@@ -57,9 +57,9 @@ export default function Office24Dashboard() {
+
+
+ Chain registry
+
+
+
+
+
+ Public content
+
+
+
DBIS Trade
@@ -314,4 +326,3 @@ export default function Office24Dashboard() {
);
}
-
diff --git a/frontend-dapp/src/features/omnl-dashboard/DashboardSection.tsx b/frontend-dapp/src/features/omnl-dashboard/DashboardSection.tsx
index b52981e..53743bb 100644
--- a/frontend-dapp/src/features/omnl-dashboard/DashboardSection.tsx
+++ b/frontend-dapp/src/features/omnl-dashboard/DashboardSection.tsx
@@ -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 (
-
-
-
-
-
-
-
-
-
- {icon && }
-
- {title}
-
-
-
- {subtitle &&
{subtitle}
}
-
-
-
- {action}
-
-
-
-
-
- {children}
-
-
-
-
-
- );
+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 (
+
+
+
+
+ {icon && }
+ {title}
+
+ {subtitle &&
{subtitle}
}
+
+ {action}
+
+ {children}
+
+ )
+}
diff --git a/frontend-dapp/src/features/omnl-dashboard/NasaPageHeader.tsx b/frontend-dapp/src/features/omnl-dashboard/NasaPageHeader.tsx
index 5c18953..65ea482 100644
--- a/frontend-dapp/src/features/omnl-dashboard/NasaPageHeader.tsx
+++ b/frontend-dapp/src/features/omnl-dashboard/NasaPageHeader.tsx
@@ -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 (
@@ -19,6 +20,7 @@ export default function NasaPageHeader({ icon, title, subtitle, badges, centered
+ {missionId &&
{missionId}
}
{title}
{subtitle &&
{subtitle}
}
diff --git a/frontend-dapp/src/features/omnl-dashboard/QuickPortalLinks.tsx b/frontend-dapp/src/features/omnl-dashboard/QuickPortalLinks.tsx
index ad0ee33..90c559e 100644
--- a/frontend-dapp/src/features/omnl-dashboard/QuickPortalLinks.tsx
+++ b/frontend-dapp/src/features/omnl-dashboard/QuickPortalLinks.tsx
@@ -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 (
diff --git a/frontend-dapp/src/features/online-bank/SettlementProtocolsPanel.tsx b/frontend-dapp/src/features/online-bank/SettlementProtocolsPanel.tsx
index e7b8a65..b62f1e8 100644
--- a/frontend-dapp/src/features/online-bank/SettlementProtocolsPanel.tsx
+++ b/frontend-dapp/src/features/online-bank/SettlementProtocolsPanel.tsx
@@ -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 = {
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;
diff --git a/frontend-dapp/src/features/online-bank/ZBankFullSettlementPanel.tsx b/frontend-dapp/src/features/online-bank/ZBankFullSettlementPanel.tsx
index 1eaeb16..b876c5d 100644
--- a/frontend-dapp/src/features/online-bank/ZBankFullSettlementPanel.tsx
+++ b/frontend-dapp/src/features/online-bank/ZBankFullSettlementPanel.tsx
@@ -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);
diff --git a/frontend-dapp/src/features/owner-auth/OwnerGate.test.tsx b/frontend-dapp/src/features/owner-auth/OwnerGate.test.tsx
new file mode 100644
index 0000000..0bf041b
--- /dev/null
+++ b/frontend-dapp/src/features/owner-auth/OwnerGate.test.tsx
@@ -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(
+
+ SECRET_BANK_BALANCES
+ ,
+ );
+
+ 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(
+
+ SECRET_BANK_BALANCES
+ ,
+ );
+
+ 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(
+
+ SECRET_BANK_BALANCES
+ ,
+ );
+
+ 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 '),
+ }),
+ }),
+ );
+ });
+});
diff --git a/frontend-dapp/src/features/owner-auth/OwnerGate.tsx b/frontend-dapp/src/features/owner-auth/OwnerGate.tsx
new file mode 100644
index 0000000..a611cb8
--- /dev/null
+++ b/frontend-dapp/src/features/owner-auth/OwnerGate.tsx
@@ -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(() => readOwnerSession());
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState(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 (
+
+
+
+
+
+
+
+
Restricted
+
Z Online Bank
+
+
+
+ Owner credentials or an approved wallet are required. This console is not public.
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Session active · {sessionMinutesLeft} min remaining
+
+
+ Sign out {signedInAs}
+
+
+ {children}
+
+ );
+}
diff --git a/frontend-dapp/src/features/owner-auth/ownerAuth.test.ts b/frontend-dapp/src/features/owner-auth/ownerAuth.test.ts
new file mode 100644
index 0000000..bd82078
--- /dev/null
+++ b/frontend-dapp/src/features/owner-auth/ownerAuth.test.ts
@@ -0,0 +1,161 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import {
+ OWNER_SESSION_KEY,
+ clearOwnerSession,
+ decodeJwtExp,
+ fetchWeb3Challenge,
+ isOwnerRole,
+ isOwnerSessionValid,
+ loginOwner,
+ loginOwnerWithWeb3,
+ logoutOwner,
+ readOwnerSession,
+ writeOwnerSession,
+} from './ownerAuth';
+
+function jwtWithExp(exp: number): string {
+ const header = btoa(JSON.stringify({ alg: 'none', typ: 'JWT' }));
+ const payload = btoa(JSON.stringify({ exp, userId: 1, username: 'pc.walker', role: 'super_admin' }));
+ return `${header}.${payload}.sig`;
+}
+
+describe('ownerAuth', () => {
+ beforeEach(() => {
+ sessionStorage.clear();
+ vi.restoreAllMocks();
+ });
+
+ afterEach(() => {
+ sessionStorage.clear();
+ });
+
+ it('accepts only super_admin', () => {
+ expect(isOwnerRole('super_admin')).toBe(true);
+ expect(isOwnerRole('admin')).toBe(false);
+ expect(isOwnerRole('operator')).toBe(false);
+ });
+
+ it('decodes JWT exp and rejects expired sessions', () => {
+ const future = Math.floor(Date.now() / 1000) + 3600;
+ const past = Math.floor(Date.now() / 1000) - 10;
+ expect(decodeJwtExp(jwtWithExp(future))).toBe(future);
+ expect(
+ isOwnerSessionValid({
+ token: 'x',
+ username: 'pc.walker',
+ role: 'super_admin',
+ exp: past,
+ }),
+ ).toBe(false);
+ });
+
+ it('clears invalid stored sessions', () => {
+ writeOwnerSession({
+ token: 'x',
+ username: 'pc.walker',
+ role: 'super_admin',
+ exp: Math.floor(Date.now() / 1000) - 5,
+ });
+ expect(readOwnerSession()).toBeNull();
+ expect(sessionStorage.getItem(OWNER_SESSION_KEY)).toBeNull();
+ });
+
+ it('stores a valid owner session after login', async () => {
+ const exp = Math.floor(Date.now() / 1000) + 3600;
+ vi.stubGlobal(
+ 'fetch',
+ vi.fn().mockResolvedValue({
+ ok: true,
+ status: 200,
+ json: async () => ({
+ token: jwtWithExp(exp),
+ user: { username: 'pc.walker', role: 'super_admin' },
+ }),
+ }),
+ );
+
+ const session = await loginOwner('pc.walker', 'secret');
+ expect(session.username).toBe('pc.walker');
+ expect(readOwnerSession()?.username).toBe('pc.walker');
+ clearOwnerSession();
+ expect(readOwnerSession()).toBeNull();
+ });
+
+ it('logs out through the API when a token is present', async () => {
+ const fetchMock = vi.fn().mockResolvedValue({
+ ok: true,
+ status: 200,
+ json: async () => ({ success: true }),
+ });
+ vi.stubGlobal('fetch', fetchMock);
+
+ writeOwnerSession({
+ token: 'token-123',
+ username: 'pc.walker',
+ role: 'super_admin',
+ exp: Math.floor(Date.now() / 1000) + 3600,
+ });
+
+ await logoutOwner('token-123');
+
+ expect(fetchMock).toHaveBeenCalledWith(
+ '/api/v1/admin/auth/logout',
+ expect.objectContaining({
+ method: 'POST',
+ headers: expect.objectContaining({
+ Authorization: 'Bearer token-123',
+ }),
+ }),
+ );
+ expect(readOwnerSession()).toBeNull();
+ });
+
+ it('rejects non-owner roles even with a token', async () => {
+ vi.stubGlobal(
+ 'fetch',
+ vi.fn().mockResolvedValue({
+ ok: true,
+ status: 200,
+ json: async () => ({
+ token: jwtWithExp(Math.floor(Date.now() / 1000) + 3600),
+ user: { username: 'clerk', role: 'operator' },
+ }),
+ }),
+ );
+
+ await expect(loginOwner('clerk', 'secret')).rejects.toThrow('not authorized');
+ expect(readOwnerSession()).toBeNull();
+ });
+
+ it('fetches a web3 challenge and stores a web3 session after signature verification', async () => {
+ const exp = Math.floor(Date.now() / 1000) + 3600;
+ const fetchMock = vi.fn()
+ .mockResolvedValueOnce({
+ ok: true,
+ status: 200,
+ json: async () => ({
+ address: '0xabc123',
+ nonce: 'nonce-1',
+ message: 'DBIS central-bank Web3 login\nDomain: secure.d-bis.org\nWallet: 0xabc123\nNonce: nonce-1',
+ expiresInSeconds: 600,
+ }),
+ })
+ .mockResolvedValueOnce({
+ ok: true,
+ status: 200,
+ json: async () => ({
+ token: jwtWithExp(exp),
+ user: { username: 'wallet:0xabc123', role: 'super_admin', walletAddress: '0xabc123', authMethod: 'web3' },
+ }),
+ });
+ vi.stubGlobal('fetch', fetchMock);
+
+ const challenge = await fetchWeb3Challenge('0xabc123');
+ expect(challenge.nonce).toBe('nonce-1');
+
+ const session = await loginOwnerWithWeb3('0xabc123', 'signed-message');
+ expect(session.authMethod).toBe('web3');
+ expect(session.walletAddress).toBe('0xabc123');
+ expect(readOwnerSession()?.authMethod).toBe('web3');
+ });
+});
diff --git a/frontend-dapp/src/features/owner-auth/ownerAuth.ts b/frontend-dapp/src/features/owner-auth/ownerAuth.ts
new file mode 100644
index 0000000..db13df2
--- /dev/null
+++ b/frontend-dapp/src/features/owner-auth/ownerAuth.ts
@@ -0,0 +1,221 @@
+export const OWNER_SESSION_KEY = 'omnl_owner_session';
+export const OWNER_LOGIN_PATH = '/api/v1/admin/auth/login';
+export const OWNER_LOGOUT_PATH = '/api/v1/admin/auth/logout';
+export const OWNER_ROLES = ['super_admin'] as const;
+
+export type OwnerSession = {
+ token: string;
+ username: string;
+ role: string;
+ exp: number;
+ authMethod?: 'password' | 'web3';
+ walletAddress?: string;
+};
+
+type LoginResponse = {
+ token?: string;
+ error?: string;
+ user?: {
+ username?: string;
+ role?: string;
+ walletAddress?: string;
+ authMethod?: 'password' | 'web3';
+ };
+ expiresInSeconds?: number;
+};
+
+export class OwnerAuthError extends Error {
+ readonly status: number;
+
+ constructor(message: string, status = 401) {
+ super(message);
+ this.name = 'OwnerAuthError';
+ this.status = status;
+ }
+}
+
+export function decodeJwtExp(token: string): number | null {
+ try {
+ const payload = token.split('.')[1];
+ if (!payload) return null;
+ const normalized = payload.replace(/-/g, '+').replace(/_/g, '/');
+ const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=');
+ const json = JSON.parse(atob(padded)) as { exp?: unknown };
+ return typeof json.exp === 'number' ? json.exp : null;
+ } catch {
+ return null;
+ }
+}
+
+export function isOwnerRole(role: string | undefined): boolean {
+ return OWNER_ROLES.includes((role ?? '') as (typeof OWNER_ROLES)[number]);
+}
+
+export function isOwnerSessionValid(session: OwnerSession | null, nowSec = Math.floor(Date.now() / 1000)): boolean {
+ if (!session?.token || !session.username || !isOwnerRole(session.role)) return false;
+ return session.exp > nowSec + 30;
+}
+
+export function readOwnerSession(): OwnerSession | null {
+ try {
+ const raw = sessionStorage.getItem(OWNER_SESSION_KEY);
+ if (!raw) return null;
+ const parsed = JSON.parse(raw) as OwnerSession;
+ if (!isOwnerSessionValid(parsed)) {
+ sessionStorage.removeItem(OWNER_SESSION_KEY);
+ return null;
+ }
+ return parsed;
+ } catch {
+ sessionStorage.removeItem(OWNER_SESSION_KEY);
+ return null;
+ }
+}
+
+export function writeOwnerSession(session: OwnerSession): void {
+ sessionStorage.setItem(OWNER_SESSION_KEY, JSON.stringify(session));
+}
+
+export function clearOwnerSession(): void {
+ sessionStorage.removeItem(OWNER_SESSION_KEY);
+}
+
+export async function loginOwner(username: string, password: string): Promise {
+ const user = username.trim();
+ if (!user || !password) {
+ throw new OwnerAuthError('Username and password are required', 400);
+ }
+
+ let res: Response;
+ try {
+ res = await fetch(OWNER_LOGIN_PATH, {
+ method: 'POST',
+ credentials: 'same-origin',
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
+ body: JSON.stringify({ username: user, password }),
+ });
+ } catch {
+ throw new OwnerAuthError('Owner login service unreachable', 503);
+ }
+
+ let body: LoginResponse = {};
+ try {
+ body = (await res.json()) as LoginResponse;
+ } catch {
+ if (res.status === 401) {
+ throw new OwnerAuthError('Portal access denied');
+ }
+ throw new OwnerAuthError(res.ok ? 'Invalid login response' : 'Owner login failed', res.status);
+ }
+
+ if (!res.ok || !body.token) {
+ throw new OwnerAuthError(body.error || 'Invalid credentials', res.status || 401);
+ }
+
+ const role = body.user?.role ?? '';
+ if (!isOwnerRole(role)) {
+ throw new OwnerAuthError('This account is not authorized for the online bank', 403);
+ }
+
+ const exp = decodeJwtExp(body.token) ?? Math.floor(Date.now() / 1000) + 24 * 60 * 60;
+ const session: OwnerSession = {
+ token: body.token,
+ username: body.user?.username || user,
+ role,
+ exp,
+ authMethod: body.user?.authMethod ?? 'password',
+ };
+ writeOwnerSession(session);
+ return session;
+}
+
+type Web3Challenge = {
+ address?: string;
+ nonce?: string;
+ message?: string;
+ expiresInSeconds?: number;
+ error?: string;
+};
+
+export async function loginOwnerWithWeb3(address: string, signature: string): Promise {
+ const wallet = address.trim();
+ if (!wallet || !signature) {
+ throw new OwnerAuthError('Wallet address and signature are required', 400);
+ }
+
+ let res: Response;
+ try {
+ res = await fetch('/api/v1/admin/auth/web3/login', {
+ method: 'POST',
+ credentials: 'same-origin',
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
+ body: JSON.stringify({ address: wallet, signature }),
+ });
+ } catch {
+ throw new OwnerAuthError('Web3 login service unreachable', 503);
+ }
+
+ let body: LoginResponse = {};
+ try {
+ body = (await res.json()) as LoginResponse;
+ } catch {
+ throw new OwnerAuthError(res.ok ? 'Invalid web3 login response' : 'Web3 login failed', res.status);
+ }
+
+ if (!res.ok || !body.token) {
+ throw new OwnerAuthError(body.error || 'Web3 login failed', res.status || 401);
+ }
+
+ const role = body.user?.role ?? '';
+ if (!isOwnerRole(role)) {
+ throw new OwnerAuthError('This wallet is not authorized for the online bank', 403);
+ }
+
+ const exp = decodeJwtExp(body.token) ?? Math.floor(Date.now() / 1000) + 4 * 60 * 60;
+ const session: OwnerSession = {
+ token: body.token,
+ username: body.user?.username || wallet,
+ role,
+ exp,
+ authMethod: body.user?.authMethod ?? 'web3',
+ walletAddress: body.user?.walletAddress ?? wallet,
+ };
+ writeOwnerSession(session);
+ return session;
+}
+
+export async function logoutOwner(token?: string): Promise {
+ if (!token) {
+ clearOwnerSession();
+ return;
+ }
+
+ try {
+ await fetch(OWNER_LOGOUT_PATH, {
+ method: 'POST',
+ credentials: 'same-origin',
+ headers: {
+ Accept: 'application/json',
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ } catch {
+ // Local logout must still complete even if the API is unavailable.
+ } finally {
+ clearOwnerSession();
+ }
+}
+
+export async function fetchWeb3Challenge(address: string): Promise {
+ const wallet = address.trim();
+ if (!wallet) throw new OwnerAuthError('Wallet address is required', 400);
+ const res = await fetch(`/api/v1/admin/auth/web3/challenge?address=${encodeURIComponent(wallet)}`, {
+ credentials: 'same-origin',
+ headers: { Accept: 'application/json' },
+ });
+ const body = (await res.json().catch(() => ({}))) as Web3Challenge;
+ if (!res.ok) {
+ throw new OwnerAuthError(body.error || 'Unable to create Web3 challenge', res.status);
+ }
+ return body;
+}
diff --git a/frontend-dapp/src/features/public-content/PublicInstitutionContent.tsx b/frontend-dapp/src/features/public-content/PublicInstitutionContent.tsx
new file mode 100644
index 0000000..b3e9f06
--- /dev/null
+++ b/frontend-dapp/src/features/public-content/PublicInstitutionContent.tsx
@@ -0,0 +1,545 @@
+import { Link } from 'react-router-dom';
+
+import type { ReactNode } from 'react';
+
+import DashboardSection from '../omnl-dashboard/DashboardSection';
+
+import DashboardStatCard from '../omnl-dashboard/DashboardStatCard';
+
+import { formatUsd } from '../../utils/formatMoney';
+
+import type { MoneySupply } from '../central-bank/useCentralBank';
+
+type OfficeSummary = {
+ officeId?: number;
+ name?: string;
+ externalId?: string;
+};
+
+type Props = {
+ office: OfficeSummary | null;
+ moneySupply: MoneySupply | null;
+ tokenCount: number;
+ loading: boolean;
+};
+
+type PublicationLink = {
+ label: string;
+ description: string;
+ href: string;
+ external?: boolean;
+ provenance: 'Live' | 'Modelled' | 'Reference' | 'Published';
+};
+
+type MetadataRow = {
+ label: string;
+ value: string;
+ note: string;
+};
+
+const POLICY_ROWS = [
+ {
+ topic: 'Trust and governance',
+ source: '/.well-known/trust.json',
+ status: 'Published',
+ provenance: 'Reference' as const,
+ note: 'Trust artifact and public boundary statements.',
+ },
+ {
+ topic: 'Operating governance',
+ source: '/governance.json',
+ status: 'Published',
+ provenance: 'Reference' as const,
+ note: 'Operator and institutional guardrails for the public stack.',
+ },
+ {
+ topic: 'Settlement access',
+ source: '/settlement/chains',
+ status: 'Live',
+ provenance: 'Live' as const,
+ note: 'Chain registry and capability flags for settlement routing.',
+ },
+ {
+ topic: 'Office 24 opening policy',
+ source: '/office-24',
+ status: 'Published',
+ provenance: 'Published' as const,
+ note: 'Office-specific operating view and registry anchor.',
+ },
+ {
+ topic: 'Public data API',
+ source: 'https://data.d-bis.org',
+ status: 'Live',
+ provenance: 'Live' as const,
+ note: 'Official data and metrics surface used by the portal.',
+ },
+] satisfies Array<{
+ topic: string;
+ source: string;
+ status: string;
+ provenance: PublicationLink['provenance'];
+ note: string;
+}>;
+
+const PUBLICATION_LINKS: PublicationLink[] = [
+ {
+ label: 'Public content audit',
+ description: 'Strict peer-bank benchmark and remediation tracker.',
+ href: '/docs',
+ provenance: 'Published',
+ },
+ {
+ label: 'Chain registry paths',
+ description: 'Public registry map and API source notes.',
+ href: '/office-24#chain-registry',
+ provenance: 'Reference',
+ },
+ {
+ label: 'Infrastructure inventory',
+ description: 'Domains, systems, explorers, RPC and status monitoring.',
+ href: '/docs',
+ provenance: 'Published',
+ },
+ {
+ label: 'Live dashboard',
+ description: 'Public metrics and workspace summary.',
+ href: '/hub',
+ provenance: 'Live',
+ },
+ {
+ label: 'Public publications page',
+ description: 'Mandate, policy, statistics, and service directory.',
+ href: '/publications',
+ provenance: 'Published',
+ },
+];
+
+const SERVICE_LINKS: PublicationLink[] = [
+ {
+ label: 'Public dashboard',
+ description: 'Entry point for public metrics and institutional workspace.',
+ href: '/hub',
+ provenance: 'Live',
+ },
+ {
+ label: 'Central bank surface',
+ description: 'Operational balances, payments, and settlement views.',
+ href: '/central-bank',
+ provenance: 'Live',
+ },
+ {
+ label: 'Office 24',
+ description: 'Corridor registry, office opening, and settlement anchors.',
+ href: '/office-24',
+ provenance: 'Published',
+ },
+ {
+ label: 'Status monitoring',
+ description: 'System health and service availability.',
+ href: 'https://status.d-bis.org/',
+ external: true,
+ provenance: 'Live',
+ },
+ {
+ label: 'Developer reference',
+ description: 'Same-origin route map and integration notes.',
+ href: '/docs',
+ provenance: 'Reference',
+ },
+ {
+ label: 'Public infrastructure',
+ description: 'Domain inventory and technical transparency.',
+ href: '/docs',
+ provenance: 'Published',
+ },
+];
+
+const GOVERNANCE_ROWS: MetadataRow[] = [
+ {
+ label: 'Institutional purpose',
+ value: 'Public operating portal',
+ note: 'Exposes public information, governed service routes, and operator surfaces with explicit boundaries.',
+ },
+ {
+ label: 'Public source of truth',
+ value: 'This portal plus operator manifests',
+ note: 'Public pages are curated from the live portal; operator manifests define the canonical operational record.',
+ },
+ {
+ label: 'Auth boundaries',
+ value: 'Public / authenticated / owner / operator',
+ note: 'Access tiers are separated so users can tell what is open, logged-in, restricted, or operator-only.',
+ },
+ {
+ label: 'Data status labels',
+ value: 'Live / Modelled / Reference / Published',
+ note: 'Public claims carry explicit provenance to reduce ambiguity for auditors and external reviewers.',
+ },
+];
+
+const CONTACT_ROWS: MetadataRow[] = [
+ {
+ label: 'Editorial owner',
+ value: 'DBIS portal operators',
+ note: 'Responsible for page structure, publication metadata, and route integrity.',
+ },
+ {
+ label: 'Operational owner',
+ value: 'Authorized super-admin / owner session',
+ note: 'Owner-only surfaces are gated and logout revocation is explicit on the API side.',
+ },
+ {
+ label: 'Public contact',
+ value: 'Public support and institutional inboxes',
+ note: 'Use the published domain/contact channels for public requests instead of operator-only paths.',
+ },
+ {
+ label: 'Change control',
+ value: 'Versioned commits and deploys',
+ note: 'Major changes should remain reviewable as separate commit slices with validation evidence.',
+ },
+];
+
+const LEGAL_ROWS: MetadataRow[] = [
+ {
+ label: 'Publication intent',
+ value: 'Informational institutional disclosure',
+ note: 'The page describes the public operating model and does not substitute for legal advice or formal notices.',
+ },
+ {
+ label: 'Jurisdiction',
+ value: 'Host-jurisdiction publication context',
+ note: 'External publication obligations should be confirmed by legal counsel in the relevant deployment jurisdiction.',
+ },
+ {
+ label: 'Scope',
+ value: 'Public portals and controlled access surfaces',
+ note: 'Operator-only tooling, secrets, and privileged actions remain outside the public disclosure set.',
+ },
+ {
+ label: 'Revision practice',
+ value: 'Versioned and reviewable',
+ note: 'Material public claims should be traceable to a commit, a publication page, or a signed operational artifact.',
+ },
+];
+
+function Badge({ children, tone }: { children: ReactNode; tone: 'live' | 'published' | 'reference' | 'modelled' }) {
+ const classes: Record = {
+ live: 'border-[#3dff8a]/30 text-[#3dff8a] bg-[#3dff8a]/10',
+ published: 'border-[#00d4ff]/25 text-[#00d4ff] bg-[#00d4ff]/10',
+ reference: 'border-[#ffb347]/25 text-[#ffb347] bg-[#ffb347]/10',
+ modelled: 'border-[#7a9bb8]/25 text-[#7a9bb8] bg-[#7a9bb8]/10',
+ };
+
+ return {children};
+}
+
+function ExternalOrInternalLink({ href, external, children }: { href: string; external?: boolean; children: ReactNode }) {
+ if (external) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ return (
+
+ {children}
+
+ );
+}
+
+export default function PublicInstitutionContent({ office, moneySupply, tokenCount, loading }: Props) {
+ return (
+
+
+
+
+
Public portal
+
DBIS publishes the institutional portal, live dashboard, and public infrastructure inventory.
+
+
+
Settlement role
+
+ Office {office?.officeId ?? 24} remains the operational settlement view for balances, payments, and corridor status.
+
+
+
+
Public services
+
Status, developer reference, and transparency surfaces are exposed as same-origin routes or public FQDNs.
+
+
+
+ DBIS is not claiming a sovereign monetary-policy mandate here. The public site documents the operating model, trust
+ boundaries, and publication rules that govern the portal and settlement surfaces.
+
+
+
+
+
+
+
+
+ | Topic |
+ Source |
+ Status |
+ Provenance |
+ Note |
+
+
+
+ {POLICY_ROWS.map((row) => (
+
+ | {row.topic} |
+
+ {row.source}
+ |
+
+ {row.status}
+ |
+
+ {row.provenance}
+ |
+ {row.note} |
+
+ ))}
+
+
+
+
+
+
+ {loading ? 'refreshing' : 'live'}}
+ >
+
+
+
+
+
+
+
+
+
+ Series above are pulled from the settlement middleware where available and otherwise modelled from the live registry
+ and office ledger configuration.
+
+
+
+
+
+
+
+ {PUBLICATION_LINKS.map((item) => (
+
+
+
+
{item.label}
+
{item.description}
+
+
{item.provenance}
+
+
+ Open
+
+
+ ))}
+
+
+
+
+
+
+
+
+
Governance summary
+
+ {GOVERNANCE_ROWS.map((row) => (
+
+
- {row.label}
+ - {row.value}
+ - {row.note}
+
+ ))}
+
+
+
+
Disclosure posture
+
+ - Public pages are intentionally concise and direct; operator-only routes stay out of the public menu where possible.
+ - Published metrics distinguish live values from modeled or reference values so external readers can judge confidence correctly.
+ - Institutional claims should carry provenance, revision history, and a clear owner before they are treated as official.
+ - Authentication state should be obvious, and logout must revoke access both locally and at the API boundary.
+
+
+
+
+
+
+
+
+
+ {SERVICE_LINKS.map((item) => (
+
+
+
+
{item.label}
+
{item.description}
+
+
{item.provenance}
+
+
+ {item.href}
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ {LEGAL_ROWS.map((row) => (
+
+
- {row.label}
+ - {row.value}
+ - {row.note}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
Provenance legend
+
+ - Live Fetched from a current endpoint or live service.
+ - Modelled Derived from live data, registry state, or computed series.
+ - Reference Canonical repo or trust artifact.
+ - Published Public page intended for broad institutional readership.
+
+
+
+
Recommended publication metadata
+
+{`Title
+Date
+Audience
+Owner
+Source of truth
+Provenance
+Revision status`}
+
+
+
+
+
+
+ );
+}
diff --git a/frontend-dapp/src/features/swap/DBISSwapPanel.tsx b/frontend-dapp/src/features/swap/DBISSwapPanel.tsx
index 5cbdee9..a54e21f 100644
--- a/frontend-dapp/src/features/swap/DBISSwapPanel.tsx
+++ b/frontend-dapp/src/features/swap/DBISSwapPanel.tsx
@@ -58,14 +58,15 @@ function SidePanel({
export default function DBISSwapPanel() {
const swap = useDbisSwap();
+ const { loadMonitor, loadLedgers, isConnected, address } = swap;
useEffect(() => {
- swap.loadMonitor();
- }, [swap.loadMonitor]);
+ loadMonitor();
+ }, [loadMonitor]);
useEffect(() => {
- if (swap.isConnected) swap.loadLedgers();
- }, [swap.isConnected, swap.address, swap.loadLedgers]);
+ if (isConnected) loadLedgers();
+ }, [isConnected, address, loadLedgers]);
const outDisplay =
swap.plan?.amountOut && swap.tokenOut
diff --git a/frontend-dapp/src/features/trade/BinanceTradePanel.tsx b/frontend-dapp/src/features/trade/BinanceTradePanel.tsx
index 432cb0d..ceceeec 100644
--- a/frontend-dapp/src/features/trade/BinanceTradePanel.tsx
+++ b/frontend-dapp/src/features/trade/BinanceTradePanel.tsx
@@ -69,17 +69,18 @@ function OrderBookMock({ price }: { price: string }) {
export default function BinanceTradePanel() {
const swap = useDbisSwap();
+ const { loadMonitor, loadLedgers, isConnected, address } = swap;
const [side, setSide] = useState('buy');
const [marketSearch, setMarketSearch] = useState('');
const [orderType, setOrderType] = useState<'market' | 'limit'>('market');
useEffect(() => {
- swap.loadMonitor();
- }, [swap.loadMonitor]);
+ loadMonitor();
+ }, [loadMonitor]);
useEffect(() => {
- if (swap.isConnected) swap.loadLedgers();
- }, [swap.isConnected, swap.address, swap.loadLedgers]);
+ if (isConnected) loadLedgers();
+ }, [isConnected, address, loadLedgers]);
const quoteSymbol = 'cUSDT';
const quoteToken = swap.tokens.find((t) => t.symbol === quoteSymbol) ?? swap.tokens.find((t) => t.symbol.includes('USDT'));
diff --git a/frontend-dapp/src/helpers/admin/safeHelpers.ts b/frontend-dapp/src/helpers/admin/safeHelpers.ts
index 7711d3d..94b862f 100644
--- a/frontend-dapp/src/helpers/admin/safeHelpers.ts
+++ b/frontend-dapp/src/helpers/admin/safeHelpers.ts
@@ -2,7 +2,7 @@
* Safe SDK Helper Utilities - Utilities for Safe wallet operations
*/
-import type { Address } from 'viem'
+import type { Abi, Address } from 'viem'
export interface SafeConfig {
owners: Address[]
@@ -30,7 +30,7 @@ export function encodeSafeTransaction(
_contractAddress: Address,
_functionName: string,
_args: unknown[],
- _abi: any[]
+ _abi: Abi
): `0x${string}` {
// This would use viem's encodeFunctionData in production
// For now, return placeholder
diff --git a/frontend-dapp/src/index.css b/frontend-dapp/src/index.css
index 8df5e1b..98dc4f7 100644
--- a/frontend-dapp/src/index.css
+++ b/frontend-dapp/src/index.css
@@ -4,6 +4,22 @@
@tailwind components;
@tailwind utilities;
+:root {
+ color-scheme: dark;
+ --bg: #0d1210;
+ --bg-elevated: #151c19;
+ --bg-surface: #1a2320;
+ --bg-surface-2: #202925;
+ --border: rgba(255, 255, 255, 0.12);
+ --border-strong: rgba(255, 255, 255, 0.22);
+ --text: #f2f6f4;
+ --text-muted: #b9c3be;
+ --text-soft: #8f9b95;
+ --accent: #4ade80;
+ --accent-strong: #22c55e;
+ --accent-warm: #f0b90b;
+}
+
body {
margin: 0;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
@@ -11,7 +27,11 @@ body {
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
- background: #141a16;
+ background:
+ radial-gradient(circle at top left, rgba(74, 222, 128, 0.1), transparent 32%),
+ radial-gradient(circle at top right, rgba(15, 118, 110, 0.12), transparent 28%),
+ var(--bg);
+ color: var(--text);
min-height: 100vh;
}
@@ -84,30 +104,30 @@ body {
}
::-webkit-scrollbar-track {
- background: #f1f1f1;
+ background: #111714;
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
- background: linear-gradient(to bottom, #22c55e, #16a34a);
+ background: linear-gradient(to bottom, #3ddc84, #15803d);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
- background: linear-gradient(to bottom, #16a34a, #15803d);
+ background: linear-gradient(to bottom, #22c55e, #166534);
}
/* Glass morphism effect */
.glass {
- background: rgba(255, 255, 255, 0.7);
+ background: rgba(14, 20, 18, 0.82);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
- border: 1px solid rgba(255, 255, 255, 0.18);
+ border: 1px solid var(--border);
}
/* Gradient text */
.gradient-text {
- background: linear-gradient(135deg, #22c55e 0%, #059669 100%);
+ background: linear-gradient(135deg, #4ade80 0%, #0ea5a4 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
@@ -115,13 +135,13 @@ body {
/* Button hover effects */
.btn-gradient {
- background: linear-gradient(135deg, #22c55e 0%, #059669 100%);
+ background: linear-gradient(135deg, #22c55e 0%, #0f766e 100%);
transition: all 0.3s ease;
}
.btn-gradient:hover {
transform: translateY(-2px);
- box-shadow: 0 10px 25px rgba(34, 197, 94, 0.4);
+ box-shadow: 0 12px 30px rgba(34, 197, 94, 0.28);
}
/* Card hover effect */
@@ -131,7 +151,7 @@ body {
.card-hover:hover {
transform: translateY(-5px);
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
}
/* Additional animations */
@@ -193,7 +213,7 @@ body {
/* Focus styles for accessibility */
*:focus-visible {
- outline: 2px solid #22c55e;
+ outline: 2px solid #4ade80;
outline-offset: 2px;
border-radius: 4px;
}
@@ -223,8 +243,8 @@ textarea:focus {
/* Loading spinner */
.spinner {
- border: 3px solid rgba(59, 130, 246, 0.1);
- border-top-color: #22c55e;
+ border: 3px solid rgba(255, 255, 255, 0.1);
+ border-top-color: #4ade80;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
diff --git a/frontend-dapp/src/pages/AdminConsole.tsx b/frontend-dapp/src/pages/AdminConsole.tsx
index 7725119..0463164 100644
--- a/frontend-dapp/src/pages/AdminConsole.tsx
+++ b/frontend-dapp/src/pages/AdminConsole.tsx
@@ -69,8 +69,8 @@ export default function AdminConsole() {
} else {
throw new Error('Pause failed');
}
- } catch (error: any) {
- alert(`Error: ${error.message}`);
+ } catch (error: unknown) {
+ alert(`Error: ${error instanceof Error ? error.message : 'Pause failed'}`);
}
};
@@ -90,8 +90,8 @@ export default function AdminConsole() {
} else {
throw new Error('Refund failed');
}
- } catch (error: any) {
- alert(`Error: ${error.message}`);
+ } catch (error: unknown) {
+ alert(`Error: ${error instanceof Error ? error.message : 'Refund failed'}`);
}
};
diff --git a/frontend-dapp/src/pages/CentralBankPage.tsx b/frontend-dapp/src/pages/CentralBankPage.tsx
index 22eb0a9..ab5e05c 100644
--- a/frontend-dapp/src/pages/CentralBankPage.tsx
+++ b/frontend-dapp/src/pages/CentralBankPage.tsx
@@ -1,6 +1,7 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import OnlineBankApp from '../features/online-bank/OnlineBankApp';
+import OwnerGate from '../features/owner-auth/OwnerGate';
export default function CentralBankPage() {
const { hash } = useLocation();
@@ -12,5 +13,9 @@ export default function CentralBankPage() {
el?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, [hash]);
- return ;
+ return (
+
+
+
+ );
}
diff --git a/frontend-dapp/src/pages/DocsPage.tsx b/frontend-dapp/src/pages/DocsPage.tsx
index 6ea95ad..ff226c4 100644
--- a/frontend-dapp/src/pages/DocsPage.tsx
+++ b/frontend-dapp/src/pages/DocsPage.tsx
@@ -31,6 +31,36 @@ export default function DocsPage() {
localhost, or 127.0.0.1. Institutional share URLs live in operator manifests.
+
+ Public content
+
+ -
+
+ Public content and publications
+ {' '}
+ - mandate, policy, statistics, publications, and directory.
+
+ -
+
+ Central Bank
+ {' '}
+ - live money supply and treasury view.
+
+ -
+
+ Office 24
+ {' '}
+ - corridor registry and settlement surface.
+
+ -
+
+ Governance and disclosures
+ {' '}
+ - public controls, audience boundaries, and review posture.
+
+
+
+
Policy
diff --git a/frontend-dapp/src/pages/HubPage.tsx b/frontend-dapp/src/pages/HubPage.tsx
index 7fa9102..db3a77b 100644
--- a/frontend-dapp/src/pages/HubPage.tsx
+++ b/frontend-dapp/src/pages/HubPage.tsx
@@ -18,6 +18,7 @@ const CHAIN138: { path: string; label: string; icon: NasaIconName }[] = [
{ path: '/history', label: 'History', icon: 'chart' },
{ path: '/wallets', label: 'Wallets', icon: 'globe' },
{ path: '/docs', label: 'Developers / API docs', icon: 'link' },
+ { path: '/publications', label: 'Public content', icon: 'ledger' },
{ path: '/admin', label: 'Admin', icon: 'satellite' },
];
diff --git a/frontend-dapp/src/pages/PublicationsPage.tsx b/frontend-dapp/src/pages/PublicationsPage.tsx
new file mode 100644
index 0000000..6c6715b
--- /dev/null
+++ b/frontend-dapp/src/pages/PublicationsPage.tsx
@@ -0,0 +1,44 @@
+import { useEffect } from 'react';
+import { useLocation } from 'react-router-dom';
+
+import NasaPageHeader from '../features/omnl-dashboard/NasaPageHeader';
+
+import { useCentralBank } from '../features/central-bank/useCentralBank';
+
+import PublicInstitutionContent from '../features/public-content/PublicInstitutionContent';
+
+export default function PublicationsPage() {
+ const { hash } = useLocation();
+ const cb = useCentralBank();
+
+ useEffect(() => {
+ if (!hash) return;
+ const id = hash.replace(/^#/, '');
+ document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ }, [hash]);
+
+ return (
+
+
+ Published
+ Public
+ Live metrics
+ >
+ }
+ />
+
+
+
+ );
+}
diff --git a/frontend-dapp/src/pages/WalletsDemoPage.tsx b/frontend-dapp/src/pages/WalletsDemoPage.tsx
index cb07f75..e3b5e77 100644
--- a/frontend-dapp/src/pages/WalletsDemoPage.tsx
+++ b/frontend-dapp/src/pages/WalletsDemoPage.tsx
@@ -56,17 +56,25 @@ const wallets = [
}),
]
+const fallbackWalletAvatarUrl = '/favicon.ico'
+const evmAddressPattern = /^0x[a-fA-F0-9]{40}$/
+
+function getValidEvmAddress(address: string | undefined) {
+ return address && evmAddressPattern.test(address) ? address : undefined
+}
+
function WalletsDemoContent() {
const account = useActiveAccount()
+ const accountAddress = getValidEvmAddress(account?.address)
const { data: balance, isLoading: balanceLoading } = useWalletBalance({
client,
chain: defaultThirdwebChain,
- address: account?.address,
+ address: accountAddress,
})
const { data: shivaBalance, isLoading: shivaBalanceLoading } = useWalletBalance({
client,
chain: shivaThirdwebChain,
- address: account?.address,
+ address: accountAddress,
})
return (
@@ -95,11 +103,13 @@ function WalletsDemoContent() {
padding: '0.75rem 1.5rem',
},
}}
+ detailsButton={{ connectedAccountAvatarUrl: fallbackWalletAvatarUrl }}
+ detailsModal={{ connectedAccountAvatarUrl: fallbackWalletAvatarUrl }}
/>
- {account && (
+ {accountAddress && (
- Address: {account.address}
+ Address: {accountAddress}
{balanceLoading ? (
Loading balance…
@@ -123,17 +133,19 @@ function WalletsDemoContent() {
chain={shivaThirdwebChain}
wallets={wallets}
theme="dark"
- connectButton={{
- label: `Connect ${onexShiva.brand.wallet}`,
- style: {
+ connectButton={{
+ label: `Connect ${onexShiva.brand.wallet}`,
+ style: {
backgroundColor: '#7c3aed',
color: 'white',
borderRadius: '0.75rem',
- padding: '0.75rem 1.5rem',
- },
- }}
- />
- {account && (
+ padding: '0.75rem 1.5rem',
+ },
+ }}
+ detailsButton={{ connectedAccountAvatarUrl: fallbackWalletAvatarUrl }}
+ detailsModal={{ connectedAccountAvatarUrl: fallbackWalletAvatarUrl }}
+ />
+ {accountAddress && (
{shivaBalanceLoading ? (
Loading Shiva balance…
diff --git a/frontend-dapp/src/pages/ZWalletPage.tsx b/frontend-dapp/src/pages/ZWalletPage.tsx
index 135aee7..a3fbdb9 100644
--- a/frontend-dapp/src/pages/ZWalletPage.tsx
+++ b/frontend-dapp/src/pages/ZWalletPage.tsx
@@ -40,12 +40,20 @@ const wallets = [
}),
];
+const fallbackWalletAvatarUrl = '/favicon.ico';
+const evmAddressPattern = /^0x[a-fA-F0-9]{40}$/;
+
+function getValidEvmAddress(address: string | undefined) {
+ return address && evmAddressPattern.test(address) ? address : undefined;
+}
+
function ZWalletContent() {
const account = useActiveAccount();
+ const accountAddress = getValidEvmAddress(account?.address);
const { data: balance, isLoading: balanceLoading } = useWalletBalance({
client,
chain: zThirdwebChain,
- address: account?.address,
+ address: accountAddress,
});
if (!zWalletEnabled) {
@@ -87,11 +95,13 @@ function ZWalletContent() {
padding: '0.75rem 1.5rem',
},
}}
+ detailsButton={{ connectedAccountAvatarUrl: fallbackWalletAvatarUrl }}
+ detailsModal={{ connectedAccountAvatarUrl: fallbackWalletAvatarUrl }}
/>
- {account && (
+ {accountAddress && (
- Address: {account.address}
+ Address: {accountAddress}
{balanceLoading ? (
Loading balance…
@@ -110,9 +120,9 @@ function ZWalletContent() {
)}
-
+
-
+
);
}
diff --git a/frontend-dapp/src/setupTests.ts b/frontend-dapp/src/setupTests.ts
index 8ec7afc..8179670 100644
--- a/frontend-dapp/src/setupTests.ts
+++ b/frontend-dapp/src/setupTests.ts
@@ -24,7 +24,7 @@ const localStorageMock = {
removeItem: vi.fn(),
clear: vi.fn(),
}
-global.localStorage = localStorageMock as any
+global.localStorage = localStorageMock as unknown as Storage
// Mock matchMedia
Object.defineProperty(window, 'matchMedia', {
diff --git a/frontend-dapp/src/styles/app-dashboard.css b/frontend-dapp/src/styles/app-dashboard.css
index 24842f5..5fc72d1 100644
--- a/frontend-dapp/src/styles/app-dashboard.css
+++ b/frontend-dapp/src/styles/app-dashboard.css
@@ -379,8 +379,8 @@ html:has(.omnl-app) body {
.omnl-app-header {
background: #1a241c !important;
- border-color: rgba(34, 197, 94, 0.2) !important;
- box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
+ border-color: rgba(74, 222, 128, 0.32) !important;
+ box-shadow: 0 1px 0 rgba(0, 0, 0, 0.28);
}
.nasa-nav-brand {
@@ -401,7 +401,7 @@ html:has(.omnl-app) body {
font-weight: 500;
letter-spacing: normal;
text-transform: none;
- color: var(--nasa-muted);
+ color: #c7d0c9;
border-radius: 8px;
border: 1px solid transparent;
transition: color 0.15s, background 0.15s;
@@ -409,7 +409,7 @@ html:has(.omnl-app) body {
.nasa-nav-link:hover {
color: var(--nasa-text);
- background: rgba(255, 255, 255, 0.05);
+ background: rgba(255, 255, 255, 0.08);
}
.nasa-nav-link--active {
diff --git a/frontend-dapp/src/styles/dbis-dashboard.css b/frontend-dapp/src/styles/dbis-dashboard.css
index 644a414..abeaf5c 100644
--- a/frontend-dapp/src/styles/dbis-dashboard.css
+++ b/frontend-dapp/src/styles/dbis-dashboard.css
@@ -321,7 +321,7 @@
min-height: 3.5rem;
padding-bottom: env(safe-area-inset-bottom, 0);
background: rgba(26, 36, 28, 0.96);
- border-top: 1px solid rgba(34, 197, 94, 0.2);
+ border-top: 1px solid rgba(74, 222, 128, 0.3);
backdrop-filter: blur(12px);
}
@@ -342,7 +342,7 @@
min-height: 3.5rem;
font-size: 0.6rem;
font-weight: 500;
- color: #848e9c;
+ color: #c7d0c9;
text-decoration: none;
border: none;
background: transparent;
@@ -351,11 +351,11 @@
}
.dbis-bottom-nav__item--active {
- color: #4ade80;
+ color: #bbf7d0;
}
.dbis-bottom-nav__item:active {
- background: rgba(255, 255, 255, 0.05);
+ background: rgba(255, 255, 255, 0.08);
}
.dbis-has-bottom-nav {
diff --git a/frontend-dapp/src/styles/omnl-apps.css b/frontend-dapp/src/styles/omnl-apps.css
index 7a1e223..cc1f42d 100644
--- a/frontend-dapp/src/styles/omnl-apps.css
+++ b/frontend-dapp/src/styles/omnl-apps.css
@@ -9,22 +9,23 @@
.cb-card,
.o24-card,
.dash-card {
- background: #1e2329;
- border: 1px solid #2b3139;
- border-radius: 8px;
+ background: linear-gradient(180deg, rgba(26, 35, 32, 0.96), rgba(17, 23, 20, 0.96));
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ border-radius: 14px;
padding: 1.25rem;
+ box-shadow: 0 12px 28px rgba(0, 0, 0, 0.24);
}
.dash-page-title {
font-size: 1.875rem;
font-weight: 700;
- color: #eaecef;
+ color: #f4f7f5;
line-height: 1.2;
}
.dash-page-subtitle {
margin-top: 0.35rem;
- color: #848e9c;
+ color: #b8c2bc;
font-size: 0.95rem;
max-width: 42rem;
}
@@ -55,21 +56,21 @@
}
.dash-banner {
- border-radius: 8px;
+ border-radius: 14px;
padding: 1rem 1.15rem;
- border: 1px solid #2b3139;
+ border: 1px solid rgba(255, 255, 255, 0.12);
}
.dash-banner__title {
font-size: 0.95rem;
font-weight: 600;
- color: #eaecef;
+ color: #f4f7f5;
margin-bottom: 0.25rem;
}
.dash-banner__detail {
font-size: 0.85rem;
- color: #848e9c;
+ color: #b8c2bc;
line-height: 1.45;
}
@@ -101,12 +102,12 @@
}
.dash-banner--loading {
- background: #1e2329;
+ background: rgba(26, 35, 32, 0.7);
}
.dash-stat-label {
font-size: 0.75rem;
- color: #848e9c;
+ color: #b8c2bc;
font-weight: 500;
margin-bottom: 0.35rem;
}
@@ -114,26 +115,26 @@
.dash-stat-value {
font-size: 1.65rem;
font-weight: 700;
- color: #eaecef;
+ color: #f4f7f5;
font-variant-numeric: tabular-nums;
}
.dash-stat-hint {
margin-top: 0.35rem;
font-size: 0.75rem;
- color: #848e9c;
+ color: #a4afaa;
}
.dash-section-title {
font-size: 1.05rem;
font-weight: 600;
- color: #eaecef;
+ color: #f4f7f5;
}
.dash-section-subtitle {
margin-top: 0.2rem;
font-size: 0.8rem;
- color: #848e9c;
+ color: #b8c2bc;
}
.dash-kv {
@@ -142,7 +143,7 @@
gap: 1rem;
padding: 0.45rem 0;
font-size: 0.875rem;
- border-bottom: 1px solid #2b3139;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.dash-kv:last-child {
@@ -150,12 +151,12 @@
}
.dash-kv dt {
- color: #848e9c;
+ color: #b8c2bc;
flex-shrink: 0;
}
.dash-kv dd {
- color: #eaecef;
+ color: #f4f7f5;
text-align: right;
word-break: break-word;
}
@@ -168,7 +169,7 @@
.dash-quick-links__label {
font-size: 0.75rem;
font-weight: 500;
- color: #848e9c;
+ color: #b8c2bc;
margin-bottom: 0.5rem;
}
@@ -178,16 +179,16 @@
border-radius: 999px;
font-size: 0.75rem;
font-weight: 500;
- color: #eaecef;
- background: #2b3139;
- border: 1px solid #474d57;
+ color: #f4f7f5;
+ background: rgba(43, 49, 57, 0.95);
+ border: 1px solid rgba(255, 255, 255, 0.14);
transition: background 0.15s, border-color 0.15s;
}
.dash-quick-links__chip:hover {
- background: #474d57;
- border-color: #22c55e;
- color: #4ade80;
+ background: rgba(58, 68, 62, 0.95);
+ border-color: #4ade80;
+ color: #d8ffe8;
}
.dash-btn {
@@ -196,13 +197,13 @@
border-radius: 6px;
font-size: 0.875rem;
font-weight: 500;
- background: #2b3139;
- color: #eaecef;
+ background: rgba(43, 49, 57, 0.95);
+ color: #f4f7f5;
transition: background 0.15s;
}
.dash-btn:hover {
- background: #474d57;
+ background: rgba(58, 68, 62, 0.95);
}
.dash-btn:disabled {
@@ -216,7 +217,7 @@
grid-template-columns: 240px 1fr;
grid-template-rows: 1fr;
min-height: calc(100vh - 56px);
- background: #0b0e11;
+ background: linear-gradient(180deg, #090c0e 0%, #111614 100%);
}
@media (max-width: 1023px) {
@@ -237,12 +238,12 @@
}
.trade-market-row:hover {
- background: #2b3139;
+ background: #202925;
}
.trade-market-row--active {
- background: #2b3139;
- border-left: 2px solid #22c55e;
+ background: #202925;
+ border-left: 2px solid #4ade80;
}
.trade-orderbook {
@@ -251,5 +252,73 @@
}
.omnl-app-header {
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28);
+}
+
+.omnl-app {
+ background:
+ radial-gradient(circle at top left, rgba(74, 222, 128, 0.08), transparent 24%),
+ radial-gradient(circle at top right, rgba(245, 158, 11, 0.07), transparent 28%),
+ #0d1210;
+}
+
+.nasa-header {
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ background: linear-gradient(180deg, rgba(26, 35, 32, 0.92), rgba(17, 23, 20, 0.92));
+ border-radius: 18px;
+ padding: 1.25rem 1.25rem 1rem;
+ box-shadow: 0 12px 30px rgba(0, 0, 0, 0.24);
+}
+
+.nasa-header__title-row {
+ display: flex;
+ align-items: center;
+ gap: 0.9rem;
+}
+
+.nasa-header__icon-wrap {
+ width: 2.75rem;
+ height: 2.75rem;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 0.9rem;
+ background: rgba(74, 222, 128, 0.14);
+ border: 1px solid rgba(74, 222, 128, 0.22);
+}
+
+.nasa-title {
+ margin: 0;
+ color: #f4f7f5;
+ font-size: 1.65rem;
+ line-height: 1.15;
+ letter-spacing: -0.02em;
+}
+
+.nasa-header--centered .nasa-header__body {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 1rem;
+}
+
+.nasa-header__body {
+ display: flex;
+ flex-direction: column;
+ gap: 0.9rem;
+}
+
+.dbis-bottom-nav {
+ background: rgba(13, 18, 16, 0.96);
+ border-top: 1px solid rgba(255, 255, 255, 0.12);
+ backdrop-filter: blur(14px);
+}
+
+.dbis-bottom-nav__item {
+ color: #b8c2bc;
+}
+
+.dbis-bottom-nav__item--active {
+ color: #f4f7f5;
+ background: rgba(74, 222, 128, 0.14);
}
diff --git a/frontend-dapp/src/styles/online-bank.css b/frontend-dapp/src/styles/online-bank.css
index 5f2f42d..08ab86d 100644
--- a/frontend-dapp/src/styles/online-bank.css
+++ b/frontend-dapp/src/styles/online-bank.css
@@ -31,7 +31,7 @@
.omnl-banking-theme .omnl-app-header {
background: #ffffff !important;
- border-bottom: 1px solid var(--rb-border) !important;
+ border-bottom: 1px solid var(--rb-border-strong) !important;
box-shadow: none !important;
}
@@ -42,7 +42,7 @@
}
.omnl-banking-theme .omnl-app-header nav a {
- color: var(--rb-muted) !important;
+ color: var(--rb-text-body) !important;
}
.omnl-banking-theme .omnl-app-header nav a.bg-green-600 {
@@ -59,7 +59,7 @@
}
.omnl-banking-theme .omnl-app-header .hover\:bg-white\/10:hover {
- background: var(--rb-bg) !important;
+ background: var(--rb-accent-soft) !important;
}
.revolut-bank__inner {
diff --git a/frontend-dapp/src/styles/z-ecosystem.css b/frontend-dapp/src/styles/z-ecosystem.css
index 48c7fc0..f3567ab 100644
--- a/frontend-dapp/src/styles/z-ecosystem.css
+++ b/frontend-dapp/src/styles/z-ecosystem.css
@@ -14,7 +14,7 @@
min-height: 3.5rem;
padding-bottom: env(safe-area-inset-bottom, 0);
background: rgba(26, 22, 36, 0.96);
- border-top: 1px solid rgba(147, 51, 234, 0.35);
+ border-top: 1px solid rgba(196, 181, 253, 0.42);
backdrop-filter: blur(12px);
}
@@ -35,7 +35,7 @@
min-height: 3.5rem;
font-size: 0.6rem;
font-weight: 500;
- color: #9ca3af;
+ color: #d1d5db;
text-decoration: none;
border: none;
background: transparent;
@@ -44,11 +44,11 @@
}
.z-ecosystem-theme .z-bottom-nav__item--active {
- color: #c084fc;
+ color: #e9d5ff;
}
.z-ecosystem-theme .z-bottom-nav__item:active {
- background: rgba(255, 255, 255, 0.05);
+ background: rgba(255, 255, 255, 0.08);
}
.z-ecosystem-theme.omnl-banking-theme .omnl-app-header nav a.bg-purple-600 {
diff --git a/frontend-dapp/src/types/admin.ts b/frontend-dapp/src/types/admin.ts
index 515b922..404c2eb 100644
--- a/frontend-dapp/src/types/admin.ts
+++ b/frontend-dapp/src/types/admin.ts
@@ -61,7 +61,7 @@ export interface TransactionRequest {
error?: string;
contractAddress?: string; // For admin actions
functionName?: string; // For admin actions
- args?: any[]; // For admin actions
+ args?: unknown[]; // For admin actions
}
export interface MultiSigApproval {
@@ -93,7 +93,7 @@ export interface GasEstimate {
export interface WalletBalance {
native: string;
nativeFormatted: string;
- tokens: any[];
+ tokens: TokenBalance[];
totalUsdValue?: string;
}
@@ -110,10 +110,10 @@ export interface TokenBalance {
export interface AdminAction {
id: string;
- type: 'pause' | 'unpause' | 'setAdmin' | 'anchorStateProof' | 'mirrorTransaction' | 'custom';
+ type: string;
contractAddress: string;
functionName: string;
- args: any[];
+ args: unknown[];
status: TransactionRequestStatus;
createdAt: number;
executedAt?: number;
@@ -129,7 +129,7 @@ export interface AuditLog {
resourceType: string;
resourceId: string;
ipAddress?: string;
- details?: any;
+ details?: Record
| unknown;
status: 'success' | 'failure';
}
diff --git a/frontend-dapp/src/utils/contractEvents.ts b/frontend-dapp/src/utils/contractEvents.ts
index f2ed5c3..4fa08d6 100644
--- a/frontend-dapp/src/utils/contractEvents.ts
+++ b/frontend-dapp/src/utils/contractEvents.ts
@@ -3,12 +3,12 @@
*/
import type { Address } from 'viem'
-import type { Abi } from 'viem'
+import type { Abi, AbiEvent, PublicClient } from 'viem'
export interface ContractEvent {
address: Address
eventName: string
- args: any
+ args: unknown
blockNumber: bigint
transactionHash: `0x${string}`
logIndex: number
@@ -21,7 +21,7 @@ export type EventCallback = (event: ContractEvent) => void
* Subscribe to contract events
*/
export async function subscribeToContractEvents(
- publicClient: any,
+ publicClient: Pick,
contractAddress: Address,
abi: Abi,
eventName: string,
@@ -43,7 +43,7 @@ export async function subscribeToContractEvents(
event: {
type: 'event',
name: eventName,
- } as any,
+ } as AbiEvent,
fromBlock: lastBlock,
toBlock: currentBlock,
})
@@ -91,7 +91,7 @@ export async function subscribeToContractEvents(
* Listen to specific event once
*/
export async function waitForEvent(
- publicClient: any,
+ publicClient: Pick,
contractAddress: Address,
abi: Abi,
eventName: string,
@@ -118,7 +118,7 @@ export async function waitForEvent(
* Get recent events for a contract
*/
export async function getRecentEvents(
- publicClient: any,
+ publicClient: Pick,
contractAddress: Address,
abi: Abi,
eventName: string,
@@ -133,7 +133,7 @@ export async function getRecentEvents(
event: {
type: 'event',
name: eventName,
- } as any,
+ } as AbiEvent,
fromBlock,
toBlock: currentBlock,
})
diff --git a/frontend-dapp/src/utils/realtimeMonitor.ts b/frontend-dapp/src/utils/realtimeMonitor.ts
index e856890..61faa00 100644
--- a/frontend-dapp/src/utils/realtimeMonitor.ts
+++ b/frontend-dapp/src/utils/realtimeMonitor.ts
@@ -7,7 +7,13 @@ import type { Address } from 'viem'
export interface MonitorEvent {
type: 'block' | 'transaction' | 'contract_event' | 'state_change'
timestamp: number
- data: any
+ data: unknown
+}
+
+interface RealtimeMessage {
+ type?: string
+ key?: string
+ event?: MonitorEvent
}
export type EventCallback = (event: MonitorEvent) => void
@@ -153,11 +159,18 @@ class RealtimeMonitor {
/**
* Handle incoming WebSocket message
*/
- private handleMessage(data: any): void {
- const { type, key, event } = data
+ private handleMessage(data: unknown): void {
+ if (typeof data !== 'object' || data === null) {
+ return
+ }
+
+ const message = data as RealtimeMessage
+ const { type, key, event } = message
if (type === 'event' && key) {
- this.notifyListeners(key, event)
+ if (event) {
+ this.notifyListeners(key, event)
+ }
}
}
@@ -180,7 +193,7 @@ class RealtimeMonitor {
/**
* Send message to WebSocket server
*/
- send(message: any): void {
+ send(message: unknown): void {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(message))
} else {
@@ -215,12 +228,12 @@ export function getRealtimeMonitor(wsUrl?: string): RealtimeMonitor {
*/
export async function monitorContractState(
_contractAddress: Address,
- _publicClient: any,
- onStateChange: (state: any) => void,
+ _publicClient: unknown,
+ onStateChange: (state: Record) => void,
pollInterval = 10000
): Promise<() => void> {
let isMonitoring = true
- let lastState: any = null
+ let lastState: Record | null = null
const checkState = async () => {
if (!isMonitoring) return
diff --git a/frontend-dapp/src/utils/security.ts b/frontend-dapp/src/utils/security.ts
index 98cbd2d..cafd493 100644
--- a/frontend-dapp/src/utils/security.ts
+++ b/frontend-dapp/src/utils/security.ts
@@ -4,6 +4,7 @@
*/
import { isAddress, getAddress } from 'viem';
+import type { PublicClient } from 'viem';
import { ERROR_MESSAGES, VALIDATION, SECURITY } from './constants';
/**
@@ -29,8 +30,11 @@ export function validateAddress(address: string): {
try {
const checksummed = getAddress(address);
return { valid: true, checksummed };
- } catch (error: any) {
- return { valid: false, error: error.message || 'Address validation failed' };
+ } catch (error: unknown) {
+ return {
+ valid: false,
+ error: error instanceof Error ? error.message : 'Address validation failed',
+ };
}
}
@@ -39,7 +43,7 @@ export function validateAddress(address: string): {
*/
export async function isContractAddress(
address: string,
- publicClient: any // viem PublicClient
+ publicClient: Pick // viem PublicClient
): Promise {
try {
const code = await publicClient.getBytecode({ address: address as `0x${string}` });
diff --git a/frontend-dapp/src/utils/transactionSimulator.ts b/frontend-dapp/src/utils/transactionSimulator.ts
index 9d9e700..a7d7313 100644
--- a/frontend-dapp/src/utils/transactionSimulator.ts
+++ b/frontend-dapp/src/utils/transactionSimulator.ts
@@ -3,15 +3,16 @@
*/
import type { Address } from 'viem'
-import type { Abi } from 'viem'
+import { formatEther } from 'viem'
+import type { Abi, AbiFunction, Hex, PublicClient } from 'viem'
export interface SimulationResult {
success: boolean
gasEstimate: bigint
- returnValue?: any
+ returnValue?: unknown
error?: string
- traces?: any[]
- logs?: any[]
+ traces?: unknown[]
+ logs?: unknown[]
}
export interface SimulatedCall {
@@ -26,7 +27,7 @@ export interface SimulatedCall {
* Simulate a contract call
*/
export async function simulateCall(
- publicClient: any,
+ publicClient: Pick,
call: SimulatedCall
): Promise {
try {
@@ -38,15 +39,14 @@ export async function simulateCall(
return {
success: true,
- gasEstimate: result.gasUsed || 21000n,
+ gasEstimate: 21000n,
returnValue: result.data,
- logs: result.logs,
}
- } catch (error: any) {
+ } catch (error: unknown) {
return {
success: false,
gasEstimate: 0n,
- error: error.message || 'Simulation failed',
+ error: error instanceof Error ? error.message : 'Simulation failed',
}
}
}
@@ -55,7 +55,7 @@ export async function simulateCall(
* Simulate contract function call
*/
export async function simulateFunctionCall(
- publicClient: any,
+ publicClient: Pick,
contractAddress: Address,
abi: Abi,
functionName: string,
@@ -67,7 +67,7 @@ export async function simulateFunctionCall(
const { encodeFunctionData } = await import('viem')
const data = encodeFunctionData({
abi,
- functionName: functionName as any,
+ functionName: functionName as never,
args,
})
@@ -86,16 +86,14 @@ export async function simulateFunctionCall(
})
// Decode return value if possible
- let returnValue: any = null
+ let returnValue: unknown = null
try {
const { decodeFunctionResult } = await import('viem')
- const functionAbi = abi.find(
- (item: any) => item.type === 'function' && item.name === functionName
- ) as any
+ const functionAbi = abi.find((item): item is AbiFunction => item.type === 'function' && item.name === functionName)
if (functionAbi && functionAbi.outputs && result.data && result.data !== '0x') {
returnValue = decodeFunctionResult({
abi,
- functionName: functionName as any,
+ functionName: functionAbi.name as never,
data: result.data,
})
}
@@ -108,15 +106,15 @@ export async function simulateFunctionCall(
success: true,
gasEstimate,
returnValue,
- logs: result.logs,
}
- } catch (error: any) {
+ } catch (error: unknown) {
// Try to extract revert reason
- let errorMessage = error.message || 'Simulation failed'
- if (error.data || error.cause?.data) {
+ const errorRecord = error as { data?: unknown; cause?: { data?: unknown }; message?: string }
+ let errorMessage = errorRecord.message || 'Simulation failed'
+ if (errorRecord.data || errorRecord.cause?.data) {
try {
const { decodeErrorResult } = await import('viem')
- const errorData = error.data || error.cause?.data
+ const errorData = (errorRecord.data || errorRecord.cause?.data) as Hex
const decoded = decodeErrorResult({
abi,
data: errorData,
@@ -139,7 +137,7 @@ export async function simulateFunctionCall(
* Simulate batch of calls
*/
export async function simulateBatch(
- publicClient: any,
+ publicClient: Pick,
calls: SimulatedCall[]
): Promise {
const results: SimulationResult[] = []
@@ -163,7 +161,6 @@ export function estimateGasCost(
gasPrice: bigint
): string {
const cost = gasEstimate * gasPrice
- const { formatEther } = require('viem')
return formatEther(cost)
}
diff --git a/frontend-dapp/src/vite-env.d.ts b/frontend-dapp/src/vite-env.d.ts
index 911df7e..a873f4d 100644
--- a/frontend-dapp/src/vite-env.d.ts
+++ b/frontend-dapp/src/vite-env.d.ts
@@ -18,15 +18,9 @@ declare global {
Buffer: typeof import('buffer').Buffer
EventEmitter: typeof import('events').EventEmitter
}
-
- // eslint-disable-next-line @typescript-eslint/no-empty-object-type
- interface GlobalThis {
- Buffer: typeof import('buffer').Buffer
- EventEmitter: typeof import('events').EventEmitter
- }
-
- var Buffer: typeof import('buffer').Buffer
- var EventEmitter: typeof import('events').EventEmitter
}
+declare const Buffer: typeof import('buffer').Buffer
+declare const EventEmitter: typeof import('events').EventEmitter
+
export {}
diff --git a/frontend-dapp/vite.config.ts b/frontend-dapp/vite.config.ts
index 5493c0f..e860d95 100644
--- a/frontend-dapp/vite.config.ts
+++ b/frontend-dapp/vite.config.ts
@@ -24,80 +24,84 @@ export default defineConfig(({ mode }) => {
const isZ = env.VITE_ECOSYSTEM === 'z'
return {
- plugins: [
- react(),
- ecosystemHtmlPlugin(isZ),
- nodePolyfills({
- // Enable polyfills for specific modules
- globals: {
- Buffer: true,
- global: true,
- process: true,
- },
- // Include specific polyfills
- include: ['buffer', 'events', 'stream', 'util', 'crypto', 'vm'],
- // Exclude Node.js built-ins that shouldn't be polyfilled
- exclude: ['https', 'http', 'url', 'path', 'fs', 'os', 'net', 'tls', 'zlib'],
- }),
- ],
- resolve: {
- alias: {
- '@': path.resolve(__dirname, './src'),
- },
- // Dedupe to avoid "multiple instances" warnings from transitive deps (e.g. @emotion/react, Lit)
- dedupe: [
- 'react',
- 'react-dom',
- '@emotion/react',
- '@emotion/styled',
- 'lit',
- 'lit-html',
- 'lit-element',
+ plugins: [
+ react(),
+ ecosystemHtmlPlugin(isZ),
+ nodePolyfills({
+ globals: {
+ Buffer: true,
+ global: true,
+ process: true,
+ },
+ include: ['buffer', 'events', 'stream', 'util', 'crypto', 'vm'],
+ exclude: ['https', 'http', 'url', 'path', 'fs', 'os', 'net', 'tls', 'zlib'],
+ }),
],
- },
- server: {
- port: 3002,
- proxy: {
- '/api': { target: 'http://localhost:3000', changeOrigin: true },
- '/settlement': { target: 'https://secure.omdnl.org', changeOrigin: true },
- '/exchange': { target: 'https://exchange.omdnl.org', changeOrigin: true },
- '/omnl': {
- target: 'http://localhost:3000',
- changeOrigin: true,
- bypass(req) {
- const url = req.url ?? '';
- if (/^\/omnl\/(compliance|dashboard|terminal)\/?(\?.*)?$/.test(url)) return url;
- if (url.startsWith('/omnl/static/')) return url;
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src'),
+ },
+ // Dedupe to avoid "multiple instances" warnings from transitive deps (e.g. @emotion/react, Lit)
+ dedupe: [
+ 'react',
+ 'react-dom',
+ '@emotion/react',
+ '@emotion/styled',
+ 'lit',
+ 'lit-html',
+ 'lit-element',
+ ],
+ },
+ server: {
+ port: 3002,
+ proxy: {
+ '/api': { target: 'http://localhost:3000', changeOrigin: true },
+ '/settlement': { target: 'https://secure.omdnl.org', changeOrigin: true },
+ '/exchange': { target: 'https://exchange.omdnl.org', changeOrigin: true },
+ '/omnl': {
+ target: 'http://localhost:3000',
+ changeOrigin: true,
+ bypass(req) {
+ const url = req.url ?? '';
+ if (/^\/omnl\/(compliance|dashboard|terminal)\/?(\?.*)?$/.test(url)) return url;
+ if (url.startsWith('/omnl/static/')) return url;
+ },
+ },
+ '/reserve': { target: 'http://localhost:3000', changeOrigin: true },
+ },
+ },
+ optimizeDeps: {
+ // Exclude problematic packages from optimization
+ exclude: ['https', 'http', 'url', 'stream', 'util', 'crypto', 'path', 'fs', 'os', 'net', 'tls', 'zlib'],
+ include: ['@safe-global/protocol-kit'],
+ },
+ define: {
+ global: 'globalThis',
+ 'process.env': {},
+ },
+ build: {
+ rollupOptions: {
+ external: (id) => {
+ // Do NOT externalise crypto, buffer, stream, util, events - vite-plugin-node-polyfills provides them for the browser.
+ // Externalise only Node built-ins we don't polyfill (bundling would fail or break in browser).
+ const nodeBuiltIns = ['path', 'fs', 'os', 'net', 'tls', 'zlib', 'https', 'http', 'url']
+ return nodeBuiltIns.includes(id)
+ },
+ onwarn(warning, warn) {
+ const message = warning.message || ''
+ if (
+ message.includes('contains an annotation that Rollup cannot interpret') ||
+ message.includes('Use of eval in')
+ ) {
+ return
+ }
+ warn(warning)
},
},
- '/reserve': { target: 'http://localhost:3000', changeOrigin: true },
- },
- },
- optimizeDeps: {
- // Exclude problematic packages from optimization
- exclude: ['https', 'http', 'url', 'stream', 'util', 'crypto', 'path', 'fs', 'os', 'net', 'tls', 'zlib'],
- include: ['@safe-global/protocol-kit'],
- },
- define: {
- global: 'globalThis',
- 'process.env': {},
- },
- build: {
- rollupOptions: {
- output: {
- // Add Content Security Policy meta tag via HTML plugin if needed
- },
- external: (id) => {
- // Do NOT externalise crypto, buffer, stream, util, events - vite-plugin-node-polyfills provides them for the browser.
- // Externalise only Node built-ins we don't polyfill (bundling would fail or break in browser).
- const nodeBuiltIns = ['path', 'fs', 'os', 'net', 'tls', 'zlib', 'https', 'http', 'url']
- return nodeBuiltIns.includes(id)
+ commonjsOptions: {
+ transformMixedEsModules: true,
},
+ chunkSizeWarningLimit: 50000,
},
- commonjsOptions: {
- transformMixedEsModules: true,
- },
- chunkSizeWarningLimit: 1000,
- },
}
})
diff --git a/scripts/deployment/print-omnl-dns-records.sh b/scripts/deployment/print-omnl-dns-records.sh
index 60368df..7d911cb 100644
--- a/scripts/deployment/print-omnl-dns-records.sh
+++ b/scripts/deployment/print-omnl-dns-records.sh
@@ -2,6 +2,7 @@
# Print Cloudflare/registrar DNS records for OMNL production domains.
# Origin server: dev-bis-ali @ 76.53.10.34 (nginx vhosts already deployed)
+set -euo pipefail
ORIGIN_IP="${OMNL_ORIGIN_IP:-76.53.10.34}"
PROXY="${OMNL_DNS_PROXY:-proxied}"
@@ -21,17 +22,25 @@ Zone: omdnl.org
Zone: omdnl.org (Office 24)
Type Name Content Proxy Dashboard
A office24 $ORIGIN_IP yes Office 24 (/office-24)
+ A terminal $ORIGIN_IP yes OMNL terminal (/omnl/terminal)
Zone: d-bis.org
Type Name Content Proxy Notes
A exchange $ORIGIN_IP yes DBIS Trade (/trade)
A secure $ORIGIN_IP yes UPDATE — currently 76.53.10.36
A forex $ORIGIN_IP yes Forex terminal (/trade)
+ A terminal $ORIGIN_IP yes OMNL terminal alias (/omnl/terminal)
+
+Zone: novabank.uk
+ Type Name Content Proxy Dashboard
+ A terminal $ORIGIN_IP yes NovaBank terminal (/novabank/terminal)
# Cloudflare SSL/TLS mode: Full (origin is HTTP on :80)
# After DNS propagates (1–15 min), verify:
# curl -sI https://exchange.omdnl.org/ | head -3
# curl -sI https://office24.omdnl.org/ | head -3
+# curl -sI https://hft-rail.omdnl.org/ | head -3
+# curl -sI https://terminal.novabank.uk/ | head -3
# curl -sI https://secure.d-bis.org/ | head -3
Live URLs once DNS is active:
@@ -44,6 +53,9 @@ Live URLs once DNS is active:
https://zblockchainsystem.com/z-wallet
https://zblockchainsystem.com/swap
https://office24.omdnl.org/office-24
+ https://hft-rail.omdnl.org/
+ https://terminal.d-bis.org/
+ https://terminal.novabank.uk/
https://exchange.d-bis.org/trade
https://secure.d-bis.org/central-bank
https://forex.d-bis.org/trade