diff --git a/config/offices/office-24-settlement.v1.json b/config/offices/office-24-settlement.v1.json index 2f91f8b..770b3f1 100644 --- a/config/offices/office-24-settlement.v1.json +++ b/config/offices/office-24-settlement.v1.json @@ -24,7 +24,7 @@ "rails": { "swift": { "enabled": true, "listenerUrl": "http://192.168.11.114:8788" }, "hybx": { "enabled": true, "sidecarUrl": "http://192.168.11.89:8080" }, - "chain138": { "enabled": true, "rpcEnv": "RPC_URL_138", "defaultLineId": "USD-M1" } + "chain138": { "enabled": true, "rpcEnv": "RPC_URL_138", "defaultLineId": "USD-M2" } }, "omnlLegalName": "ORGANISATION MONDIALE DU NUMERIQUE L.P.B.C.", "omnlLei": "98450070C57395F6B906" diff --git a/config/omnl-customer-security.production.v1.json b/config/omnl-customer-security.production.v1.json index b356c5b..c09a5a8 100644 --- a/config/omnl-customer-security.production.v1.json +++ b/config/omnl-customer-security.production.v1.json @@ -9,7 +9,7 @@ "rateLimits": { "customerPerMinute": 30, "superAdminPerMinute": 120, - "anonymousPerMinute": 0 + "anonymousPerMinute": 30 }, "corsOrigins": [ "https://secure.d-bis.org", @@ -24,7 +24,11 @@ "/health", "/health/ledger", "/money-supply/public", - "/public/money-supply" + "/public/money-supply", + "/office", + "/tokens/m2", + "/chains", + "/chains/active" ], "notes": "Customer keys: OMNL_CUSTOMER_API_KEYS=key:read|trade or OMNL_CUSTOMER_API_KEY_1..3. Super-admin keys: OMNL_SUPER_ADMIN_*_KEY in config/omnl-super-admins.v1.json. Portal browsers use nginx-injected auth — never VITE_OMNL_API_KEY." } diff --git a/deploy/nginx/omnl-common-locations.conf b/deploy/nginx/omnl-common-locations.conf index 47420cf..8fd30c9 100644 --- a/deploy/nginx/omnl-common-locations.conf +++ b/deploy/nginx/omnl-common-locations.conf @@ -19,7 +19,6 @@ location /settlement/ { 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_set_header Authorization $http_authorization; proxy_read_timeout 120s; } diff --git a/packages/integration-foundation/dist/omnl/omnl-api-auth.d.ts b/packages/integration-foundation/dist/omnl/omnl-api-auth.d.ts index 41ae9fb..34462b3 100644 --- a/packages/integration-foundation/dist/omnl/omnl-api-auth.d.ts +++ b/packages/integration-foundation/dist/omnl/omnl-api-auth.d.ts @@ -8,6 +8,7 @@ export type OmnlPrincipal = { export type SuperAdminEntry = { id: string; username: string; + email?: string; displayName: string; portalCtid: number; portalName: string; @@ -28,6 +29,7 @@ export type CustomerSecurityPolicy = { superAdminPerMinute: number; anonymousPerMinute: number; }; + publicEndpoints?: string[]; corsOrigins: string[]; }; export declare function loadSuperAdminRegistry(): SuperAdminEntry[]; @@ -42,3 +44,5 @@ export declare function resolveOmnlPrincipal(req: { }, key?: string): OmnlPrincipal | null; export declare function principalHasScope(principal: OmnlPrincipal | null, scope: string): boolean; export declare function isProductionSecurityMode(): boolean; +/** Paths on settlement/exchange routers that skip anonymous rate limits (dashboard read APIs). */ +export declare function isOmnlPublicEndpoint(pathname: string): boolean; diff --git a/packages/integration-foundation/dist/omnl/omnl-api-auth.js b/packages/integration-foundation/dist/omnl/omnl-api-auth.js index d3ff901..f3c3611 100644 --- a/packages/integration-foundation/dist/omnl/omnl-api-auth.js +++ b/packages/integration-foundation/dist/omnl/omnl-api-auth.js @@ -9,6 +9,7 @@ exports.loadCustomerSecurityPolicy = loadCustomerSecurityPolicy; exports.resolveOmnlPrincipal = resolveOmnlPrincipal; exports.principalHasScope = principalHasScope; exports.isProductionSecurityMode = isProductionSecurityMode; +exports.isOmnlPublicEndpoint = isOmnlPublicEndpoint; const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); let superAdminCache = null; @@ -71,7 +72,8 @@ function loadCustomerSecurityPolicy() { requireApiKeyAllExchangeRoutes: true, customerAllowedScopes: ['read', 'trade'], superAdminOnlyScopes: ['settle', 'token_load', 'transfer', 'admin'], - rateLimits: { customerPerMinute: 30, superAdminPerMinute: 120, anonymousPerMinute: 0 }, + rateLimits: { customerPerMinute: 30, superAdminPerMinute: 120, anonymousPerMinute: 30 }, + publicEndpoints: ['/health', '/health/ledger', '/money-supply/public', '/public/money-supply'], corsOrigins: [], }; } @@ -126,3 +128,20 @@ function isProductionSecurityMode() { process.env.OMNL_CUSTOMER_SECURITY_PRODUCTION === '1' || (process.env.NODE_ENV || '').toLowerCase() === 'production'); } +const DEFAULT_PUBLIC_ENDPOINTS = [ + '/health', + '/health/ledger', + '/money-supply/public', + '/public/money-supply', + '/office', + '/tokens/m2', + '/chains', + '/chains/active', +]; +/** Paths on settlement/exchange routers that skip anonymous rate limits (dashboard read APIs). */ +function isOmnlPublicEndpoint(pathname) { + const policy = loadCustomerSecurityPolicy(); + const allowed = policy.publicEndpoints?.length ? policy.publicEndpoints : DEFAULT_PUBLIC_ENDPOINTS; + const path = pathname.split('?')[0] || '/'; + return allowed.some((ep) => path === ep || path.startsWith(`${ep}/`)); +} diff --git a/packages/integration-foundation/src/omnl/omnl-api-auth.ts b/packages/integration-foundation/src/omnl/omnl-api-auth.ts index 5b6d283..f02a283 100644 --- a/packages/integration-foundation/src/omnl/omnl-api-auth.ts +++ b/packages/integration-foundation/src/omnl/omnl-api-auth.ts @@ -35,6 +35,7 @@ export type CustomerSecurityPolicy = { superAdminPerMinute: number; anonymousPerMinute: number; }; + publicEndpoints?: string[]; corsOrigins: string[]; }; @@ -106,7 +107,8 @@ export function loadCustomerSecurityPolicy(): CustomerSecurityPolicy { requireApiKeyAllExchangeRoutes: true, customerAllowedScopes: ['read', 'trade'], superAdminOnlyScopes: ['settle', 'token_load', 'transfer', 'admin'], - rateLimits: { customerPerMinute: 30, superAdminPerMinute: 120, anonymousPerMinute: 0 }, + rateLimits: { customerPerMinute: 30, superAdminPerMinute: 120, anonymousPerMinute: 30 }, + publicEndpoints: ['/health', '/health/ledger', '/money-supply/public', '/public/money-supply'], corsOrigins: [], }; } @@ -172,3 +174,22 @@ export function isProductionSecurityMode(): boolean { (process.env.NODE_ENV || '').toLowerCase() === 'production' ); } + +const DEFAULT_PUBLIC_ENDPOINTS = [ + '/health', + '/health/ledger', + '/money-supply/public', + '/public/money-supply', + '/office', + '/tokens/m2', + '/chains', + '/chains/active', +]; + +/** Paths on settlement/exchange routers that skip anonymous rate limits (dashboard read APIs). */ +export function isOmnlPublicEndpoint(pathname: string): boolean { + const policy = loadCustomerSecurityPolicy(); + const allowed = policy.publicEndpoints?.length ? policy.publicEndpoints : DEFAULT_PUBLIC_ENDPOINTS; + const path = pathname.split('?')[0] || '/'; + return allowed.some((ep) => path === ep || path.startsWith(`${ep}/`)); +} diff --git a/packages/settlement-core/dist/m2-token-registry.d.ts b/packages/settlement-core/dist/m2-token-registry.d.ts index 8dae52a..0a39464 100644 --- a/packages/settlement-core/dist/m2-token-registry.d.ts +++ b/packages/settlement-core/dist/m2-token-registry.d.ts @@ -42,3 +42,4 @@ export declare function mergeExchangeTokensWithM2Registry(baseTokens: { }[], registry: M2TokenRegistryFile): M2TokenCapabilities[]; export declare function findM2TokenByAddress(registry: M2TokenRegistryFile, address: string): M2TokenCapabilities | undefined; export declare function findM2TokenBySymbol(registry: M2TokenRegistryFile, symbol: string): M2TokenCapabilities | undefined; +export declare function findM2TokenByLineId(registry: M2TokenRegistryFile, lineId: string): M2TokenCapabilities | undefined; diff --git a/packages/settlement-core/dist/m2-token-registry.js b/packages/settlement-core/dist/m2-token-registry.js index 3244ab4..10248b1 100644 --- a/packages/settlement-core/dist/m2-token-registry.js +++ b/packages/settlement-core/dist/m2-token-registry.js @@ -6,6 +6,7 @@ exports.withM2Capabilities = withM2Capabilities; exports.mergeExchangeTokensWithM2Registry = mergeExchangeTokensWithM2Registry; exports.findM2TokenByAddress = findM2TokenByAddress; exports.findM2TokenBySymbol = findM2TokenBySymbol; +exports.findM2TokenByLineId = findM2TokenByLineId; const money_supply_1 = require("./money-supply"); const CURRENCY_FROM_SYMBOL = { USD: 'USD', @@ -84,3 +85,7 @@ function findM2TokenBySymbol(registry, symbol) { const s = symbol.trim().toLowerCase(); return registry.tokens.find((t) => t.symbol.toLowerCase() === s); } +function findM2TokenByLineId(registry, lineId) { + const id = lineId.trim().toUpperCase(); + return registry.tokens.find((t) => t.omnlLine.toUpperCase() === id); +} diff --git a/packages/settlement-core/src/m2-token-registry.ts b/packages/settlement-core/src/m2-token-registry.ts index b485908..fb8b5a3 100644 --- a/packages/settlement-core/src/m2-token-registry.ts +++ b/packages/settlement-core/src/m2-token-registry.ts @@ -133,3 +133,11 @@ export function findM2TokenBySymbol( const s = symbol.trim().toLowerCase(); return registry.tokens.find((t) => t.symbol.toLowerCase() === s); } + +export function findM2TokenByLineId( + registry: M2TokenRegistryFile, + lineId: string, +): M2TokenCapabilities | undefined { + const id = lineId.trim().toUpperCase(); + return registry.tokens.find((t) => t.omnlLine.toUpperCase() === id); +} diff --git a/scripts/deployment/activate-omnl-production.sh b/scripts/deployment/activate-omnl-production.sh index 2005350..463cf33 100644 --- a/scripts/deployment/activate-omnl-production.sh +++ b/scripts/deployment/activate-omnl-production.sh @@ -26,20 +26,37 @@ bash scripts/deployment/sync-ali-omnl-banking-portal-lxcs.sh --apply log "=== 4/5 Push Fineract/API env to each LXC ===" if [[ -f "$ENV_FILE" ]]; then for CTID in 5825 5826 5827 5828; do - bash scripts/deployment/push-portal-env-to-lxc.sh "$CTID" || log "WARN: env push failed for CT $CTID" - omnl_pve_ssh "pct exec ${CTID} -- bash ${OMNL_PORTAL_ROOT:-/srv/zardasht-portal}/scripts/deployment/start-omnl-banking-portal.sh" 2>/dev/null || true + bash scripts/deployment/push-portal-env-to-lxc.sh "$CTID" + omnl_pve_ssh "pct exec ${CTID} -- bash ${OMNL_PORTAL_ROOT:-/srv/zardasht-portal}/scripts/deployment/start-omnl-banking-portal.sh" 2>/dev/null || log "WARN: portal restart failed for CT $CTID" done else log "WARN: no $ENV_FILE — skip LXC Fineract env" fi +log "=== 4b Super-admin DB seed (control panel) ===" +if [[ -f "$ENV_FILE" ]] && [[ "${SEED_SUPER_ADMIN_USERS:-1}" == "1" ]]; then + set -a + set +u + # shellcheck disable=SC1090 + source "$ENV_FILE" + set -u + set +a + if [[ -n "${DATABASE_URL:-}" ]]; then + node scripts/deployment/seed-omnl-super-admin-users.mjs --stop-services | tee "$HOME/omnl-super-admin-credentials.txt" || log "WARN: super-admin user seed failed" + else + log "WARN: DATABASE_URL unset — skip super-admin user seed" + fi +fi + log "=== 5/5 Office 24 opening journal (activate live ledgers) ===" MATERIAL_OPENING_USD="${OMNL_MATERIAL_OPENING_THRESHOLD:-10000000}" STATUS_FILE="$REPO_DIR/config/omnl-hospitallers-office24-status.v1.json" if [[ "$SEED_JOURNAL" == "1" ]] && [[ -f "$ENV_FILE" ]]; then set -a + set +u # shellcheck disable=SC1090 source "$ENV_FILE" + set -u set +a if [[ -f "$STATUS_FILE" ]] && grep -q '"fundingPosted"[[:space:]]*:[[:space:]]*true' "$STATUS_FILE" 2>/dev/null; then log "Office 24 fundingPosted=true in status SSOT — skip single-leg seed (journal already live)" diff --git a/scripts/deployment/deploy-omnl-bank-production.sh b/scripts/deployment/deploy-omnl-bank-production.sh index 70a1206..8cc2338 100644 --- a/scripts/deployment/deploy-omnl-bank-production.sh +++ b/scripts/deployment/deploy-omnl-bank-production.sh @@ -56,6 +56,9 @@ if [[ -f "$ENV_FILE" ]] && ! grep -q '^OMNL_SUPER_ADMIN_OFFICE24_KEY=' "$ENV_FIL fi if [[ -f "$ENV_FILE" ]]; then + for CTID in 5825 5826 5827 5828; do + bash "$REPO_DIR/scripts/deployment/generate-portal-nginx-auth-snippet.sh" "$CTID" "$REPO_DIR/deploy/nginx/omnl-portal-auth-${CTID}.conf.inc" || true + done bash "$REPO_DIR/scripts/deployment/generate-portal-nginx-auth-snippet.sh" 5827 "$REPO_DIR/deploy/nginx/omnl-portal-auth-current.conf.inc" || true if [[ -f "$REPO_DIR/deploy/nginx/omnl-portal-auth-current.conf.inc" ]]; then sudo cp "$REPO_DIR/deploy/nginx/omnl-portal-auth-current.conf.inc" /etc/nginx/omnl-portal-auth-current.conf.inc 2>/dev/null || \ @@ -122,9 +125,10 @@ start_svc() { OMNL_FINERACT_USERNAME="${OMNL_FINERACT_USERNAME:-}" \ OMNL_FINERACT_PASSWORD="${OMNL_FINERACT_PASSWORD:-}" \ OMNL_PUBLIC_MONEY_SUPPLY="${OMNL_PUBLIC_MONEY_SUPPLY:-1}" \ - SETTLEMENT_ALLOW_HYBX_PRODUCTION="${SETTLEMENT_ALLOW_HYBX_PRODUCTION:-}" \ - SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE="${SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE:-}" \ - OMNL_ALLOW_CHAIN_MINT_EXECUTE="${OMNL_ALLOW_CHAIN_MINT_EXECUTE:-}" \ + 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:-}" \ @@ -152,12 +156,23 @@ sleep 5 log "Health checks..." curl -sf "http://127.0.0.1:3011/api/v1/settlement/health" | head -c 200 || true echo -curl -sf "http://127.0.0.1:3011/api/v1/settlement/money-supply/public" | head -c 300 || true +curl -sf "http://127.0.0.1:3011/api/v1/settlement/money-supply/public" | head -c 400 || true echo curl -sf "http://127.0.0.1:3012/api/v1/exchange/health" | head -c 200 || true echo curl -sf -o /dev/null -w "frontend=%{http_code}\n" "http://127.0.0.1:$FRONTEND_PORT/central-bank" +if [[ -n "${DATABASE_URL:-}" ]] && [[ "${SEED_SUPER_ADMIN_USERS:-1}" == "1" ]]; then + log "Seeding super-admin control-panel users..." + node "$REPO_DIR/scripts/deployment/seed-omnl-super-admin-users.mjs" --stop-services >"$LOG_DIR/super-admin-seed.log" 2>&1 || log "WARN: super-admin seed failed — see $LOG_DIR/super-admin-seed.log" + start_svc token-aggregation services/token-aggregation 3000 "npm run start" + sleep 2 + start_svc settlement-middleware services/settlement-middleware 3011 "npm run start" + sleep 2 + start_svc dbis-exchange services/dbis-exchange 3012 "npm run start" + sleep 2 +fi + log "OMNL Bank deploy complete" log " Central Bank: http://127.0.0.1:$FRONTEND_PORT/central-bank" log " Office 24: http://127.0.0.1:$FRONTEND_PORT/office-24" diff --git a/scripts/deployment/push-portal-env-to-lxc.sh b/scripts/deployment/push-portal-env-to-lxc.sh index ca497b9..3f26157 100644 --- a/scripts/deployment/push-portal-env-to-lxc.sh +++ b/scripts/deployment/push-portal-env-to-lxc.sh @@ -19,12 +19,14 @@ if [[ ! -f "$ENV_FILE" ]]; then fi set -a +set +u # shellcheck disable=SC1090 source "$ENV_FILE" +set -u set +a FRAG="$(mktemp)" -grep -E '^(OMNL_FINERACT_|OMNL_PUBLIC_MONEY_SUPPLY=|OMNL_REQUIRE_API_KEY=|OFFICE24_OPENING_M1_USD=|OMNL_PORTAL_INTERNAL_SECRET=|OMNL_CUSTOMER_SECURITY_PRODUCTION=|JWT_SECRET=)' "$ENV_FILE" >"$FRAG" || true +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" >"$FRAG" || true ADMIN_ENV_KEY="$(node -e " const fs = require('fs'); @@ -46,9 +48,9 @@ elif [[ -n "${OMNL_API_KEY:-}" ]]; then fi if [[ ! -s "$FRAG" ]]; then - log "No Fineract/API keys found in $ENV_FILE — skip CT $CTID" + log "ERROR: No portal env keys matched in $ENV_FILE for CT $CTID" >&2 rm -f "$FRAG" - exit 0 + exit 1 fi log "Pushing portal env keys to CT $CTID..." diff --git a/scripts/deployment/seed-omnl-super-admin-users.mjs b/scripts/deployment/seed-omnl-super-admin-users.mjs index d391b9d..558640f 100644 --- a/scripts/deployment/seed-omnl-super-admin-users.mjs +++ b/scripts/deployment/seed-omnl-super-admin-users.mjs @@ -6,9 +6,9 @@ * Usage: * node scripts/deployment/seed-omnl-super-admin-users.mjs * node scripts/deployment/seed-omnl-super-admin-users.mjs --dry-run + * node scripts/deployment/seed-omnl-super-admin-users.mjs --credentials-only * * Passwords: generated per user unless OMNL_SUPER_ADMIN__PASSWORD env is set. - * Output: one-time credentials printed to stdout (never logged to git). */ import fs from 'fs'; import path from 'path'; @@ -25,6 +25,24 @@ const pg = require('pg'); const configPath = path.join(repoRoot, 'config/omnl-super-admins.v1.json'); const dryRun = process.argv.includes('--dry-run'); const credentialsOnly = process.argv.includes('--credentials-only'); +const stopServices = process.argv.includes('--stop-services'); + +const envFile = process.env.OMNL_BANK_ENV || path.join(repoRoot, '.env'); +if (fs.existsSync(envFile)) { + for (const line of fs.readFileSync(envFile, 'utf8').split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const eq = trimmed.indexOf('='); + if (eq <= 0) continue; + const key = trimmed.slice(0, eq).trim(); + if (process.env[key] !== undefined) continue; + let val = trimmed.slice(eq + 1).trim(); + if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) { + val = val.slice(1, -1); + } + process.env[key] = val; + } +} const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8')); const databaseUrl = process.env.DATABASE_URL; @@ -80,45 +98,59 @@ if (!databaseUrl) { process.exit(1); } +if (stopServices) { + const { execSync } = await import('child_process'); + for (const pattern of ['token-aggregation', 'settlement-middleware', 'dbis-exchange']) { + try { + execSync(`pkill -f "smom-dbis-138/services/${pattern}" || true`, { stdio: 'ignore' }); + } catch { + /* ignore */ + } + } + await new Promise((r) => setTimeout(r, 2000)); +} + const client = new pg.Client({ connectionString: databaseUrl }); -await client.connect(); +try { + await client.connect(); -await client.query(` - CREATE TABLE IF NOT EXISTS admin_users ( - id SERIAL PRIMARY KEY, - username VARCHAR(255) UNIQUE NOT NULL, - email VARCHAR(255), - password_hash TEXT NOT NULL, - role VARCHAR(50) NOT NULL DEFAULT 'admin', - is_active BOOLEAN DEFAULT true, - last_login TIMESTAMPTZ, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() - ); -`); + await client.query(` + CREATE TABLE IF NOT EXISTS admin_users ( + id SERIAL PRIMARY KEY, + username VARCHAR(255) UNIQUE NOT NULL, + email VARCHAR(255), + password_hash TEXT NOT NULL, + role VARCHAR(50) NOT NULL DEFAULT 'admin', + is_active BOOLEAN DEFAULT true, + last_login TIMESTAMPTZ, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() + ); + `); -for (const { admin, password, hash } of rows) { - await client.query( - `INSERT INTO admin_users (username, email, password_hash, role, is_active) - VALUES ($1, $2, $3, 'super_admin', true) - ON CONFLICT (username) DO UPDATE SET - email = EXCLUDED.email, - password_hash = EXCLUDED.password_hash, - role = 'super_admin', - is_active = true, - updated_at = NOW()`, - [admin.username, admin.email, hash], - ); -} - -await client.end(); - -console.log('=== OMNL super_admin accounts (one-time — distribute securely) ==='); -for (const { admin, password } of rows) { - console.log(`${admin.displayName}`); - console.log(` Portal CT ${admin.portalCtid} · ${admin.products.join(', ')}`); - console.log(` Username: ${admin.username}`); - console.log(` Email: ${admin.email}`); - console.log(` Password: ${password}`); - console.log(''); + for (const { admin, password, hash } of rows) { + await client.query( + `INSERT INTO admin_users (username, email, password_hash, role, is_active) + VALUES ($1, $2, $3, 'super_admin', true) + ON CONFLICT (username) DO UPDATE SET + email = EXCLUDED.email, + password_hash = EXCLUDED.password_hash, + role = 'super_admin', + is_active = true, + updated_at = NOW()`, + [admin.username, admin.email, hash], + ); + } + + console.log('=== OMNL super_admin accounts (one-time — distribute securely) ==='); + for (const { admin, password } of rows) { + console.log(`${admin.displayName}`); + console.log(` Portal CT ${admin.portalCtid} · ${admin.products.join(', ')}`); + console.log(` Username: ${admin.username}`); + console.log(` Email: ${admin.email}`); + console.log(` Password: ${password}`); + console.log(''); + } +} finally { + await client.end().catch(() => {}); } diff --git a/scripts/deployment/start-omnl-banking-portal.sh b/scripts/deployment/start-omnl-banking-portal.sh index 8db706f..8f647a2 100644 --- a/scripts/deployment/start-omnl-banking-portal.sh +++ b/scripts/deployment/start-omnl-banking-portal.sh @@ -1,61 +1,92 @@ -#!/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" -mkdir -p "$LOG_DIR" - -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" - -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_API_KEY="${OMNL_API_KEY:-}" \ - 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}" \ - 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 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 diff --git a/services/dbis-exchange/dist/middleware/rate-limit.js b/services/dbis-exchange/dist/middleware/rate-limit.js index 01c0f75..bd10903 100644 --- a/services/dbis-exchange/dist/middleware/rate-limit.js +++ b/services/dbis-exchange/dist/middleware/rate-limit.js @@ -11,6 +11,7 @@ function asHandler(h) { } exports.exchangeRateLimiter = asHandler((0, express_rate_limit_1.default)({ windowMs: 60_000, + skip: (req) => (0, integration_foundation_1.isOmnlPublicEndpoint)(req.path), max: (req) => { if (!(0, integration_foundation_1.isProductionSecurityMode)()) return 200; diff --git a/services/dbis-exchange/src/middleware/rate-limit.ts b/services/dbis-exchange/src/middleware/rate-limit.ts index 07f5a27..7238800 100644 --- a/services/dbis-exchange/src/middleware/rate-limit.ts +++ b/services/dbis-exchange/src/middleware/rate-limit.ts @@ -1,6 +1,11 @@ import rateLimit from 'express-rate-limit'; import type { RequestHandler } from 'express'; -import { isProductionSecurityMode, loadCustomerSecurityPolicy, resolveOmnlPrincipal } from '@dbis/integration-foundation'; +import { + isOmnlPublicEndpoint, + isProductionSecurityMode, + loadCustomerSecurityPolicy, + resolveOmnlPrincipal, +} from '@dbis/integration-foundation'; function asHandler(h: ReturnType): RequestHandler { return h as unknown as RequestHandler; @@ -9,6 +14,7 @@ function asHandler(h: ReturnType): RequestHandler { export const exchangeRateLimiter = asHandler( rateLimit({ windowMs: 60_000, + skip: (req) => isOmnlPublicEndpoint(req.path), max: (req) => { if (!isProductionSecurityMode()) return 200; const policy = loadCustomerSecurityPolicy(); diff --git a/services/settlement-middleware/dist/adapters/fineract.js b/services/settlement-middleware/dist/adapters/fineract.js index 33b78df..c74d624 100644 --- a/services/settlement-middleware/dist/adapters/fineract.js +++ b/services/settlement-middleware/dist/adapters/fineract.js @@ -8,6 +8,7 @@ exports.postJournalEntry = postJournalEntry; exports.resolveGlAccountId = resolveGlAccountId; exports.fetchGlBalances = fetchGlBalances; const axios_1 = __importDefault(require("axios")); +const settlement_store_1 = require("../store/settlement-store"); const GL_HINTS = ['1000', '1050', '1410', '2000', '2100', '2200', '2300']; function fineractBase() { return (process.env.OMNL_FINERACT_BASE_URL || '').replace(/\/$/, ''); @@ -173,28 +174,38 @@ async function fetchGlBalances(officeId) { const base = fineractBase(); if (!base || !process.env.OMNL_FINERACT_PASSWORD) return {}; + let out = {}; try { const { data } = await axios_1.default.get(`${base}/runreports/General Ledger Report`, { headers: authHeaders(), params: { R_officeId: officeId, genericResultSet: false }, timeout: 60000, }); - const out = {}; const rows = Array.isArray(data) ? data : data?.data ?? []; for (const row of rows) { if (row.glCode) out[row.glCode] = String(row.balance ?? 0); } - if (Object.keys(out).length > 0) - return out; + if (Object.keys(out).length === 0) { + out = await fetchGlBalancesFromJournals(officeId); + } } catch { - /* report unavailable on this Fineract — fall through */ - } - try { - return await fetchGlBalancesFromJournals(officeId); - } - catch { - return {}; + try { + out = await fetchGlBalancesFromJournals(officeId); + } + catch { + out = {}; + } } + out.inFlightSwift = sumInFlightSwift().toFixed(2); + return out; +} +function sumInFlightSwift() { + const inFlightPhases = new Set(['HYBX_RAIL_DISPATCHED', 'ISO20022_ARCHIVED', 'CHAIN_MINT_REQUESTED']); + return settlement_store_1.settlementStore + .list(500) + .filter((r) => inFlightPhases.has(r.phase) && + (r.request.rail === 'SWIFT' || r.request.rail === 'RTGS' || r.request.rail === 'SEPA')) + .reduce((sum, r) => sum + (parseFloat(r.request.amount ?? '0') || 0), 0); } diff --git a/services/settlement-middleware/dist/middleware/rate-limit.js b/services/settlement-middleware/dist/middleware/rate-limit.js index 3461f18..d596d77 100644 --- a/services/settlement-middleware/dist/middleware/rate-limit.js +++ b/services/settlement-middleware/dist/middleware/rate-limit.js @@ -11,6 +11,7 @@ function asHandler(h) { } exports.settlementRateLimiter = asHandler((0, express_rate_limit_1.default)({ windowMs: 60_000, + skip: (req) => (0, integration_foundation_1.isOmnlPublicEndpoint)(req.path), max: (req) => { if (!(0, integration_foundation_1.isProductionSecurityMode)()) return 200; diff --git a/services/settlement-middleware/dist/workflows/external-transfer.js b/services/settlement-middleware/dist/workflows/external-transfer.js index 187ba10..69fa49b 100644 --- a/services/settlement-middleware/dist/workflows/external-transfer.js +++ b/services/settlement-middleware/dist/workflows/external-transfer.js @@ -120,6 +120,8 @@ async function processExternalTransfer(input) { const m3Check = (0, settlement_core_1.m3TokenLoadAmount)(loadAmount, snap.M2.broadMoney, snap.M3.tokenLiabilities); if (!fullyBacked || !m3Check.fullyBacked) { record.errors.push(`M2/M3 backing insufficient for token load (${loadAmount}/${req.amount}, M3 headroom ${m3Check.loadAmount})`); + advance('FAILED'); + return record; } const mint = await (0, omnl_1.requestTokenMint)({ lineId: req.convertToCrypto.tokenLineId, diff --git a/services/settlement-middleware/dist/workflows/token-load.js b/services/settlement-middleware/dist/workflows/token-load.js index ae761dd..d48ca15 100644 --- a/services/settlement-middleware/dist/workflows/token-load.js +++ b/services/settlement-middleware/dist/workflows/token-load.js @@ -1,12 +1,38 @@ "use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.processTokenLoad = processTokenLoad; const uuid_1 = require("uuid"); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); const settlement_core_1 = require("@dbis/settlement-core"); const config_1 = require("../config"); const fineract_1 = require("../adapters/fineract"); const omnl_1 = require("../adapters/omnl"); const settlement_store_1 = require("../store/settlement-store"); +function loadTokenRegistry() { + const registryPath = process.env.OMNL_M2_TOKEN_REGISTRY || + path_1.default.resolve(__dirname, '../../../config/omnl-m2-token-registry.v1.json'); + return JSON.parse(fs_1.default.readFileSync(registryPath, 'utf8')); +} +function resolveTokenAddress(req) { + if (req.tokenAddress?.startsWith('0x')) + return req.tokenAddress; + const registry = loadTokenRegistry(); + const byLine = (0, settlement_core_1.findM2TokenByLineId)(registry, req.lineId); + if (byLine?.address.startsWith('0x') && byLine.address !== '0x0000000000000000000000000000000000000000') { + return byLine.address; + } + if (req.symbol) { + const bySym = registry.tokens.find((t) => t.symbol.toLowerCase() === req.symbol.toLowerCase()); + if (bySym?.address.startsWith('0x') && bySym.address !== '0x0000000000000000000000000000000000000000') { + return bySym.address; + } + } + return undefined; +} async function processTokenLoad(input) { const cfg = (0, config_1.loadConfig)(); const profile = (0, config_1.loadOfficeProfile)(); @@ -93,7 +119,7 @@ async function processTokenLoad(input) { settlementRef: record.settlementId, dryRun: !cfg.production.allowChainMintExecute, symbol: req.symbol, - tokenAddress: req.tokenAddress, + tokenAddress: resolveTokenAddress(req), }); advance('CHAIN_MINT_REQUESTED', { chainTxHash: mint.txHash }); advance('SETTLED'); diff --git a/services/settlement-middleware/src/adapters/fineract.ts b/services/settlement-middleware/src/adapters/fineract.ts index 6625c4c..fb3f2c2 100644 --- a/services/settlement-middleware/src/adapters/fineract.ts +++ b/services/settlement-middleware/src/adapters/fineract.ts @@ -1,4 +1,5 @@ import axios from 'axios'; +import { settlementStore } from '../store/settlement-store'; const GL_HINTS = ['1000', '1050', '1410', '2000', '2100', '2200', '2300']; @@ -179,24 +180,39 @@ export async function resolveGlAccountId(glCode: string): Promise { export async function fetchGlBalances(officeId: number): Promise> { const base = fineractBase(); if (!base || !process.env.OMNL_FINERACT_PASSWORD) return {}; + let out: Record = {}; try { const { data } = await axios.get(`${base}/runreports/General Ledger Report`, { headers: authHeaders(), params: { R_officeId: officeId, genericResultSet: false }, timeout: 60000, }); - const out: Record = {}; const rows = Array.isArray(data) ? data : (data as { data?: unknown[] })?.data ?? []; for (const row of rows as { glCode?: string; balance?: number }[]) { if (row.glCode) out[row.glCode] = String(row.balance ?? 0); } - if (Object.keys(out).length > 0) return out; + if (Object.keys(out).length === 0) { + out = await fetchGlBalancesFromJournals(officeId); + } } catch { - /* report unavailable on this Fineract — fall through */ - } - try { - return await fetchGlBalancesFromJournals(officeId); - } catch { - return {}; + try { + out = await fetchGlBalancesFromJournals(officeId); + } catch { + out = {}; + } } + out.inFlightSwift = sumInFlightSwift().toFixed(2); + return out; +} + +function sumInFlightSwift(): number { + const inFlightPhases = new Set(['HYBX_RAIL_DISPATCHED', 'ISO20022_ARCHIVED', 'CHAIN_MINT_REQUESTED']); + return settlementStore + .list(500) + .filter( + (r) => + inFlightPhases.has(r.phase) && + (r.request.rail === 'SWIFT' || r.request.rail === 'RTGS' || r.request.rail === 'SEPA'), + ) + .reduce((sum, r) => sum + (parseFloat(r.request.amount ?? '0') || 0), 0); } diff --git a/services/settlement-middleware/src/middleware/rate-limit.ts b/services/settlement-middleware/src/middleware/rate-limit.ts index b928c9f..3adce65 100644 --- a/services/settlement-middleware/src/middleware/rate-limit.ts +++ b/services/settlement-middleware/src/middleware/rate-limit.ts @@ -1,6 +1,11 @@ import rateLimit from 'express-rate-limit'; import type { RequestHandler } from 'express'; -import { isProductionSecurityMode, loadCustomerSecurityPolicy, resolveOmnlPrincipal } from '@dbis/integration-foundation'; +import { + isOmnlPublicEndpoint, + isProductionSecurityMode, + loadCustomerSecurityPolicy, + resolveOmnlPrincipal, +} from '@dbis/integration-foundation'; function asHandler(h: ReturnType): RequestHandler { return h as unknown as RequestHandler; @@ -9,6 +14,7 @@ function asHandler(h: ReturnType): RequestHandler { export const settlementRateLimiter = asHandler( rateLimit({ windowMs: 60_000, + skip: (req) => isOmnlPublicEndpoint(req.path), max: (req) => { if (!isProductionSecurityMode()) return 200; const policy = loadCustomerSecurityPolicy(); diff --git a/services/settlement-middleware/src/workflows/external-transfer.ts b/services/settlement-middleware/src/workflows/external-transfer.ts index 0b79908..b952249 100644 --- a/services/settlement-middleware/src/workflows/external-transfer.ts +++ b/services/settlement-middleware/src/workflows/external-transfer.ts @@ -142,6 +142,8 @@ export async function processExternalTransfer( record.errors.push( `M2/M3 backing insufficient for token load (${loadAmount}/${req.amount}, M3 headroom ${m3Check.loadAmount})`, ); + advance('FAILED'); + return record; } const mint = await requestTokenMint({ lineId: req.convertToCrypto.tokenLineId, diff --git a/services/settlement-middleware/src/workflows/token-load.ts b/services/settlement-middleware/src/workflows/token-load.ts index 587ee4e..7722eb2 100644 --- a/services/settlement-middleware/src/workflows/token-load.ts +++ b/services/settlement-middleware/src/workflows/token-load.ts @@ -1,9 +1,13 @@ import { v4 as uuidv4 } from 'uuid'; +import fs from 'fs'; +import path from 'path'; import { buildMoneySupplySnapshot, + findM2TokenByLineId, GL_CODES, m2FiatToTokenLoadAmount, m3TokenLoadAmount, + type M2TokenRegistryFile, type TokenLoadRequest, type SettlementRecord, } from '@dbis/settlement-core'; @@ -12,6 +16,29 @@ import { fetchGlBalances, postJournalEntry, resolveGlAccountId } from '../adapte import { requestTokenMint } from '../adapters/omnl'; import { settlementStore } from '../store/settlement-store'; +function loadTokenRegistry(): M2TokenRegistryFile { + const registryPath = + process.env.OMNL_M2_TOKEN_REGISTRY || + path.resolve(__dirname, '../../../config/omnl-m2-token-registry.v1.json'); + return JSON.parse(fs.readFileSync(registryPath, 'utf8')) as M2TokenRegistryFile; +} + +function resolveTokenAddress(req: TokenLoadRequest): string | undefined { + if (req.tokenAddress?.startsWith('0x')) return req.tokenAddress; + const registry = loadTokenRegistry(); + const byLine = findM2TokenByLineId(registry, req.lineId); + if (byLine?.address.startsWith('0x') && byLine.address !== '0x0000000000000000000000000000000000000000') { + return byLine.address; + } + if (req.symbol) { + const bySym = registry.tokens.find((t) => t.symbol.toLowerCase() === req.symbol!.toLowerCase()); + if (bySym?.address.startsWith('0x') && bySym.address !== '0x0000000000000000000000000000000000000000') { + return bySym.address; + } + } + return undefined; +} + export async function processTokenLoad(input: TokenLoadRequest): Promise { const cfg = loadConfig(); const profile = loadOfficeProfile(); @@ -106,7 +133,7 @@ export async function processTokenLoad(input: TokenLoadRequest): Promise; + }; + const id = lineId.trim().toUpperCase(); + const byLine = registry.tokens.find((t) => t.omnlLine.toUpperCase() === id); + if (byLine?.address.startsWith('0x') && byLine.address !== '0x0000000000000000000000000000000000000000') { + return byLine.address; + } + if (symbol) { + const bySym = registry.tokens.find((t) => t.symbol.toLowerCase() === symbol.toLowerCase()); + if (bySym?.address.startsWith('0x') && bySym.address !== '0x0000000000000000000000000000000000000000') { + return bySym.address; + } + } + } catch { + /* registry optional */ + } + return undefined; +} const router = Router(); router.use(omnlRateLimiter); @@ -381,6 +409,9 @@ router.post('/omnl/settlement/token-load', async (req: Request, res: Response) = return; } + if (!tokenAddress?.startsWith('0x')) { + tokenAddress = resolveMintTokenAddress(lineId, symbol, tokenAddress); + } if (!tokenAddress?.startsWith('0x')) { res.status(400).json({ error: 'tokenAddress required for on-chain mint',