From abdd8881e2994c3bcbfadcf0688a7e7df86bce72 Mon Sep 17 00:00:00 2001 From: defiQUG Date: Sun, 5 Jul 2026 16:48:36 -0700 Subject: [PATCH 01/10] Add Z Bank mainnet payment presets --- .../central-bank/CentralBankDashboard.tsx | 54 +++-- .../ZBankMainnetTransferPanel.tsx | 140 +++++++++++++ .../central-bank/useZBankSettlement.ts | 188 ++++++++++++++++++ .../central-bank/zbank-chain-assets.ts | 48 +++++ packages/settlement-core/src/types.ts | 2 + .../src/adapters/token-aggregation.ts | 16 +- .../dbis-exchange/src/api/routes/exchange.ts | 34 +++- .../src/api/routes/settlement.ts | 2 + .../src/workflows/external-transfer.ts | 2 + 9 files changed, 464 insertions(+), 22 deletions(-) create mode 100644 frontend-dapp/src/features/central-bank/ZBankMainnetTransferPanel.tsx create mode 100644 frontend-dapp/src/features/central-bank/useZBankSettlement.ts create mode 100644 frontend-dapp/src/features/central-bank/zbank-chain-assets.ts diff --git a/frontend-dapp/src/features/central-bank/CentralBankDashboard.tsx b/frontend-dapp/src/features/central-bank/CentralBankDashboard.tsx index d60bdc5..2629546 100644 --- a/frontend-dapp/src/features/central-bank/CentralBankDashboard.tsx +++ b/frontend-dapp/src/features/central-bank/CentralBankDashboard.tsx @@ -1,8 +1,10 @@ +import { useState } from 'react'; import { Link } from 'react-router-dom'; import { useCentralBank } from './useCentralBank'; import ChainMatrixPanel from './ChainMatrixPanel'; +import ZBankMainnetTransferPanel from './ZBankMainnetTransferPanel'; import FineractLedgerBanner from '../omnl-dashboard/FineractLedgerBanner'; @@ -23,6 +25,7 @@ import { formatUsd } from '../../utils/formatMoney'; export default function CentralBankDashboard() { const cb = useCentralBank(); + const [activeTab, setActiveTab] = useState<'overview' | 'payments'>('overview'); const office = cb.office as { @@ -80,6 +83,26 @@ export default function CentralBankDashboard() { +
+ {([ + ['overview', 'Overview'], + ['payments', 'Payments'], + ] as const).map(([value, label]) => ( + + ))} +
+ {cb.error && ( @@ -102,19 +125,17 @@ export default function CentralBankDashboard() { - + ) : ( + <> + - moneySupply={cb.moneySupply} - - loading={cb.loading} - - officeName={office?.name} - - /> - - - -
+

@@ -146,11 +167,11 @@ export default function CentralBankDashboard() { -

+
-
+
@@ -312,11 +333,12 @@ export default function CentralBankDashboard() { -
+
+ + )}
); } - diff --git a/frontend-dapp/src/features/central-bank/ZBankMainnetTransferPanel.tsx b/frontend-dapp/src/features/central-bank/ZBankMainnetTransferPanel.tsx new file mode 100644 index 0000000..f4be742 --- /dev/null +++ b/frontend-dapp/src/features/central-bank/ZBankMainnetTransferPanel.tsx @@ -0,0 +1,140 @@ +import NasaIcon from '../../components/icons/NasaIcon'; + +import { useZBankSettlement } from './useZBankSettlement'; + +export default function ZBankMainnetTransferPanel() { + const settlement = useZBankSettlement(); + + const actionByPreset = { + 'eth-mainnet': settlement.sendEthToMainnet, + 'usdt-mainnet': settlement.sendUsdtToMainnet, + } as const; + + return ( +
+
+
+

+ + Payments → Send to Ethereum mainnet +

+

+ One-click settlement, mint, and bridge presets for Ethereum mainnet. +

+
+ {settlement.status} +
+ +
+ + + +
+ +
+ {settlement.presets.map((preset) => ( +
+
+
+

{preset.buttonLabel}

+

+ {preset.bridgeRail} · {preset.tokenLineId} · targetChainId: {preset.destinationChainId} +

+
+ {preset.amountLabel} +
+ + + + +
+ ))} +
+ + {settlement.error && ( +

+ + {settlement.error} +

+ )} + +
+

Result

+ {settlement.result ? ( +
+
+
Settlement phase
+
{settlement.result.settlement?.phase ?? '—'}
+
+
+
Settlement ID
+
{settlement.result.settlement?.settlementId ?? '—'}
+
+
+
Mint tx
+
{settlement.result.settlement?.chainTxHash ?? '—'}
+
+
+
Bridge tx
+
{settlement.result.bridgeTxHash ?? '—'}
+
+
+
Route
+
+ {settlement.result.executionPlan?.plannerResponse?.selectedRouteReason ?? + settlement.result.executionPlan?.plannerResponse?.routePlan?.legs + ?.map((leg) => leg.provider) + .filter(Boolean) + .join(' → ') ?? + '—'} +
+
+
+ ) : ( +

No transfer submitted yet.

+ )} +
+
+ ); +} diff --git a/frontend-dapp/src/features/central-bank/useZBankSettlement.ts b/frontend-dapp/src/features/central-bank/useZBankSettlement.ts new file mode 100644 index 0000000..dcfca10 --- /dev/null +++ b/frontend-dapp/src/features/central-bank/useZBankSettlement.ts @@ -0,0 +1,188 @@ +import { useEffect, useState } from 'react'; +import { useAccount, useSendTransaction } from 'wagmi'; + +import { omnlApiHeaders, parseAmountToWei, exchangeApi, settlementApi } from '../../config/dex'; + +import { ZBANK_MAINNET_PRESETS, type ZBankMainnetPreset } from './zbank-chain-assets'; + +type SettlementRecord = { + settlementId?: string; + phase?: string; + chainTxHash?: string; + errors?: string[]; +}; + +type ExecutionPlan = { + error?: string; + execution?: { + contractAddress: string; + encodedCalldata: string; + }; + plannerResponse?: { + selectedRouteReason?: string; + routePlan?: { + legs?: Array<{ provider?: string }>; + }; + }; +}; + +type TransferResult = { + presetId: ZBankMainnetPreset['id']; + amount: string; + recipientWallet: string; + creditorIban: string; + settlement?: SettlementRecord; + executionPlan?: ExecutionPlan; + bridgeTxHash?: string; +}; + +export function useZBankSettlement() { + const { address, isConnected } = useAccount(); + const { sendTransactionAsync } = useSendTransaction(); + + const [mainnetWallet, setMainnetWallet] = useState(''); + const [creditorIban, setCreditorIban] = useState(''); + const [amounts, setAmounts] = useState>({ + 'eth-mainnet': '', + 'usdt-mainnet': '', + }); + const [loadingPreset, setLoadingPreset] = useState(null); + const [status, setStatus] = useState('Idle'); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + if (address && !mainnetWallet) { + setMainnetWallet(address); + } + }, [address, mainnetWallet]); + + async function postJson(url: string, body: Record): Promise { + const res = await fetch(url, { + method: 'POST', + headers: omnlApiHeaders(), + body: JSON.stringify(body), + }); + const data = (await res.json()) as T & { error?: string }; + if (!res.ok || data.error) { + throw new Error(data.error || `Request failed: ${res.status}`); + } + return data; + } + + async function runPreset(preset: ZBankMainnetPreset) { + const amount = amounts[preset.id]?.trim(); + if (!isConnected || !address) { + setError('Connect wallet before sending to mainnet.'); + return; + } + if (!mainnetWallet.trim()) { + setError('Ethereum mainnet wallet is required.'); + return; + } + if (!creditorIban.trim()) { + setError('Z Bank IBAN is required.'); + return; + } + if (!amount) { + setError(`${preset.amountLabel} amount is required.`); + return; + } + + setLoadingPreset(preset.id); + setStatus('Posting settlement'); + setError(null); + setResult(null); + + try { + const settlement = await postJson(settlementApi('/convert/fiat-to-crypto'), { + idempotencyKey: `ZB-${preset.id}-${Date.now()}`, + valueDate: new Date().toISOString().slice(0, 10), + currency: preset.currency, + amount, + creditorIban: creditorIban.trim(), + beneficiaryName: 'Z Bank Mainnet Transfer', + remittanceInfo: `${preset.buttonLabel} → ${mainnetWallet.trim()}`, + convertToCrypto: { + enabled: true, + targetChainId: preset.destinationChainId, + tokenLineId: preset.tokenLineId, + recipientAddress: address, + symbol: preset.sourceSymbol, + tokenAddress: preset.sourceTokenAddress, + }, + }); + + setResult({ + presetId: preset.id, + amount, + recipientWallet: mainnetWallet.trim(), + creditorIban: creditorIban.trim(), + settlement, + }); + + setStatus('Building bridge execution'); + const executionPlan = await postJson(exchangeApi('/execute-plan'), { + tokenIn: preset.sourceTokenAddress, + tokenOut: preset.destinationTokenAddress, + amountIn: parseAmountToWei(amount, preset.sourceDecimals), + recipient: mainnetWallet.trim(), + destinationChainId: preset.destinationChainId, + allowBridge: true, + preferredBridges: [preset.preferredBridgeLabel], + }); + + if (!executionPlan.execution) { + throw new Error(executionPlan.error || 'Bridge execution plan missing calldata.'); + } + + setResult((current) => + current + ? { + ...current, + executionPlan, + } + : current, + ); + + setStatus('Submitting bridge transaction'); + const bridgeTxHash = await sendTransactionAsync({ + to: executionPlan.execution.contractAddress as `0x${string}`, + data: executionPlan.execution.encodedCalldata as `0x${string}`, + }); + + setResult((current) => + current + ? { + ...current, + bridgeTxHash, + } + : current, + ); + setStatus('Submitted'); + } catch (err) { + setError(err instanceof Error ? err.message : 'Mainnet transfer failed'); + setStatus('Failed'); + } finally { + setLoadingPreset(null); + } + } + + return { + address, + isConnected, + mainnetWallet, + setMainnetWallet, + creditorIban, + setCreditorIban, + amounts, + setAmounts, + loadingPreset, + status, + result, + error, + presets: ZBANK_MAINNET_PRESETS, + sendEthToMainnet: () => runPreset(ZBANK_MAINNET_PRESETS[0]), + sendUsdtToMainnet: () => runPreset(ZBANK_MAINNET_PRESETS[1]), + }; +} diff --git a/frontend-dapp/src/features/central-bank/zbank-chain-assets.ts b/frontend-dapp/src/features/central-bank/zbank-chain-assets.ts new file mode 100644 index 0000000..446c930 --- /dev/null +++ b/frontend-dapp/src/features/central-bank/zbank-chain-assets.ts @@ -0,0 +1,48 @@ +export type ZBankMainnetPreset = { + id: 'eth-mainnet' | 'usdt-mainnet'; + buttonLabel: string; + amountLabel: string; + sourceSymbol: string; + sourceTokenAddress: `0x${string}`; + sourceDecimals: number; + destinationSymbol: string; + destinationTokenAddress: `0x${string}`; + destinationChainId: 1; + bridgeRail: string; + preferredBridgeLabel: string; + tokenLineId: string; + currency: string; +}; + +export const ZBANK_MAINNET_PRESETS: ZBankMainnetPreset[] = [ + { + id: 'eth-mainnet', + buttonLabel: 'Send ETH → Mainnet', + amountLabel: 'ETH', + sourceSymbol: 'WETH', + sourceTokenAddress: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', + sourceDecimals: 18, + destinationSymbol: 'WETH', + destinationTokenAddress: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', + destinationChainId: 1, + bridgeRail: 'ccip-weth9', + preferredBridgeLabel: 'CCIPWETH9Bridge', + tokenLineId: 'WETH-M2', + currency: 'USD', + }, + { + id: 'usdt-mainnet', + buttonLabel: 'Send USDT → Mainnet', + amountLabel: 'USDT', + sourceSymbol: 'USDT', + sourceTokenAddress: '0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1', + sourceDecimals: 6, + destinationSymbol: 'USDT', + destinationTokenAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7', + destinationChainId: 1, + bridgeRail: 'trustless-lockbox', + preferredBridgeLabel: 'GRUTransport', + tokenLineId: 'USD-M2', + currency: 'USD', + }, +]; diff --git a/packages/settlement-core/src/types.ts b/packages/settlement-core/src/types.ts index 488c4a7..ed6d535 100644 --- a/packages/settlement-core/src/types.ts +++ b/packages/settlement-core/src/types.ts @@ -57,6 +57,8 @@ export type ExternalTransferRequest = { targetChainId: number; tokenLineId: string; recipientAddress: string; + symbol?: string; + tokenAddress?: string; }; }; diff --git a/services/dbis-exchange/src/adapters/token-aggregation.ts b/services/dbis-exchange/src/adapters/token-aggregation.ts index 187fa5f..b1b4dc7 100644 --- a/services/dbis-exchange/src/adapters/token-aggregation.ts +++ b/services/dbis-exchange/src/adapters/token-aggregation.ts @@ -26,6 +26,8 @@ export async function fetchRoutePlan(body: { amountIn: string; recipient?: string; maxSlippageBps?: number; + allowBridge?: boolean; + preferredBridges?: string[]; }) { const { data } = await axios.post( `${tokenAggUrl()}/api/v2/routes/plan`, @@ -36,7 +38,11 @@ export async function fetchRoutePlan(body: { tokenOut: body.tokenOut, amountIn: body.amountIn, recipient: body.recipient, - constraints: { maxSlippageBps: body.maxSlippageBps ?? 50, allowBridge: false }, + constraints: { + maxSlippageBps: body.maxSlippageBps ?? 50, + allowBridge: body.allowBridge ?? false, + preferredBridges: body.preferredBridges, + }, }, { timeout: 120000 }, ); @@ -51,6 +57,8 @@ export async function fetchExecutionPlan(body: { amountIn: string; recipient: string; maxSlippageBps?: number; + allowBridge?: boolean; + preferredBridges?: string[]; }) { const { data } = await axios.post( `${tokenAggUrl()}/api/v2/routes/internal-execution-plan`, @@ -61,7 +69,11 @@ export async function fetchExecutionPlan(body: { tokenOut: body.tokenOut, amountIn: body.amountIn, recipient: body.recipient, - constraints: { maxSlippageBps: body.maxSlippageBps ?? 50, allowBridge: false }, + constraints: { + maxSlippageBps: body.maxSlippageBps ?? 50, + allowBridge: body.allowBridge ?? false, + preferredBridges: body.preferredBridges, + }, }, { timeout: 120000 }, ); diff --git a/services/dbis-exchange/src/api/routes/exchange.ts b/services/dbis-exchange/src/api/routes/exchange.ts index 51d1f97..c8756e1 100644 --- a/services/dbis-exchange/src/api/routes/exchange.ts +++ b/services/dbis-exchange/src/api/routes/exchange.ts @@ -106,19 +106,32 @@ export function createExchangeRouter(): Router { router.post('/plan', async (req, res) => { if (!requireTrade(req, res)) return; try { - const { tokenIn, tokenOut, amountIn, recipient, maxSlippageBps } = req.body as Record; + const { + tokenIn, + tokenOut, + amountIn, + recipient, + maxSlippageBps, + destinationChainId, + allowBridge, + preferredBridges, + } = req.body as Record; if (!tokenIn || !tokenOut || !amountIn) { res.status(400).json({ error: 'tokenIn, tokenOut, amountIn required' }); return; } const plan = await fetchRoutePlan({ sourceChainId: cfg.chainId, - destinationChainId: cfg.chainId, + destinationChainId: destinationChainId ? Number(destinationChainId) : cfg.chainId, tokenIn: String(tokenIn), tokenOut: String(tokenOut), amountIn: String(amountIn), recipient: recipient ? String(recipient) : undefined, maxSlippageBps: maxSlippageBps ? Number(maxSlippageBps) : cfg.defaultSlippageBps, + allowBridge: typeof allowBridge === 'boolean' ? allowBridge : undefined, + preferredBridges: Array.isArray(preferredBridges) + ? preferredBridges.map((value) => String(value)) + : undefined, }); res.json(plan); } catch (e) { @@ -129,19 +142,32 @@ export function createExchangeRouter(): Router { router.post('/execute-plan', async (req, res) => { if (!requireTrade(req, res)) return; try { - const { tokenIn, tokenOut, amountIn, recipient, maxSlippageBps } = req.body as Record; + const { + tokenIn, + tokenOut, + amountIn, + recipient, + maxSlippageBps, + destinationChainId, + allowBridge, + preferredBridges, + } = req.body as Record; if (!tokenIn || !tokenOut || !amountIn || !recipient) { res.status(400).json({ error: 'tokenIn, tokenOut, amountIn, recipient required' }); return; } const plan = await fetchExecutionPlan({ sourceChainId: cfg.chainId, - destinationChainId: cfg.chainId, + destinationChainId: destinationChainId ? Number(destinationChainId) : cfg.chainId, tokenIn: String(tokenIn), tokenOut: String(tokenOut), amountIn: String(amountIn), recipient: String(recipient), maxSlippageBps: maxSlippageBps ? Number(maxSlippageBps) : cfg.defaultSlippageBps, + allowBridge: typeof allowBridge === 'boolean' ? allowBridge : undefined, + preferredBridges: Array.isArray(preferredBridges) + ? preferredBridges.map((value) => String(value)) + : undefined, }); res.json(plan); } catch (e) { diff --git a/services/settlement-middleware/src/api/routes/settlement.ts b/services/settlement-middleware/src/api/routes/settlement.ts index b410a74..e0e3cac 100644 --- a/services/settlement-middleware/src/api/routes/settlement.ts +++ b/services/settlement-middleware/src/api/routes/settlement.ts @@ -224,6 +224,8 @@ export function createSettlementRouter(): Router { targetChainId: body.convertToCrypto?.targetChainId ?? 138, tokenLineId: body.convertToCrypto?.tokenLineId ?? `${body.currency ?? 'USD'}-M2`, recipientAddress: body.convertToCrypto?.recipientAddress ?? '', + symbol: body.convertToCrypto?.symbol, + tokenAddress: body.convertToCrypto?.tokenAddress, }, moneyLayers: ['M2', 'M3'], }); diff --git a/services/settlement-middleware/src/workflows/external-transfer.ts b/services/settlement-middleware/src/workflows/external-transfer.ts index dd5f437..4dd0043 100644 --- a/services/settlement-middleware/src/workflows/external-transfer.ts +++ b/services/settlement-middleware/src/workflows/external-transfer.ts @@ -160,6 +160,8 @@ export async function processExternalTransfer( recipient: req.convertToCrypto.recipientAddress, settlementRef: record.settlementId, dryRun: !cfg.production.allowChainMintExecute, + symbol: req.convertToCrypto.symbol, + tokenAddress: req.convertToCrypto.tokenAddress, }); advance('CHAIN_MINT_REQUESTED', { chainTxHash: mint.txHash }); } else { -- 2.34.1 From 8ee8930b5bf73f12c95ce58bca4f75da0b6d204f Mon Sep 17 00:00:00 2001 From: defiQUG Date: Sun, 5 Jul 2026 17:31:33 -0700 Subject: [PATCH 02/10] Harden OMNL dotenv layering and local health --- .../build-portal-lxc-env-fragment.sh | 56 ++++- .../deployment/deploy-omnl-bank-production.sh | 36 ++- scripts/deployment/merge-env-fragment.sh | 53 +++-- .../deployment/start-omnl-banking-portal.sh | 222 ++++++++++-------- scripts/lib/deployment/dotenv.sh | 30 ++- services/token-aggregation/src/api/server.ts | 13 + 6 files changed, 278 insertions(+), 132 deletions(-) diff --git a/scripts/deployment/build-portal-lxc-env-fragment.sh b/scripts/deployment/build-portal-lxc-env-fragment.sh index 634596d..64331d2 100755 --- a/scripts/deployment/build-portal-lxc-env-fragment.sh +++ b/scripts/deployment/build-portal-lxc-env-fragment.sh @@ -11,18 +11,56 @@ CONFIG="$REPO_DIR/config/omnl-super-admins.v1.json" [[ -f "$ENV_FILE" ]] || { echo "Missing $ENV_FILE" >&2; exit 1; } -set -a -set +u -# shellcheck disable=SC1090 -source "$ENV_FILE" -set -u -set +a +if [[ -f "$REPO_DIR/scripts/lib/deployment/dotenv.sh" ]]; then + # shellcheck disable=SC1090 + source "$REPO_DIR/scripts/lib/deployment/dotenv.sh" + ENV_FILE="$ENV_FILE" load_deployment_env --repo-root "$REPO_DIR" +else + set -a + set +u + # shellcheck disable=SC1090 + source "$ENV_FILE" + set -u + set +a +fi -grep -E '^(OMNL_FINERACT_|OMNL_PUBLIC_MONEY_SUPPLY=|OMNL_REQUIRE_API_KEY=|OMNL_CUSTOMER_SECURITY_PRODUCTION=|OMNL_CUSTOMER_API_KEYS=|OFFICE24_OPENING_M1_USD=|OMNL_PORTAL_INTERNAL_SECRET=|JWT_SECRET=|DATABASE_URL=|RPC_URL_138=|CHAIN_138_RPC_URL=|SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE=|SETTLEMENT_ALLOW_HYBX_PRODUCTION=|OMNL_ALLOW_CHAIN_MINT_EXECUTE=|OMNL_MINT_OPERATOR_PRIVATE_KEY=|TOKEN_AGGREGATION_URL=|SWIFT_LISTENER_URL=|HYBX_MIFOS_FINERACT_SIDECAR_URL=|INFURA_PROJECT_ID=|INFURA_PROJECT_SECRET=)' "$ENV_FILE" || true +emit_key() { + local key="$1" + local value="${!key:-}" + [[ -n "$value" ]] && printf '%s=%s\n' "$key" "$value" +} + +for key in \ + OMNL_FINERACT_BASE_URL \ + OMNL_FINERACT_TENANT \ + OMNL_FINERACT_USERNAME \ + OMNL_FINERACT_USER \ + OMNL_FINERACT_PASSWORD \ + OMNL_PUBLIC_MONEY_SUPPLY \ + OMNL_REQUIRE_API_KEY \ + OMNL_CUSTOMER_SECURITY_PRODUCTION \ + OMNL_CUSTOMER_API_KEYS \ + OFFICE24_OPENING_M1_USD \ + OMNL_PORTAL_INTERNAL_SECRET \ + JWT_SECRET \ + DATABASE_URL \ + RPC_URL_138 \ + CHAIN_138_RPC_URL \ + SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE \ + SETTLEMENT_ALLOW_HYBX_PRODUCTION \ + OMNL_ALLOW_CHAIN_MINT_EXECUTE \ + OMNL_MINT_OPERATOR_PRIVATE_KEY \ + TOKEN_AGGREGATION_URL \ + SWIFT_LISTENER_URL \ + HYBX_MIFOS_FINERACT_SIDECAR_URL \ + INFURA_PROJECT_ID \ + INFURA_PROJECT_SECRET +do + emit_key "$key" +done for key in OMNL_SUPER_ADMIN_OFFICE24_KEY OMNL_SUPER_ADMIN_EXCHANGE_KEY OMNL_SUPER_ADMIN_SECURE_KEY OMNL_SUPER_ADMIN_FOREX_KEY; do - val="${!key:-}" - [[ -n "$val" ]] && echo "${key}=${val}" + emit_key "$key" done ADMIN_ENV_KEY="$(node -e " diff --git a/scripts/deployment/deploy-omnl-bank-production.sh b/scripts/deployment/deploy-omnl-bank-production.sh index 730b044..fbbeb6d 100644 --- a/scripts/deployment/deploy-omnl-bank-production.sh +++ b/scripts/deployment/deploy-omnl-bank-production.sh @@ -6,21 +6,27 @@ REPO_DIR="${OMNL_BANK_ROOT:-$HOME/smom-dbis-138}" BRANCH="${OMNL_BANK_BRANCH:-main}" LOG_DIR="${OMNL_BANK_LOG_DIR:-$HOME/omnl-bank/logs}" ENV_FILE="${OMNL_BANK_ENV:-$REPO_DIR/.env}" +SKIP_GIT_PULL="${OMNL_SKIP_GIT_PULL:-0}" +DEPLOY_DOTENV_LIB="${REPO_DIR}/scripts/lib/deployment/dotenv.sh" log() { echo "[$(date -Iseconds)] $*"; } mkdir -p "$LOG_DIR" -if [ ! -d "$REPO_DIR/.git" ]; then +if ! git -C "$REPO_DIR" rev-parse --git-dir >/dev/null 2>&1; then log "Cloning smom-dbis-138..." git clone "https://gitea.d-bis.org/d-bis/smom-dbis-138.git" "$REPO_DIR" fi cd "$REPO_DIR" -log "Pull latest ($BRANCH)..." -git fetch origin -git checkout "$BRANCH" -git pull --ff-only origin "$BRANCH" +if [[ "$SKIP_GIT_PULL" == "1" ]]; then + log "Skipping git fetch/checkout/pull (OMNL_SKIP_GIT_PULL=1); building current working tree." +else + log "Pull latest ($BRANCH)..." + git fetch origin + git checkout "$BRANCH" + git pull --ff-only origin "$BRANCH" +fi log "Generate 128-chain registry..." node scripts/generate-omnl-128-chains.mjs @@ -30,12 +36,20 @@ export DBIS_EXCHANGE_CONFIG="${DBIS_EXCHANGE_CONFIG:-$REPO_DIR/config/dbis-excha export OMNL_SUPPORTED_CHAINS_CONFIG="${OMNL_SUPPORTED_CHAINS_CONFIG:-$REPO_DIR/config/omnl-supported-chains.v1.json}" export OMNL_M2_TOKEN_REGISTRY="${OMNL_M2_TOKEN_REGISTRY:-$REPO_DIR/config/omnl-m2-token-registry.v1.json}" -if [ -f "$ENV_FILE" ]; then - if grep -q $'\r' "$ENV_FILE" 2>/dev/null; then - log "Normalizing CRLF line endings in $ENV_FILE" - tr -d '\r' <"$ENV_FILE" >"${ENV_FILE}.lf" && mv "${ENV_FILE}.lf" "$ENV_FILE" - chmod 600 "$ENV_FILE" - fi +if [[ -f "$ENV_FILE" && "$(grep -c $'\r' "$ENV_FILE" 2>/dev/null || true)" -gt 0 ]]; then + log "Normalizing CRLF line endings in $ENV_FILE" + tr -d '\r' <"$ENV_FILE" >"${ENV_FILE}.lf" && mv "${ENV_FILE}.lf" "$ENV_FILE" + chmod 600 "$ENV_FILE" +fi + +if [[ -f "$DEPLOY_DOTENV_LIB" ]]; then + # Reuse the shared loader so repo .env values are loaded first and smom-dbis-138/.env backfills only missing keys. + export ENV_FILE + export PROJECT_ROOT="$REPO_DIR" + # shellcheck disable=SC1090 + source "$DEPLOY_DOTENV_LIB" + load_deployment_env --repo-root "$REPO_DIR" +elif [ -f "$ENV_FILE" ]; then set -a set +u # shellcheck disable=SC1090 diff --git a/scripts/deployment/merge-env-fragment.sh b/scripts/deployment/merge-env-fragment.sh index 2ab7594..d154792 100644 --- a/scripts/deployment/merge-env-fragment.sh +++ b/scripts/deployment/merge-env-fragment.sh @@ -1,33 +1,48 @@ #!/usr/bin/env bash -# Merge KEY=VALUE lines from a fragment file into target .env (replace existing keys). +# Merge KEY=VALUE lines from a fragment file into target .env. +# Default strategy replaces managed keys from the fragment and preserves all others. set -euo pipefail TARGET="${1:?target .env path}" FRAGMENT="${2:?fragment file path}" +MERGE_STRATEGY="${OMNL_MERGE_STRATEGY:-overwrite}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROXMOX_MERGE_PY="${PROXMOX_MERGE_DOTENV_PY:-$(cd "$SCRIPT_DIR/../../.." && pwd)/scripts/lib/merge-dotenv.py}" touch "$TARGET" -tmp="$(mktemp)" -if [[ -f "$TARGET" ]]; then - cp "$TARGET" "$tmp" + +if [[ -f "$PROXMOX_MERGE_PY" ]]; then + python3 "$PROXMOX_MERGE_PY" --strategy "$MERGE_STRATEGY" "$TARGET" "$FRAGMENT" >/dev/null else - : > "$tmp" + tmp="$(mktemp)" + trap 'rm -f "$tmp" "${tmp}.new"' EXIT + cp "$TARGET" "$tmp" + + while IFS= read -r line || [[ -n "$line" ]]; do + line="${line//$'\r'/}" + [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue + key="${line%%=*}" + [[ "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue + if [[ "$MERGE_STRATEGY" == "preserve" ]] && grep -q "^${key}=" "$tmp"; then + continue + fi + if [[ "$MERGE_STRATEGY" == "fill-empty" ]] && grep -q "^${key}=" "$tmp"; then + current="$(grep "^${key}=" "$tmp" | tail -n 1 | cut -d= -f2-)" + [[ -n "$current" ]] && continue + fi + grep -v "^${key}=" "$tmp" > "${tmp}.new" || true + mv "${tmp}.new" "$tmp" + printf '%s\n' "$line" >> "$tmp" + done < "$FRAGMENT" + + mv "$tmp" "$TARGET" + trap - EXIT fi -while IFS= read -r line || [[ -n "$line" ]]; do - line="${line//$'\r'/}" - [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue - key="${line%%=*}" - [[ "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue - grep -v "^${key}=" "$tmp" > "${tmp}.new" || true - mv "${tmp}.new" "$tmp" - printf '%s\n' "$line" >> "$tmp" -done < "$FRAGMENT" - -if grep -q '^OMNL_FINERACT_USER=' "$tmp" && ! grep -q '^OMNL_FINERACT_USERNAME=' "$tmp"; then - u="$(grep '^OMNL_FINERACT_USER=' "$tmp" | cut -d= -f2-)" - printf 'OMNL_FINERACT_USERNAME=%s\n' "$u" >> "$tmp" +if grep -q '^OMNL_FINERACT_USER=' "$TARGET" && ! grep -q '^OMNL_FINERACT_USERNAME=' "$TARGET"; then + u="$(grep '^OMNL_FINERACT_USER=' "$TARGET" | tail -n 1 | cut -d= -f2-)" + printf 'OMNL_FINERACT_USERNAME=%s\n' "$u" >> "$TARGET" fi -mv "$tmp" "$TARGET" tr -d '\r' <"$TARGET" >"${TARGET}.lf" && mv "${TARGET}.lf" "$TARGET" chmod 600 "$TARGET" diff --git a/scripts/deployment/start-omnl-banking-portal.sh b/scripts/deployment/start-omnl-banking-portal.sh index 8f647a2..d50cbc0 100644 --- a/scripts/deployment/start-omnl-banking-portal.sh +++ b/scripts/deployment/start-omnl-banking-portal.sh @@ -1,92 +1,130 @@ -#!/usr/bin/env bash -# Start OMNL services from /srv/zardasht-portal inside PCT -set -euo pipefail - -ROOT="${OMNL_PORTAL_ROOT:-/srv/zardasht-portal}" -LOG_DIR="${OMNL_BANK_LOG_DIR:-/var/log/omnl-bank}" -NGINX_TEMPLATE="${ROOT}/deploy/nginx/omnl-lxc-portal.conf" -ENV_FILE="${OMNL_BANK_ENV:-${ROOT}/.env}" -mkdir -p "$LOG_DIR" - -if [[ -f "$ENV_FILE" ]]; then - set -a - set +u - # shellcheck disable=SC1090 - source "$ENV_FILE" - set -u - set +a -fi - -export SETTLEMENT_MIDDLEWARE_CONFIG="${ROOT}/config/settlement-middleware.production.v1.json" -export DBIS_EXCHANGE_CONFIG="${ROOT}/config/dbis-exchange.production.v1.json" -export OMNL_SUPPORTED_CHAINS_CONFIG="${ROOT}/config/omnl-supported-chains.v1.json" -export OMNL_M2_TOKEN_REGISTRY="${ROOT}/config/omnl-m2-token-registry.v1.json" -export OMNL_BANK_ROOT="$ROOT" - -start() { - local name="$1" dir="$2" cmd="$3" - cd "${ROOT}/${dir}" - pkill -f "${ROOT}/${dir}" 2>/dev/null || true - nohup env \ - SETTLEMENT_MIDDLEWARE_CONFIG="$SETTLEMENT_MIDDLEWARE_CONFIG" \ - DBIS_EXCHANGE_CONFIG="$DBIS_EXCHANGE_CONFIG" \ - OMNL_SUPPORTED_CHAINS_CONFIG="$OMNL_SUPPORTED_CHAINS_CONFIG" \ - OMNL_M2_TOKEN_REGISTRY="$OMNL_M2_TOKEN_REGISTRY" \ - OMNL_BANK_ROOT="$ROOT" \ - OMNL_API_KEY="${OMNL_API_KEY:-}" \ - OMNL_SUPER_ADMIN_OFFICE24_KEY="${OMNL_SUPER_ADMIN_OFFICE24_KEY:-}" \ - OMNL_SUPER_ADMIN_EXCHANGE_KEY="${OMNL_SUPER_ADMIN_EXCHANGE_KEY:-}" \ - OMNL_SUPER_ADMIN_SECURE_KEY="${OMNL_SUPER_ADMIN_SECURE_KEY:-}" \ - OMNL_SUPER_ADMIN_FOREX_KEY="${OMNL_SUPER_ADMIN_FOREX_KEY:-}" \ - OMNL_PORTAL_INTERNAL_SECRET="${OMNL_PORTAL_INTERNAL_SECRET:-}" \ - OMNL_CUSTOMER_API_KEYS="${OMNL_CUSTOMER_API_KEYS:-}" \ - OMNL_CUSTOMER_SECURITY_PRODUCTION="${OMNL_CUSTOMER_SECURITY_PRODUCTION:-1}" \ - OMNL_FINERACT_BASE_URL="${OMNL_FINERACT_BASE_URL:-}" \ - OMNL_FINERACT_TENANT="${OMNL_FINERACT_TENANT:-omnl}" \ - OMNL_FINERACT_USERNAME="${OMNL_FINERACT_USERNAME:-}" \ - OMNL_FINERACT_USER="${OMNL_FINERACT_USER:-}" \ - OMNL_FINERACT_PASSWORD="${OMNL_FINERACT_PASSWORD:-}" \ - OMNL_PUBLIC_MONEY_SUPPLY="${OMNL_PUBLIC_MONEY_SUPPLY:-1}" \ - DATABASE_URL="${DATABASE_URL:-}" \ - SETTLEMENT_ALLOW_HYBX_PRODUCTION="${SETTLEMENT_ALLOW_HYBX_PRODUCTION:-1}" \ - SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE="${SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE:-1}" \ - OMNL_ALLOW_CHAIN_MINT_EXECUTE="${OMNL_ALLOW_CHAIN_MINT_EXECUTE:-1}" \ - TOKEN_AGGREGATION_URL="${TOKEN_AGGREGATION_URL:-http://127.0.0.1:3000}" \ - HYBX_MIFOS_FINERACT_SIDECAR_URL="${HYBX_MIFOS_FINERACT_SIDECAR_URL:-}" \ - SWIFT_LISTENER_URL="${SWIFT_LISTENER_URL:-}" \ - RPC_URL_138="${RPC_URL_138:-${CHAIN_138_RPC_URL:-}}" \ - CHAIN_138_RPC_URL="${CHAIN_138_RPC_URL:-${RPC_URL_138:-}}" \ - OMNL_MINT_OPERATOR_PRIVATE_KEY="${OMNL_MINT_OPERATOR_PRIVATE_KEY:-}" \ - INFURA_PROJECT_ID="${INFURA_PROJECT_ID:-}" \ - INFURA_PROJECT_SECRET="${INFURA_PROJECT_SECRET:-}" \ - bash -c "$cmd" >"${LOG_DIR}/${name}.log" 2>&1 & - echo $! >"${LOG_DIR}/${name}.pid" -} - -start token-aggregation services/token-aggregation "npm run start" -sleep 2 -start settlement-middleware services/settlement-middleware "npm run start" -sleep 2 -start dbis-exchange services/dbis-exchange "npm run start" -sleep 2 - -FRONTEND_PORT="${OMNL_BANK_FRONTEND_PORT:-3002}" -pkill -f "serve -s.*${FRONTEND_PORT}" 2>/dev/null || true -if [ -f "${LOG_DIR}/nginx.pid" ]; then - nginx -s quit -g "pid ${LOG_DIR}/nginx.pid;" 2>/dev/null || true -fi -fuser -k "${FRONTEND_PORT}/tcp" 2>/dev/null || true -sleep 1 - -if command -v nginx >/dev/null 2>&1 && [ -f "$NGINX_TEMPLATE" ]; then - sed "s|/srv/zardasht-portal|${ROOT}|g; s|/srv/ali-portal|${ROOT}|g" "$NGINX_TEMPLATE" > "${LOG_DIR}/nginx.conf" - nginx -t -c "${LOG_DIR}/nginx.conf" - nohup nginx -c "${LOG_DIR}/nginx.conf" >"${LOG_DIR}/frontend.log" 2>&1 & - echo $! >"${LOG_DIR}/frontend.pid" - echo "OMNL banking portal started under ${ROOT} (nginx :${FRONTEND_PORT})" -else - cd "${ROOT}/frontend-dapp" - nohup npx --yes serve -s dist -l "$FRONTEND_PORT" >"${LOG_DIR}/frontend.log" 2>&1 & - echo $! >"${LOG_DIR}/frontend.pid" - echo "OMNL banking portal started under ${ROOT} (serve :${FRONTEND_PORT})" -fi +#!/usr/bin/env bash +# Start OMNL banking portal services from a deployed portal root. +set -euo pipefail + +ROOT="${OMNL_PORTAL_ROOT:-/srv/zardasht-portal}" +LOG_DIR="${OMNL_BANK_LOG_DIR:-/var/log/omnl-bank}" +NGINX_TEMPLATE="${ROOT}/deploy/nginx/omnl-lxc-portal.conf" +ENV_FILE="${OMNL_BANK_ENV:-${ROOT}/.env}" +FRONTEND_PORT="${OMNL_BANK_FRONTEND_PORT:-3002}" +DEPLOY_DOTENV_LIB="${ROOT}/scripts/lib/deployment/dotenv.sh" + +mkdir -p "$LOG_DIR" + +if [[ -f "$DEPLOY_DOTENV_LIB" ]]; then + # Reuse the shared loader so explicit ENV_FILE wins while repo-root and secure-secret defaults backfill safely. + export ENV_FILE + export PROJECT_ROOT="$ROOT" + # shellcheck disable=SC1090 + source "$DEPLOY_DOTENV_LIB" + load_deployment_env --repo-root "$ROOT" +elif [[ -f "$ENV_FILE" ]]; then + set +u + set -a + # shellcheck disable=SC1090 + source "$ENV_FILE" + set +a + set -u +fi + +export SETTLEMENT_MIDDLEWARE_CONFIG="${ROOT}/config/settlement-middleware.production.v1.json" +export DBIS_EXCHANGE_CONFIG="${ROOT}/config/dbis-exchange.production.v1.json" +export OMNL_SUPPORTED_CHAINS_CONFIG="${ROOT}/config/omnl-supported-chains.v1.json" +export OMNL_M2_TOKEN_REGISTRY="${ROOT}/config/omnl-m2-token-registry.v1.json" +export OMNL_BANK_ROOT="$ROOT" + +ROOT_ALIASES=("$ROOT") +if [[ "$ROOT" == "/srv/ali-portal" ]]; then + ROOT_ALIASES+=("/srv/zardasht-portal") +elif [[ "$ROOT" == "/srv/zardasht-portal" ]]; then + ROOT_ALIASES+=("/srv/ali-portal") +fi + +start_service() { + local name="$1" + local dir="$2" + local cmd="$3" + local active_root="" + local alias_root + + for alias_root in "${ROOT_ALIASES[@]}"; do + if [[ -z "$active_root" && -d "${alias_root}/${dir}" ]]; then + active_root="$alias_root" + fi + pkill -f "${alias_root}/${dir}" 2>/dev/null || true + done + + [[ -n "$active_root" ]] || active_root="$ROOT" + cd "${active_root}/${dir}" + + local -a env_args=( + "SETTLEMENT_MIDDLEWARE_CONFIG=$SETTLEMENT_MIDDLEWARE_CONFIG" + "DBIS_EXCHANGE_CONFIG=$DBIS_EXCHANGE_CONFIG" + "OMNL_SUPPORTED_CHAINS_CONFIG=$OMNL_SUPPORTED_CHAINS_CONFIG" + "OMNL_M2_TOKEN_REGISTRY=$OMNL_M2_TOKEN_REGISTRY" + "OMNL_BANK_ROOT=$ROOT" + "OMNL_API_KEY=${OMNL_API_KEY:-}" + "OMNL_SUPER_ADMIN_OFFICE24_KEY=${OMNL_SUPER_ADMIN_OFFICE24_KEY:-}" + "OMNL_SUPER_ADMIN_EXCHANGE_KEY=${OMNL_SUPER_ADMIN_EXCHANGE_KEY:-}" + "OMNL_SUPER_ADMIN_SECURE_KEY=${OMNL_SUPER_ADMIN_SECURE_KEY:-}" + "OMNL_SUPER_ADMIN_FOREX_KEY=${OMNL_SUPER_ADMIN_FOREX_KEY:-}" + "OMNL_PORTAL_INTERNAL_SECRET=${OMNL_PORTAL_INTERNAL_SECRET:-}" + "OMNL_CUSTOMER_API_KEYS=${OMNL_CUSTOMER_API_KEYS:-}" + "OMNL_CUSTOMER_SECURITY_PRODUCTION=${OMNL_CUSTOMER_SECURITY_PRODUCTION:-1}" + "OMNL_FINERACT_BASE_URL=${OMNL_FINERACT_BASE_URL:-}" + "OMNL_FINERACT_TENANT=${OMNL_FINERACT_TENANT:-omnl}" + "OMNL_FINERACT_USERNAME=${OMNL_FINERACT_USERNAME:-}" + "OMNL_FINERACT_USER=${OMNL_FINERACT_USER:-}" + "OMNL_FINERACT_PASSWORD=${OMNL_FINERACT_PASSWORD:-}" + "OMNL_PUBLIC_MONEY_SUPPLY=${OMNL_PUBLIC_MONEY_SUPPLY:-1}" + "DATABASE_URL=${DATABASE_URL:-}" + "SETTLEMENT_ALLOW_HYBX_PRODUCTION=${SETTLEMENT_ALLOW_HYBX_PRODUCTION:-1}" + "SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE=${SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE:-1}" + "OMNL_ALLOW_CHAIN_MINT_EXECUTE=${OMNL_ALLOW_CHAIN_MINT_EXECUTE:-1}" + "TOKEN_AGGREGATION_URL=${TOKEN_AGGREGATION_URL:-http://127.0.0.1:3000}" + "HYBX_MIFOS_FINERACT_SIDECAR_URL=${HYBX_MIFOS_FINERACT_SIDECAR_URL:-}" + "SWIFT_LISTENER_URL=${SWIFT_LISTENER_URL:-}" + "RPC_URL_138=${RPC_URL_138:-${CHAIN_138_RPC_URL:-}}" + "CHAIN_138_RPC_URL=${CHAIN_138_RPC_URL:-${RPC_URL_138:-}}" + "OMNL_MINT_OPERATOR_PRIVATE_KEY=${OMNL_MINT_OPERATOR_PRIVATE_KEY:-}" + "INFURA_PROJECT_ID=${INFURA_PROJECT_ID:-}" + "INFURA_PROJECT_SECRET=${INFURA_PROJECT_SECRET:-}" + ) + + nohup env "${env_args[@]}" bash -lc "$cmd" >"${LOG_DIR}/${name}.log" 2>&1 & + echo $! >"${LOG_DIR}/${name}.pid" +} + +start_service token-aggregation services/token-aggregation "npm run start" +sleep 2 +start_service settlement-middleware services/settlement-middleware "npm run start" +sleep 2 +start_service dbis-exchange services/dbis-exchange "npm run start" +sleep 2 + +pkill -f "serve -s.*${FRONTEND_PORT}" 2>/dev/null || true +pkill -f "vite.*preview.*${FRONTEND_PORT}" 2>/dev/null || true +if [[ -f "${LOG_DIR}/nginx.pid" ]]; then + nginx -s quit -g "pid ${LOG_DIR}/nginx.pid;" 2>/dev/null || true +fi +fuser -k "${FRONTEND_PORT}/tcp" 2>/dev/null || true +sleep 1 + +if command -v nginx >/dev/null 2>&1 && [[ -f "$NGINX_TEMPLATE" ]]; then + sed "s|/srv/zardasht-portal|${ROOT}|g; s|/srv/ali-portal|${ROOT}|g" "$NGINX_TEMPLATE" >"${LOG_DIR}/nginx.conf" + nginx -t -c "${LOG_DIR}/nginx.conf" + nohup nginx -c "${LOG_DIR}/nginx.conf" >"${LOG_DIR}/frontend.log" 2>&1 & + echo $! >"${LOG_DIR}/frontend.pid" + echo "OMNL banking portal started under ${ROOT} (nginx :${FRONTEND_PORT})" +else + cd "${ROOT}/frontend-dapp" + if [[ -x "./node_modules/vite/bin/vite.js" || -f "./node_modules/vite/bin/vite.js" ]]; then + nohup node ./node_modules/vite/bin/vite.js preview --host 0.0.0.0 --port "$FRONTEND_PORT" \ + >"${LOG_DIR}/frontend.log" 2>&1 & + echo "OMNL banking portal started under ${ROOT} (vite preview :${FRONTEND_PORT})" + else + nohup npx --yes serve -s dist -l "$FRONTEND_PORT" >"${LOG_DIR}/frontend.log" 2>&1 & + echo "OMNL banking portal started under ${ROOT} (serve :${FRONTEND_PORT})" + fi + echo $! >"${LOG_DIR}/frontend.pid" +fi diff --git a/scripts/lib/deployment/dotenv.sh b/scripts/lib/deployment/dotenv.sh index d022b01..92856b3 100644 --- a/scripts/lib/deployment/dotenv.sh +++ b/scripts/lib/deployment/dotenv.sh @@ -27,6 +27,34 @@ _dep_dotenv_source() { (( had_nounset )) && set -u } +_dep_dotenv_defaults() { + local f="$1" + [[ -f "$f" ]] || return 0 + local had_nounset=0 line key value + if [[ $- == *u* ]]; then + had_nounset=1 + set +u + fi + while IFS= read -r line || [[ -n "$line" ]]; do + line="${line//$'\r'/}" + [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue + [[ "$line" == export\ * ]] && line="${line#export }" + [[ "$line" == *=* ]] || continue + key="${line%%=*}" + value="${line#*=}" + [[ "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue + [[ -n "${!key:-}" ]] && continue + value="${value%$'\n'}" + if [[ "$value" =~ ^\".*\"$ ]]; then + value="${value:1:${#value}-2}" + elif [[ "$value" =~ ^\'.*\'$ ]]; then + value="${value:1:${#value}-2}" + fi + export "$key=$value" + done < "$f" + (( had_nounset )) && set -u +} + _dep_export_from_private_key() { [[ -n "${DEPLOYER_ADDRESS:-}" || -z "${PRIVATE_KEY:-}" ]] && return 0 command -v cast >/dev/null 2>&1 || return 0 @@ -97,7 +125,7 @@ load_deployment_env() { [[ -n "${ENV_FILE:-}" && -f "${ENV_FILE}" ]] && dotenv="${ENV_FILE}" _dep_dotenv_source "$dotenv" if [[ -z "${ENV_FILE:-}" ]]; then - _dep_dotenv_source "${root}/smom-dbis-138/.env" + _dep_dotenv_defaults "${root}/smom-dbis-138/.env" fi local secure_secrets_file="${SECURE_SECRETS_FILE:-$HOME/.secure-secrets/private-keys.env}" if [[ -f "$secure_secrets_file" ]]; then diff --git a/services/token-aggregation/src/api/server.ts b/services/token-aggregation/src/api/server.ts index af603ff..5d10745 100644 --- a/services/token-aggregation/src/api/server.ts +++ b/services/token-aggregation/src/api/server.ts @@ -165,6 +165,19 @@ export class ApiServer { }, }); } catch (error) { + if (this.resolveFeatureFlag('TOKEN_AGGREGATION_DB_OPTIONAL', false)) { + res.json({ + status: 'degraded', + mode: 'database-optional', + timestamp: new Date().toISOString(), + services: { + database: 'unavailable', + indexer: this.indexerEnabled ? 'running' : 'disabled', + }, + error: error instanceof Error ? error.message : 'Unknown error', + }); + return; + } res.status(503).json({ status: 'unhealthy', timestamp: new Date().toISOString(), -- 2.34.1 From 6dcadc86a2a2c4687a3b73426e0f5c08a54f795f Mon Sep 17 00:00:00 2001 From: defiQUG Date: Tue, 7 Jul 2026 09:57:01 -0700 Subject: [PATCH 03/10] chore(omnl): consolidate portal and domain routing updates Capture non-secret OMNL portal, journal, chain, and nginx routing updates from the operator cleanup session while leaving generated auth includes unstaged. Co-authored-by: Cursor --- .../reserve-manager/go/reserve_manager.go | 5 +- config/omnl-journal-matrix.json | 148 +++++++++++++----- config/omnl-production-domains.v1.json | 17 ++ config/omnl-supported-chains.v1.json | 2 +- deploy/nginx/omnl-common-locations.conf | 11 ++ deploy/nginx/omnl-domains.conf | 16 +- fabric/chaincode/bridge/bridge.go | 1 - frontend-dapp/ENV_SETUP_COMPLETE.md | 2 +- frontend-dapp/package.json | 5 + .../src/components/layout/Layout.tsx | 12 +- 10 files changed, 159 insertions(+), 60 deletions(-) diff --git a/chaincode/reserve-manager/go/reserve_manager.go b/chaincode/reserve-manager/go/reserve_manager.go index 3c4aec7..6ff154b 100644 --- a/chaincode/reserve-manager/go/reserve_manager.go +++ b/chaincode/reserve-manager/go/reserve_manager.go @@ -100,9 +100,8 @@ func (s *ReserveManagerContract) CreateReserve(ctx contractapi.TransactionContex return fmt.Errorf("reserve %s already exists", request.ReserveID) } - // Parse total amount - totalAmount, err := strconv.ParseFloat(request.TotalAmount, 64) - if err != nil { + // Validate total amount parses as a number + if _, err := strconv.ParseFloat(request.TotalAmount, 64); err != nil { return fmt.Errorf("invalid total amount: %v", err) } diff --git a/config/omnl-journal-matrix.json b/config/omnl-journal-matrix.json index 6b3ab4d..6019c2d 100644 --- a/config/omnl-journal-matrix.json +++ b/config/omnl-journal-matrix.json @@ -1,5 +1,5 @@ { - "description": "IPSAS double-entry journal matrix for OMNL Hybx (Fineract). Every entry debits one GL and credits a distinct GL. Phase C interoffice: HO leg 2100→2410, branch leg 1410→2100. Served path: smom-dbis-138/config/omnl-journal-matrix.json (override OMNL_JOURNAL_MATRIX_PATH).", + "description": "IPSAS double-entry journal matrix for OMNL Hybx (Fineract). Every entry debits one GL and credits a distinct GL. Phase C interoffice: HO leg 2100\u21922410, branch leg 1410\u21922100. Served path: smom-dbis-138/config/omnl-journal-matrix.json (override OMNL_JOURNAL_MATRIX_PATH).", "source": "OMNL_IPSAS_COMPLIANCE_MATRIX", "currencyCode": "USD", "dateFormat": "yyyy-MM-dd", @@ -13,7 +13,7 @@ "debitGlCode": "1000", "creditGlCode": "2000", "amount": 900000000000, - "narrative": "Opening Balance Migration (Head Office) — Dr settlement asset / Cr M1 central pool", + "narrative": "Opening Balance Migration (Head Office) \u2014 Dr settlement asset / Cr M1 central pool", "ipsasRef": "IPSAS 3, 28", "moneyLayer": "M1" }, @@ -23,7 +23,7 @@ "debitGlCode": "1050", "creditGlCode": "2000", "amount": 250000000000, - "narrative": "Treasury Conversion — Dr M0 reserve / Cr M1 central pool (Head Office)", + "narrative": "Treasury Conversion \u2014 Dr M0 reserve / Cr M1 central pool (Head Office)", "ipsasRef": "IPSAS 28, 29", "moneyLayer": "M0" }, @@ -33,7 +33,7 @@ "debitGlCode": "2100", "creditGlCode": "2410", "amount": 2900000000, - "narrative": "Phase C HO leg — Shamrayan Available (M1) Office 2: Dr M1 circulating / Cr due to branch", + "narrative": "Phase C HO leg \u2014 Shamrayan Available (M1) Office 2: Dr M1 circulating / Cr due to branch", "ipsasRef": "IPSAS 28", "moneyLayer": "M1" }, @@ -43,7 +43,7 @@ "debitGlCode": "1410", "creditGlCode": "2100", "amount": 2900000000, - "narrative": "Phase C branch leg — Shamrayan Available (M1) Office 2: Dr due from HO / Cr M1 circulating", + "narrative": "Phase C branch leg \u2014 Shamrayan Available (M1) Office 2: Dr due from HO / Cr M1 circulating", "ipsasRef": "IPSAS 28", "moneyLayer": "M1" }, @@ -53,7 +53,7 @@ "debitGlCode": "2000", "creditGlCode": "2100", "amount": 2100000000, - "narrative": "Shamrayan Restricted — Dr M1 central pool / Cr M1 restricted escrow (Office 2)", + "narrative": "Shamrayan Restricted \u2014 Dr M1 central pool / Cr M1 restricted escrow (Office 2)", "ipsasRef": "IPSAS 28", "moneyLayer": "M1" }, @@ -63,7 +63,7 @@ "debitGlCode": "2000", "creditGlCode": "2100", "amount": 350000000000, - "narrative": "HYBX Capitalization Escrow — Dr M1 central pool / Cr M1 restricted (Office 3)", + "narrative": "HYBX Capitalization Escrow \u2014 Dr M1 central pool / Cr M1 restricted (Office 3)", "ipsasRef": "IPSAS 28", "moneyLayer": "M1" }, @@ -73,7 +73,7 @@ "debitGlCode": "2100", "creditGlCode": "2410", "amount": 5000000000, - "narrative": "Phase C HO leg — TAJ Allocation (M1) Office 4", + "narrative": "Phase C HO leg \u2014 TAJ Allocation (M1) Office 4", "ipsasRef": "IPSAS 28", "moneyLayer": "M1" }, @@ -83,7 +83,7 @@ "debitGlCode": "1410", "creditGlCode": "2100", "amount": 5000000000, - "narrative": "Phase C branch leg — TAJ Allocation (M1) Office 4", + "narrative": "Phase C branch leg \u2014 TAJ Allocation (M1) Office 4", "ipsasRef": "IPSAS 28", "moneyLayer": "M1" }, @@ -93,7 +93,7 @@ "debitGlCode": "2100", "creditGlCode": "2410", "amount": 5000000000, - "narrative": "Phase C HO leg — Aseret Allocation (M1) Office 5", + "narrative": "Phase C HO leg \u2014 Aseret Allocation (M1) Office 5", "ipsasRef": "IPSAS 28", "moneyLayer": "M1" }, @@ -103,7 +103,7 @@ "debitGlCode": "1410", "creditGlCode": "2100", "amount": 5000000000, - "narrative": "Phase C branch leg — Aseret Allocation (M1) Office 5", + "narrative": "Phase C branch leg \u2014 Aseret Allocation (M1) Office 5", "ipsasRef": "IPSAS 28", "moneyLayer": "M1" }, @@ -113,7 +113,7 @@ "debitGlCode": "2100", "creditGlCode": "2410", "amount": 5000000000, - "narrative": "Phase C HO leg — Mann Li Allocation (M1) Office 6", + "narrative": "Phase C HO leg \u2014 Mann Li Allocation (M1) Office 6", "ipsasRef": "IPSAS 28", "moneyLayer": "M1" }, @@ -123,7 +123,7 @@ "debitGlCode": "1410", "creditGlCode": "2100", "amount": 5000000000, - "narrative": "Phase C branch leg — Mann Li Allocation (M1) Office 6", + "narrative": "Phase C branch leg \u2014 Mann Li Allocation (M1) Office 6", "ipsasRef": "IPSAS 28", "moneyLayer": "M1" }, @@ -133,7 +133,7 @@ "debitGlCode": "2100", "creditGlCode": "2410", "amount": 50000000000, - "narrative": "Phase C HO leg — OSJ Allocation (M1) Office 7", + "narrative": "Phase C HO leg \u2014 OSJ Allocation (M1) Office 7", "ipsasRef": "IPSAS 28", "moneyLayer": "M1" }, @@ -143,7 +143,7 @@ "debitGlCode": "1410", "creditGlCode": "2100", "amount": 50000000000, - "narrative": "Phase C branch leg — OSJ Allocation (M1) Office 7", + "narrative": "Phase C branch leg \u2014 OSJ Allocation (M1) Office 7", "ipsasRef": "IPSAS 28", "moneyLayer": "M1" }, @@ -153,7 +153,7 @@ "debitGlCode": "2100", "creditGlCode": "2410", "amount": 50000000000, - "narrative": "Phase C HO leg — Alltra Allocation (M1) Office 8", + "narrative": "Phase C HO leg \u2014 Alltra Allocation (M1) Office 8", "ipsasRef": "IPSAS 28", "moneyLayer": "M1" }, @@ -163,7 +163,7 @@ "debitGlCode": "1410", "creditGlCode": "2100", "amount": 50000000000, - "narrative": "Phase C branch leg — Alltra Allocation (M1) Office 8", + "narrative": "Phase C branch leg \u2014 Alltra Allocation (M1) Office 8", "ipsasRef": "IPSAS 28", "moneyLayer": "M1" }, @@ -173,7 +173,7 @@ "debitGlCode": "2100", "creditGlCode": "2410", "amount": 1000000000, - "narrative": "HOSPITALLERS-24 opening HO leg — Dr M1 circulating / Cr due to Office 24", + "narrative": "HOSPITALLERS-24 opening HO leg \u2014 Dr M1 circulating / Cr due to Office 24", "ipsasRef": "IPSAS 28", "moneyLayer": "M1" }, @@ -183,19 +183,80 @@ "debitGlCode": "1410", "creditGlCode": "2100", "amount": 1000000000, - "narrative": "HOSPITALLERS-24 opening branch leg — Dr due from HO / Cr M1 circulating (live $1B post)", + "narrative": "HOSPITALLERS-24 opening branch leg \u2014 Dr due from HO / Cr M1 circulating (live $1B post)", "ipsasRef": "IPSAS 28", "moneyLayer": "M1" }, + { + "memo": "T-H24-S1-HO", + "officeId": 1, + "debitGlCode": "2100", + "creditGlCode": "2410", + "amount": 100000000000, + "narrative": "HOSPITALLERS-24 supplement S1 HO leg", + "ipsasRef": "IPSAS 28, 29", + "moneyLayer": "M1" + }, + { + "memo": "T-H24-S1-BR", + "officeId": 24, + "debitGlCode": "1410", + "creditGlCode": "2100", + "amount": 100000000000, + "narrative": "HOSPITALLERS-24 supplement S1 branch leg", + "ipsasRef": "IPSAS 28, 29", + "moneyLayer": "M1" + }, + { + "memo": "T-H24-S2-HO", + "officeId": 1, + "debitGlCode": "2100", + "creditGlCode": "2410", + "amount": 100000000000, + "narrative": "HOSPITALLERS-24 supplement S2 HO leg", + "ipsasRef": "IPSAS 28, 29", + "moneyLayer": "M1" + }, + { + "memo": "T-H24-S2-BR", + "officeId": 24, + "debitGlCode": "1410", + "creditGlCode": "2100", + "amount": 100000000000, + "narrative": "HOSPITALLERS-24 supplement S2 branch leg", + "ipsasRef": "IPSAS 28, 29", + "moneyLayer": "M1" + }, + { + "memo": "T-H24-S3-HO", + "officeId": 1, + "debitGlCode": "2100", + "creditGlCode": "2410", + "amount": 99000000000, + "narrative": "HOSPITALLERS-24 supplement S3 HO leg", + "ipsasRef": "IPSAS 28, 29", + "moneyLayer": "M1" + }, + { + "memo": "T-H24-S3-BR", + "officeId": 24, + "debitGlCode": "1410", + "creditGlCode": "2100", + "amount": 99000000000, + "narrative": "HOSPITALLERS-24 supplement S3 branch leg", + "ipsasRef": "IPSAS 28, 29", + "moneyLayer": "M1" + }, { "memo": "T-M1M2", "officeId": 24, "debitGlCode": "2100", "creditGlCode": "2200", "amount": 0, - "narrative": "M1→M2 broad-money conversion — Dr M1 circulating / Cr M2 broad money", + "narrative": "M1\u2192M2 broad-money conversion \u2014 Dr M1 circulating / Cr M2 broad money", "ipsasRef": "IPSAS 28, 29", - "moneyLayer": "M2" + "moneyLayer": "M2", + "variableAmount": true }, { "memo": "T-M2M3", @@ -203,9 +264,10 @@ "debitGlCode": "2200", "creditGlCode": "2300", "amount": 0, - "narrative": "M2→M3 token load — Dr M2 broad money / Cr M3 on-chain token liability", + "narrative": "M2\u2192M3 token load \u2014 Dr M2 broad money / Cr M3 on-chain token liability", "ipsasRef": "IPSAS 28, 29", - "moneyLayer": "M3" + "moneyLayer": "M3", + "variableAmount": true }, { "memo": "T-M2M4", @@ -213,9 +275,10 @@ "debitGlCode": "2200", "creditGlCode": "1000", "amount": 0, - "narrative": "M2→M4 SWIFT/RTGS outbound — Dr M2 broad money / Cr settlement suspense (GL 1000)", + "narrative": "M2\u2192M4 SWIFT/RTGS outbound \u2014 Dr M2 broad money / Cr settlement suspense (GL 1000)", "ipsasRef": "IPSAS 2, 28", - "moneyLayer": "M4" + "moneyLayer": "M4", + "variableAmount": true }, { "memo": "T-M1M4", @@ -223,9 +286,10 @@ "debitGlCode": "2100", "creditGlCode": "1000", "amount": 0, - "narrative": "M1→M4 SWIFT/RTGS outbound — Dr M1 circulating / Cr settlement suspense (GL 1000)", + "narrative": "M1\u2192M4 SWIFT/RTGS outbound \u2014 Dr M1 circulating / Cr settlement suspense (GL 1000)", "ipsasRef": "IPSAS 2, 28", - "moneyLayer": "M4" + "moneyLayer": "M4", + "variableAmount": true }, { "memo": "T-M4-IN", @@ -233,9 +297,10 @@ "debitGlCode": "1000", "creditGlCode": "2200", "amount": 0, - "narrative": "M4 inbound SWIFT settled — Dr settlement suspense / Cr M2 broad money", + "narrative": "M4 inbound SWIFT settled \u2014 Dr settlement suspense / Cr M2 broad money", "ipsasRef": "IPSAS 2, 28", - "moneyLayer": "M4" + "moneyLayer": "M4", + "variableAmount": true }, { "memo": "T-SBLC", @@ -243,9 +308,10 @@ "debitGlCode": "1410", "creditGlCode": "2100", "amount": 0, - "narrative": "SBLC trade finance — Dr inter-office receivable / Cr M1", + "narrative": "SBLC trade finance \u2014 Dr inter-office receivable / Cr M1", "ipsasRef": "IPSAS 28, 29", - "moneyLayer": "M4" + "moneyLayer": "M4", + "variableAmount": true }, { "memo": "T-DLC", @@ -253,9 +319,10 @@ "debitGlCode": "1410", "creditGlCode": "1000", "amount": 0, - "narrative": "Documentary LC — Dr contingent receivable / Cr M4 settlement suspense", + "narrative": "Documentary LC \u2014 Dr contingent receivable / Cr M4 settlement suspense", "ipsasRef": "IPSAS 2, 28", - "moneyLayer": "M4" + "moneyLayer": "M4", + "variableAmount": true }, { "memo": "T-BG", @@ -263,9 +330,10 @@ "debitGlCode": "1410", "creditGlCode": "2100", "amount": 0, - "narrative": "Bank guarantee — Dr contingent receivable / Cr M1", + "narrative": "Bank guarantee \u2014 Dr contingent receivable / Cr M1", "ipsasRef": "IPSAS 28, 29", - "moneyLayer": "M4" + "moneyLayer": "M4", + "variableAmount": true }, { "memo": "T-LS", @@ -273,9 +341,10 @@ "debitGlCode": "1000", "creditGlCode": "2200", "amount": 0, - "narrative": "Letter of settlement — Dr M4 suspense / Cr M2", + "narrative": "Letter of settlement \u2014 Dr M4 suspense / Cr M2", "ipsasRef": "IPSAS 2, 28", - "moneyLayer": "M4" + "moneyLayer": "M4", + "variableAmount": true }, { "memo": "SETTLE-NOSTRO", @@ -283,9 +352,10 @@ "debitGlCode": "13010", "creditGlCode": "2000", "amount": 0, - "narrative": "Nostro settlement — Dr FX nostro asset / Cr M1 central pool", + "narrative": "Nostro settlement \u2014 Dr FX nostro asset / Cr M1 central pool", "ipsasRef": "IPSAS 28", - "moneyLayer": "M1" + "moneyLayer": "M1", + "variableAmount": true } ] } diff --git a/config/omnl-production-domains.v1.json b/config/omnl-production-domains.v1.json index 9dc5de1..89bdf74 100644 --- a/config/omnl-production-domains.v1.json +++ b/config/omnl-production-domains.v1.json @@ -48,6 +48,23 @@ "host": "office24.omdnl.org", "product": "office-24", "defaultRoute": "/office-24" + }, + { + "host": "bank.omnl.omdnl.org", + "product": "office-24", + "defaultRoute": "/office-24", + "aliasOf": "office24.omdnl.org", + "dnsZone": "omdnl.org", + "notes": "Cloudflare A + NPMplus; operator-controlled" + }, + { + "host": "bank.omnl.hybxfinance.io", + "product": "office-24", + "defaultRoute": "/office-24", + "aliasOf": "office24.omdnl.org", + "dnsZone": "hybxfinance.io", + "externalOnly": true, + "notes": "HYBX Route53 — optional; use bank.omnl.omdnl.org if operator lacks AWS access" } ] } diff --git a/config/omnl-supported-chains.v1.json b/config/omnl-supported-chains.v1.json index ac04f46..83e2cc2 100644 --- a/config/omnl-supported-chains.v1.json +++ b/config/omnl-supported-chains.v1.json @@ -6,7 +6,7 @@ "primaryChainId": 138, "mirrorChainId": 651940, "settlementOfficeId": 24, - "generatedAt": "2026-06-28T15:22:27.822Z", + "generatedAt": "2026-07-06T00:25:58.964Z", "chains": [ { "chainId": 138, diff --git a/deploy/nginx/omnl-common-locations.conf b/deploy/nginx/omnl-common-locations.conf index e08127f..ec6f588 100644 --- a/deploy/nginx/omnl-common-locations.conf +++ b/deploy/nginx/omnl-common-locations.conf @@ -33,6 +33,17 @@ location /exchange/ { proxy_read_timeout 120s; } +location /assets/ { + proxy_pass http://omnl_frontend; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_buffering off; + proxy_request_buffering off; +} + # token-aggregation HTML consoles (/omnl/compliance, /omnl/dashboard, /omnl/terminal) location /omnl/ { include /etc/nginx/omnl-portal-auth-current.conf.inc; diff --git a/deploy/nginx/omnl-domains.conf b/deploy/nginx/omnl-domains.conf index a88f410..f7797bd 100644 --- a/deploy/nginx/omnl-domains.conf +++ b/deploy/nginx/omnl-domains.conf @@ -28,7 +28,7 @@ server { add_header X-OMNL-Chains "128" always; location = / { - return 302 /trade; + return 302 https://$host/trade; } include /etc/nginx/snippets/omnl-common-locations.conf; @@ -44,7 +44,7 @@ server { add_header X-OMNL-Chains "128" always; location = / { - return 302 /central-bank; + return 302 https://$host/central-bank; } include /etc/nginx/snippets/omnl-common-locations.conf; @@ -60,7 +60,7 @@ server { add_header X-OMNL-Chains "128" always; location = / { - return 302 /central-bank; + return 302 https://$host/central-bank; } include /etc/nginx/snippets/omnl-common-locations.conf; @@ -76,7 +76,7 @@ server { add_header X-OMNL-Chains "128" always; location = / { - return 302 /trade; + return 302 https://$host/trade; } include /etc/nginx/snippets/omnl-common-locations.conf; @@ -92,23 +92,23 @@ server { add_header X-OMNL-Chains "128" always; location = / { - return 302 /swap; + return 302 https://$host/swap; } include /etc/nginx/snippets/omnl-common-locations.conf; } -# Office 24 settlement — office24.omdnl.org +# Office 24 settlement — office24.omdnl.org + operator alias bank.omnl.omdnl.org server { listen 80; listen [::]:80; - server_name office24.omdnl.org; + server_name office24.omdnl.org bank.omnl.omdnl.org; add_header X-OMNL-Product "office-24" always; add_header X-OMNL-Chains "128" always; location = / { - return 302 /office-24; + return 302 https://$host/office-24; } include /etc/nginx/snippets/omnl-common-locations.conf; diff --git a/fabric/chaincode/bridge/bridge.go b/fabric/chaincode/bridge/bridge.go index 569ebea..22ace09 100644 --- a/fabric/chaincode/bridge/bridge.go +++ b/fabric/chaincode/bridge/bridge.go @@ -3,7 +3,6 @@ package main import ( "encoding/json" "fmt" - "strconv" "github.com/hyperledger/fabric-contract-api-go/contractapi" ) diff --git a/frontend-dapp/ENV_SETUP_COMPLETE.md b/frontend-dapp/ENV_SETUP_COMPLETE.md index c22516d..1d7f4b0 100644 --- a/frontend-dapp/ENV_SETUP_COMPLETE.md +++ b/frontend-dapp/ENV_SETUP_COMPLETE.md @@ -12,7 +12,7 @@ Location: `smom-dbis-138/frontend-dapp/.env` 1. **Reown/WalletConnect Project ID** ``` - VITE_WALLETCONNECT_PROJECT_ID=b890bbeeff48275b4a115e2ef105195a + VITE_WALLETCONNECT_PROJECT_ID=56972cec44a77f83975463494711e8fa ``` - Used by wagmi for WalletConnect connector - Configured in `src/config/wagmi.ts` diff --git a/frontend-dapp/package.json b/frontend-dapp/package.json index b81e00d..4cb940f 100644 --- a/frontend-dapp/package.json +++ b/frontend-dapp/package.json @@ -13,6 +13,8 @@ "test:coverage": "vitest --coverage" }, "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", "@safe-global/api-kit": "^4.0.1", "@safe-global/protocol-kit": "^1.3.0", "@tanstack/react-query": "^5.90.21", @@ -22,6 +24,9 @@ "@walletconnect/ethereum-provider": "^2.23.5", "autoprefixer": "^10.4.24", "ethers": "^5.8.0", + "lit": "^3.3.3", + "lit-element": "^4.2.2", + "lit-html": "^3.3.3", "postcss": "^8.5.6", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/frontend-dapp/src/components/layout/Layout.tsx b/frontend-dapp/src/components/layout/Layout.tsx index d55e4c6..e67b3bd 100644 --- a/frontend-dapp/src/components/layout/Layout.tsx +++ b/frontend-dapp/src/components/layout/Layout.tsx @@ -1,14 +1,10 @@ -import { Link, useLocation } from 'react-router-dom' +import { Link, Outlet, useLocation } from 'react-router-dom' import { useRef } from 'react' import WalletConnect from '../wallet/WalletConnect' import WalletDisconnectNotice from '../wallet/WalletDisconnectNotice' import { defaultFrontendExplorerUrl } from '../../config/networks' -interface LayoutProps { - children: React.ReactNode -} - -export default function Layout({ children }: LayoutProps) { +export default function Layout() { const location = useLocation() const userInitiatedDisconnectRef = useRef(false) @@ -73,7 +69,9 @@ export default function Layout({ children }: LayoutProps) { -
{children}
+
+ +