From 5dd67e2b1d374b20c2dc3cdd3419613e5a3d5886 Mon Sep 17 00:00:00 2001 From: zaragoza444 Date: Sun, 28 Jun 2026 08:44:42 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20OMNL=20Bank=20online=20production=20?= =?UTF-8?q?=E2=80=94=20128=20chains,=20Central=20Bank=20UI,=20settlement?= =?UTF-8?q?=20stack?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- config/dbis-exchange.production.v1.json | 27 + config/dbis-exchange.v1.json | 30 + config/deployment-omnl.production.env.example | 50 + config/offices/office-24-settlement.v1.json | 28 + config/omnl-m2-token-registry.v1.json | 527 ++++ config/omnl-supported-chains.v1.json | 2320 +++++++++++++++++ .../settlement-middleware.production.v1.json | 35 + config/settlement-middleware.v1.json | 40 + config/trade-finance/instruments.v1.json | 30 + deploy/nginx/frontend-spa.conf | 14 + deploy/nginx/omnl-bank.conf | 70 + docker-compose.omnl-production.yml | 99 + frontend-dapp/Dockerfile.production | 20 + frontend-dapp/src/App.tsx | 32 +- .../src/components/bridge/BridgeButtons.tsx | 128 +- .../components/layout/OmnlProductLayout.tsx | 60 + frontend-dapp/src/config/dex.ts | 71 + frontend-dapp/src/config/supported-chains.ts | 31 + .../central-bank/CentralBankDashboard.tsx | 170 ++ .../central-bank/ChainMatrixPanel.tsx | 100 + .../features/central-bank/useCentralBank.ts | 60 + .../features/office24/Office24Dashboard.tsx | 123 + .../src/features/office24/useOffice24.ts | 42 + .../src/features/swap/DBISSwapPanel.tsx | 298 +++ .../src/features/swap/useDbisSwap.ts | 297 +++ .../src/features/trade/BinanceTradePanel.tsx | 325 +++ frontend-dapp/src/pages/CentralBankPage.tsx | 5 + frontend-dapp/src/pages/Office24Page.tsx | 5 + frontend-dapp/src/pages/SwapPage.tsx | 14 +- frontend-dapp/src/pages/TradePage.tsx | 5 + frontend-dapp/src/styles/omnl-apps.css | 58 + .../settlement-core/dist/chain-registry.d.ts | 39 + .../settlement-core/dist/chain-registry.js | 43 + packages/settlement-core/dist/index.d.ts | 7 + packages/settlement-core/dist/index.js | 23 + .../settlement-core/dist/instruments.d.ts | 13 + packages/settlement-core/dist/instruments.js | 28 + .../dist/m2-token-registry.d.ts | 44 + .../settlement-core/dist/m2-token-registry.js | 86 + .../settlement-core/dist/money-supply.d.ts | 26 + packages/settlement-core/dist/money-supply.js | 74 + .../settlement-core/dist/state-machine.d.ts | 3 + .../settlement-core/dist/state-machine.js | 29 + packages/settlement-core/dist/types.d.ts | 102 + packages/settlement-core/dist/types.js | 3 + packages/settlement-core/dist/verbiage.d.ts | 6 + packages/settlement-core/dist/verbiage.js | 54 + packages/settlement-core/package-lock.json | 47 + packages/settlement-core/package.json | 17 + .../settlement-core/src/chain-registry.ts | 67 + packages/settlement-core/src/index.ts | 7 + packages/settlement-core/src/instruments.ts | 31 + .../settlement-core/src/m2-token-registry.ts | 135 + packages/settlement-core/src/money-supply.ts | 82 + packages/settlement-core/src/state-machine.ts | 26 + packages/settlement-core/src/types.ts | 113 + packages/settlement-core/src/verbiage.ts | 58 + packages/settlement-core/tsconfig.json | 13 + .../deployment/deploy-omnl-bank-production.sh | 100 + scripts/generate-omnl-128-chains.mjs | 142 + services/dbis-exchange/Dockerfile | 19 + .../dbis-exchange/dist/adapters/settlement.js | 111 + .../dist/adapters/token-aggregation.js | 52 + .../dbis-exchange/dist/api/routes/exchange.js | 299 +++ services/dbis-exchange/dist/config.js | 45 + services/dbis-exchange/dist/index.js | 47 + services/dbis-exchange/dist/server.js | 19 + .../dbis-exchange/dist/store/swap-monitor.js | 52 + services/dbis-exchange/package-lock.json | 1527 +++++++++++ services/dbis-exchange/package.json | 28 + .../dbis-exchange/src/adapters/settlement.ts | 159 ++ .../src/adapters/token-aggregation.ts | 76 + .../dbis-exchange/src/api/routes/exchange.ts | 322 +++ services/dbis-exchange/src/config.ts | 75 + services/dbis-exchange/src/index.ts | 10 + services/dbis-exchange/src/server.ts | 14 + .../dbis-exchange/src/store/swap-monitor.ts | 65 + services/dbis-exchange/tsconfig.json | 13 + services/settlement-middleware/Dockerfile | 19 + .../dist/adapters/fineract.js | 52 + .../dist/adapters/hybx-production.js | 51 + .../dist/adapters/omnl.js | 44 + .../dist/adapters/swift.js | 42 + .../dist/api/routes/settlement.js | 276 ++ services/settlement-middleware/dist/config.js | 57 + services/settlement-middleware/dist/index.js | 47 + services/settlement-middleware/dist/server.js | 26 + .../dist/store/settlement-store.js | 47 + .../dist/workflows/external-transfer.js | 150 ++ .../dist/workflows/office-scope.js | 32 + .../dist/workflows/swift-inbound.js | 46 + .../dist/workflows/token-load.js | 105 + .../dist/workflows/transfer.js | 130 + .../settlement-middleware/package-lock.json | 1433 ++++++++++ services/settlement-middleware/package.json | 29 + .../src/adapters/fineract.ts | 53 + .../src/adapters/hybx-production.ts | 60 + .../src/adapters/omnl.ts | 59 + .../src/adapters/swift.ts | 56 + .../src/api/routes/settlement.ts | 271 ++ services/settlement-middleware/src/config.ts | 112 + services/settlement-middleware/src/index.ts | 10 + services/settlement-middleware/src/server.ts | 21 + .../src/store/settlement-store.ts | 46 + .../src/workflows/external-transfer.ts | 171 ++ .../src/workflows/office-scope.ts | 33 + .../src/workflows/swift-inbound.ts | 49 + .../src/workflows/token-load.ts | 115 + .../src/workflows/transfer.ts | 132 + services/settlement-middleware/tsconfig.json | 13 + .../src/parsers/inboundParser.ts | 21 + services/swift-listener/src/types.ts | 1 + .../token-aggregation/src/api/routes/omnl.ts | 47 + 113 files changed, 13223 insertions(+), 98 deletions(-) create mode 100644 config/dbis-exchange.production.v1.json create mode 100644 config/dbis-exchange.v1.json create mode 100644 config/deployment-omnl.production.env.example create mode 100644 config/offices/office-24-settlement.v1.json create mode 100644 config/omnl-m2-token-registry.v1.json create mode 100644 config/omnl-supported-chains.v1.json create mode 100644 config/settlement-middleware.production.v1.json create mode 100644 config/settlement-middleware.v1.json create mode 100644 config/trade-finance/instruments.v1.json create mode 100644 deploy/nginx/frontend-spa.conf create mode 100644 deploy/nginx/omnl-bank.conf create mode 100644 docker-compose.omnl-production.yml create mode 100644 frontend-dapp/Dockerfile.production create mode 100644 frontend-dapp/src/components/layout/OmnlProductLayout.tsx create mode 100644 frontend-dapp/src/config/dex.ts create mode 100644 frontend-dapp/src/config/supported-chains.ts create mode 100644 frontend-dapp/src/features/central-bank/CentralBankDashboard.tsx create mode 100644 frontend-dapp/src/features/central-bank/ChainMatrixPanel.tsx create mode 100644 frontend-dapp/src/features/central-bank/useCentralBank.ts create mode 100644 frontend-dapp/src/features/office24/Office24Dashboard.tsx create mode 100644 frontend-dapp/src/features/office24/useOffice24.ts create mode 100644 frontend-dapp/src/features/swap/DBISSwapPanel.tsx create mode 100644 frontend-dapp/src/features/swap/useDbisSwap.ts create mode 100644 frontend-dapp/src/features/trade/BinanceTradePanel.tsx create mode 100644 frontend-dapp/src/pages/CentralBankPage.tsx create mode 100644 frontend-dapp/src/pages/Office24Page.tsx create mode 100644 frontend-dapp/src/pages/TradePage.tsx create mode 100644 frontend-dapp/src/styles/omnl-apps.css create mode 100644 packages/settlement-core/dist/chain-registry.d.ts create mode 100644 packages/settlement-core/dist/chain-registry.js create mode 100644 packages/settlement-core/dist/index.d.ts create mode 100644 packages/settlement-core/dist/index.js create mode 100644 packages/settlement-core/dist/instruments.d.ts create mode 100644 packages/settlement-core/dist/instruments.js create mode 100644 packages/settlement-core/dist/m2-token-registry.d.ts create mode 100644 packages/settlement-core/dist/m2-token-registry.js create mode 100644 packages/settlement-core/dist/money-supply.d.ts create mode 100644 packages/settlement-core/dist/money-supply.js create mode 100644 packages/settlement-core/dist/state-machine.d.ts create mode 100644 packages/settlement-core/dist/state-machine.js create mode 100644 packages/settlement-core/dist/types.d.ts create mode 100644 packages/settlement-core/dist/types.js create mode 100644 packages/settlement-core/dist/verbiage.d.ts create mode 100644 packages/settlement-core/dist/verbiage.js create mode 100644 packages/settlement-core/package-lock.json create mode 100644 packages/settlement-core/package.json create mode 100644 packages/settlement-core/src/chain-registry.ts create mode 100644 packages/settlement-core/src/index.ts create mode 100644 packages/settlement-core/src/instruments.ts create mode 100644 packages/settlement-core/src/m2-token-registry.ts create mode 100644 packages/settlement-core/src/money-supply.ts create mode 100644 packages/settlement-core/src/state-machine.ts create mode 100644 packages/settlement-core/src/types.ts create mode 100644 packages/settlement-core/src/verbiage.ts create mode 100644 packages/settlement-core/tsconfig.json create mode 100644 scripts/deployment/deploy-omnl-bank-production.sh create mode 100644 scripts/generate-omnl-128-chains.mjs create mode 100644 services/dbis-exchange/Dockerfile create mode 100644 services/dbis-exchange/dist/adapters/settlement.js create mode 100644 services/dbis-exchange/dist/adapters/token-aggregation.js create mode 100644 services/dbis-exchange/dist/api/routes/exchange.js create mode 100644 services/dbis-exchange/dist/config.js create mode 100644 services/dbis-exchange/dist/index.js create mode 100644 services/dbis-exchange/dist/server.js create mode 100644 services/dbis-exchange/dist/store/swap-monitor.js create mode 100644 services/dbis-exchange/package-lock.json create mode 100644 services/dbis-exchange/package.json create mode 100644 services/dbis-exchange/src/adapters/settlement.ts create mode 100644 services/dbis-exchange/src/adapters/token-aggregation.ts create mode 100644 services/dbis-exchange/src/api/routes/exchange.ts create mode 100644 services/dbis-exchange/src/config.ts create mode 100644 services/dbis-exchange/src/index.ts create mode 100644 services/dbis-exchange/src/server.ts create mode 100644 services/dbis-exchange/src/store/swap-monitor.ts create mode 100644 services/dbis-exchange/tsconfig.json create mode 100644 services/settlement-middleware/Dockerfile create mode 100644 services/settlement-middleware/dist/adapters/fineract.js create mode 100644 services/settlement-middleware/dist/adapters/hybx-production.js create mode 100644 services/settlement-middleware/dist/adapters/omnl.js create mode 100644 services/settlement-middleware/dist/adapters/swift.js create mode 100644 services/settlement-middleware/dist/api/routes/settlement.js create mode 100644 services/settlement-middleware/dist/config.js create mode 100644 services/settlement-middleware/dist/index.js create mode 100644 services/settlement-middleware/dist/server.js create mode 100644 services/settlement-middleware/dist/store/settlement-store.js create mode 100644 services/settlement-middleware/dist/workflows/external-transfer.js create mode 100644 services/settlement-middleware/dist/workflows/office-scope.js create mode 100644 services/settlement-middleware/dist/workflows/swift-inbound.js create mode 100644 services/settlement-middleware/dist/workflows/token-load.js create mode 100644 services/settlement-middleware/dist/workflows/transfer.js create mode 100644 services/settlement-middleware/package-lock.json create mode 100644 services/settlement-middleware/package.json create mode 100644 services/settlement-middleware/src/adapters/fineract.ts create mode 100644 services/settlement-middleware/src/adapters/hybx-production.ts create mode 100644 services/settlement-middleware/src/adapters/omnl.ts create mode 100644 services/settlement-middleware/src/adapters/swift.ts create mode 100644 services/settlement-middleware/src/api/routes/settlement.ts create mode 100644 services/settlement-middleware/src/config.ts create mode 100644 services/settlement-middleware/src/index.ts create mode 100644 services/settlement-middleware/src/server.ts create mode 100644 services/settlement-middleware/src/store/settlement-store.ts create mode 100644 services/settlement-middleware/src/workflows/external-transfer.ts create mode 100644 services/settlement-middleware/src/workflows/office-scope.ts create mode 100644 services/settlement-middleware/src/workflows/swift-inbound.ts create mode 100644 services/settlement-middleware/src/workflows/token-load.ts create mode 100644 services/settlement-middleware/src/workflows/transfer.ts create mode 100644 services/settlement-middleware/tsconfig.json diff --git a/config/dbis-exchange.production.v1.json b/config/dbis-exchange.production.v1.json new file mode 100644 index 0000000..7a592d2 --- /dev/null +++ b/config/dbis-exchange.production.v1.json @@ -0,0 +1,27 @@ +{ + "$schema": "DBIS Exchange — production (128-chain OMNL Bank)", + "version": "1.0.0", + "chainId": 138, + "brandName": "DBIS Exchange", + "officeId": 24, + "settlementOfficeExternalId": "HOSPITALLERS-ALI-IRAQ-IRAN", + "chainRegistry": "config/omnl-supported-chains.v1.json", + "maxSupportedChains": 128, + "routers": { + "enhancedSwapRouterV2": "0xa421706768aeb7fafa2d912c5e10824ef3437ad4", + "uniswapV2": "0x3019A7fDc76ba7F64F18d78e66842760037ee638", + "sushiV2": "0xB37b93D38559f53b62ab020A14919f2630a1aE34" + }, + "factory": { + "uniswapV2": "0x0C30F6e67Ab3667fCc2f5CEA8e274ef1FB920279" + }, + "tokens": [], + "defaultSlippageBps": 50, + "production": { + "requireApiKey": true, + "monitorSwaps": true, + "postSwapSettlementAudit": true, + "onlineBankMode": true + } +} + \ No newline at end of file diff --git a/config/dbis-exchange.v1.json b/config/dbis-exchange.v1.json new file mode 100644 index 0000000..e9db10e --- /dev/null +++ b/config/dbis-exchange.v1.json @@ -0,0 +1,30 @@ +{ + "$schema": "DBIS Exchange — Pancake-style DEX on Chain 138 + OMNL Office 24", + "version": "1.0.0", + "chainId": 138, + "brandName": "DBIS Exchange", + "officeId": 24, + "settlementOfficeExternalId": "HOSPITALLERS-ALI-IRAQ-IRAN", + "routers": { + "enhancedSwapRouterV2": "0xa421706768aeb7fafa2d912c5e10824ef3437ad4", + "uniswapV2": "0x3019A7fDc76ba7F64F18d78e66842760037ee638", + "sushiV2": "0xB37b93D38559f53b62ab020A14919f2630a1aE34" + }, + "factory": { + "uniswapV2": "0x0C30F6e67Ab3667fCc2f5CEA8e274ef1FB920279" + }, + "tokens": [ + { "symbol": "ETH", "name": "Ether", "address": "0x0000000000000000000000000000000000000000", "decimals": 18, "native": true }, + { "symbol": "WETH", "name": "Wrapped Ether", "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "decimals": 18 }, + { "symbol": "USDT", "name": "Tether USD", "address": "0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1", "decimals": 6 }, + { "symbol": "USDC", "name": "USD Coin", "address": "0x71D6687F38b93CCad569Fa6352c876eea967201b", "decimals": 6 }, + { "symbol": "cUSDT", "name": "Compliant USDT", "address": "0x93E66202A11B1772E55407B32B44e5Cd8eda7f22", "decimals": 6, "omnlLine": "USD-M2", "moneyLayer": "M2" }, + { "symbol": "cUSDC", "name": "Compliant USDC", "address": "0xf22258f57794CC8E06237084b353Ab30fFfa640b", "decimals": 6, "omnlLine": "USD-M2", "moneyLayer": "M2" } + ], + "defaultSlippageBps": 50, + "production": { + "requireApiKey": false, + "monitorSwaps": true, + "postSwapSettlementAudit": true + } +} diff --git a/config/deployment-omnl.production.env.example b/config/deployment-omnl.production.env.example new file mode 100644 index 0000000..abd7763 --- /dev/null +++ b/config/deployment-omnl.production.env.example @@ -0,0 +1,50 @@ +# OMNL Bank — production environment (copy to .env.production, never commit secrets) + +# === Public URLs (nginx / Cloudflare) === +OMNL_BANK_PUBLIC_URL=https://bank.omnl.hybxfinance.io +VITE_TOKEN_AGGREGATION_URL=https://bank.omnl.hybxfinance.io/api/v1 +VITE_SETTLEMENT_MIDDLEWARE_URL=https://bank.omnl.hybxfinance.io/settlement +VITE_DBIS_EXCHANGE_URL=https://bank.omnl.hybxfinance.io/exchange +VITE_OMNL_API_KEY= + +# === Production configs === +SETTLEMENT_MIDDLEWARE_CONFIG=config/settlement-middleware.production.v1.json +DBIS_EXCHANGE_CONFIG=config/dbis-exchange.production.v1.json +OMNL_SUPPORTED_CHAINS_CONFIG=config/omnl-supported-chains.v1.json +OMNL_M2_TOKEN_REGISTRY=config/omnl-m2-token-registry.v1.json +OMNL_CROSS_CHAIN_CONFIG=config/hybx-omnl-cross-chain-lines.json + +# === Production gates (enable after audit) === +SETTLEMENT_ALLOW_HYBX_PRODUCTION=1 +SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE=1 +OMNL_ALLOW_CHAIN_MINT_EXECUTE=1 + +# === Auth === +OMNL_API_KEY= + +# === Primary chain 138 === +RPC_URL_138=http://192.168.11.221:8545 +CHAIN_138_RPC_URL=http://192.168.11.221:8545 + +# === Mirror chain 651940 === +CHAIN_651940_RPC_URL=https://mainnet-rpc.alltra.global + +# === Major L1/L2 RPC (set per chain — CHAIN__RPC_URL) === +CHAIN_1_RPC_URL=https://eth.llamarpc.com +CHAIN_56_RPC_URL=https://bsc-dataseed.binance.org +CHAIN_137_RPC_URL=https://polygon-bor-rpc.publicnode.com +CHAIN_42161_RPC_URL=https://arb1.arbitrum.io/rpc +CHAIN_8453_RPC_URL=https://mainnet.base.org +CHAIN_10_RPC_URL=https://mainnet.optimism.io + +# === Fineract / Office 24 === +FINERACT_BASE_URL= +FINERACT_TENANT=omnl +OMNL_FINERACT_USERNAME= +OMNL_FINERACT_PASSWORD= + +# === Frontend === +VITE_WALLETCONNECT_PROJECT_ID= +VITE_THIRDWEB_CLIENT_ID= +VITE_RPC_URL_138=http://192.168.11.221:8545 +VITE_ENHANCED_SWAP_ROUTER_V2=0xa421706768aeb7fafa2d912c5e10824ef3437ad4 diff --git a/config/offices/office-24-settlement.v1.json b/config/offices/office-24-settlement.v1.json new file mode 100644 index 0000000..30cf81d --- /dev/null +++ b/config/offices/office-24-settlement.v1.json @@ -0,0 +1,28 @@ +{ + "$schema": "OMNL Office 24 — Ali HOSPITALLERS settlement profile", + "version": "1.0.0", + "officeId": 24, + "externalId": "HOSPITALLERS-ALI-IRAQ-IRAN", + "name": "HOSPITALLERS Ali Iraq-Iran", + "fineractTenant": "omnl", + "tenantUser": "ali_hospitallers_tenant", + "adminUser": "ali_hospitallers_admin", + "operatorUser": "ali_hospitallers_operator", + "settlement": { + "defaultCurrency": "USD", + "defaultLineId": "USD-M1", + "moneyLayers": ["M0", "M1", "M2"], + "glM0": "1050", + "glM1": "2100", + "glM2": "2200", + "glSettlement": "1000", + "glTradeFinanceContingent": "1410" + }, + "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" } + }, + "omnlLegalName": "ORGANISATION MONDIALE DU NUMERIQUE L.P.B.C.", + "omnlLei": "98450070C57395F6B906" +} diff --git a/config/omnl-m2-token-registry.v1.json b/config/omnl-m2-token-registry.v1.json new file mode 100644 index 0000000..aee138a --- /dev/null +++ b/config/omnl-m2-token-registry.v1.json @@ -0,0 +1,527 @@ +{ + "version": "1.0.0", + "chainId": 138, + "defaultMoneyLayer": "M2", + "defaultLoadFromGl": "2200", + "tokens": [ + { + "symbol": "ETH", + "name": "Ether", + "address": "0x0000000000000000000000000000000000000000", + "decimals": 18, + "native": true, + "currencyCode": "USD", + "moneyLayer": "M2", + "omnlLine": "USD-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "WETH", + "name": "Wrapped Ether", + "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "decimals": 18, + "currencyCode": "USD", + "moneyLayer": "M2", + "omnlLine": "USD-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "WETH10", + "name": "WETH10", + "address": "0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f", + "decimals": 18, + "currencyCode": "USD", + "moneyLayer": "M2", + "omnlLine": "USD-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "USDT", + "name": "Tether USD", + "address": "0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1", + "decimals": 6, + "currencyCode": "USD", + "moneyLayer": "M2", + "omnlLine": "USD-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "USDC", + "name": "USD Coin", + "address": "0x71D6687F38b93CCad569Fa6352c876eea967201b", + "decimals": 6, + "currencyCode": "USD", + "moneyLayer": "M2", + "omnlLine": "USD-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "cUSDT", + "name": "Compliant USDT", + "address": "0x93E66202A11B1772E55407B32B44e5Cd8eda7f22", + "decimals": 6, + "currencyCode": "USD", + "moneyLayer": "M2", + "omnlLine": "USD-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "cUSDC", + "name": "Compliant USDC", + "address": "0xf22258f57794CC8E06237084b353Ab30fFfa640b", + "decimals": 6, + "currencyCode": "USD", + "moneyLayer": "M2", + "omnlLine": "USD-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "cUSDT_V2", + "name": "Compliant USDT V2", + "address": "0x9FBfab33882Efe0038DAa608185718b772EE5660", + "decimals": 6, + "currencyCode": "USD", + "moneyLayer": "M2", + "omnlLine": "USD-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "cUSDC_V2", + "name": "Compliant USDC V2", + "address": "0x219522c60e83dEe01FC5b0329d6fA8fD84b9D13d", + "decimals": 6, + "currencyCode": "USD", + "moneyLayer": "M2", + "omnlLine": "USD-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "cAUSDT", + "name": "Compliant AUSDT", + "address": "0x5fdDF65733e3d590463F68f93Cf16E8c04081271", + "decimals": 6, + "currencyCode": "USD", + "moneyLayer": "M2", + "omnlLine": "USD-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "cUSDW", + "name": "Compliant USD W", + "address": "0xcA6BFa614935f1AB71c9aB106bAA6FBB6057095e", + "decimals": 6, + "currencyCode": "USD", + "moneyLayer": "M2", + "omnlLine": "USD-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "cEURC", + "name": "Euro Coin", + "address": "0x8085961F9cF02b4d800A3c6d386D31da4B34266a", + "decimals": 6, + "currencyCode": "EUR", + "moneyLayer": "M2", + "omnlLine": "EUR-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "cEURT", + "name": "Tether EUR", + "address": "0xdf4b71c61E5912712C1Bdd451416B9aC26949d72", + "decimals": 6, + "currencyCode": "EUR", + "moneyLayer": "M2", + "omnlLine": "EUR-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "cGBPC", + "name": "Pound Sterling", + "address": "0x003960f16D9d34F2e98d62723B6721Fb92074aD2", + "decimals": 6, + "currencyCode": "GBP", + "moneyLayer": "M2", + "omnlLine": "GBP-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "cGBPT", + "name": "Tether GBP", + "address": "0x350f54e4D23795f86A9c03988c7135357CCaD97c", + "decimals": 6, + "currencyCode": "GBP", + "moneyLayer": "M2", + "omnlLine": "GBP-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "cAUDC", + "name": "Australian Dollar", + "address": "0xD51482e567c03899eecE3CAe8a058161FD56069D", + "decimals": 6, + "currencyCode": "AUD", + "moneyLayer": "M2", + "omnlLine": "AUD-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "cJPYC", + "name": "Japanese Yen", + "address": "0xEe269e1226a334182aace90056EE4ee5Cc8A6770", + "decimals": 6, + "currencyCode": "JPY", + "moneyLayer": "M2", + "omnlLine": "JPY-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "cCHFC", + "name": "Swiss Franc", + "address": "0x873990849DDa5117d7C644f0aF24370797C03885", + "decimals": 6, + "currencyCode": "CHF", + "moneyLayer": "M2", + "omnlLine": "CHF-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "cCADC", + "name": "Canadian Dollar", + "address": "0x54dBd40cF05e15906A2C21f600937e96787f5679", + "decimals": 6, + "currencyCode": "CAD", + "moneyLayer": "M2", + "omnlLine": "CAD-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "cXAUC", + "name": "Gold Compliant", + "address": "0x290E52a8819A4fbD0714E517225429aA2B70EC6b", + "decimals": 6, + "currencyCode": "XAU", + "moneyLayer": "M2", + "omnlLine": "XAU-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "cXAUT", + "name": "Tether Gold", + "address": "0x94e408E26c6FD8F4ee00b54dF19082FDA07dC96E", + "decimals": 6, + "currencyCode": "XAU", + "moneyLayer": "M2", + "omnlLine": "XAU-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "cBTC", + "name": "Compliant BTC", + "address": "0xe94260c555ac1d9d3cc9e1632883452ebdf0082e", + "decimals": 8, + "currencyCode": "BTC", + "moneyLayer": "M2", + "omnlLine": "BTC-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "cETH", + "name": "Compliant ETH", + "address": "0xce7e00000000000000000000000000000000008a", + "decimals": 18, + "currencyCode": "ETH", + "moneyLayer": "M2", + "omnlLine": "ETH-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "cETHL2", + "name": "Compliant ETH L2", + "address": "0xce7200000000000000000000000000000000008a", + "decimals": 18, + "currencyCode": "ETH", + "moneyLayer": "M2", + "omnlLine": "ETH-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "cBNB", + "name": "Compliant BNB", + "address": "0xcb6b00000000000000000000000000000000008a", + "decimals": 18, + "currencyCode": "BNB", + "moneyLayer": "M2", + "omnlLine": "BNB-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "cPOL", + "name": "Compliant POL", + "address": "0xc90100000000000000000000000000000000008a", + "decimals": 18, + "currencyCode": "POL", + "moneyLayer": "M2", + "omnlLine": "POL-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "cAVAX", + "name": "Compliant AVAX", + "address": "0xcaaa00000000000000000000000000000000008a", + "decimals": 18, + "currencyCode": "AVAX", + "moneyLayer": "M2", + "omnlLine": "AVAX-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "cCRO", + "name": "Compliant CRO", + "address": "0xcc2000000000000000000000000000000000008a", + "decimals": 18, + "currencyCode": "CRO", + "moneyLayer": "M2", + "omnlLine": "CRO-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "cXDAI", + "name": "Compliant XDAI", + "address": "0xcda100000000000000000000000000000000008a", + "decimals": 18, + "currencyCode": "XDAI", + "moneyLayer": "M2", + "omnlLine": "XDAI-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "cCELO", + "name": "Compliant CELO", + "address": "0xcce100000000000000000000000000000000008a", + "decimals": 18, + "currencyCode": "CELO", + "moneyLayer": "M2", + "omnlLine": "CELO-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "cWEMIX", + "name": "Compliant WEMIX", + "address": "0xc11100000000000000000000000000000000008a", + "decimals": 18, + "currencyCode": "WEMIX", + "moneyLayer": "M2", + "omnlLine": "WEMIX-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "LINK", + "name": "Chainlink", + "address": "0xb7721dD53A8c629d9f1Ba31a5819AFe250002b03", + "decimals": 18, + "currencyCode": "USD", + "moneyLayer": "M2", + "omnlLine": "USD-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "LiXAU", + "name": "Lithium XAU", + "address": "0xff862f0d1f96aa4882a0c2d6b4a3516fd8d68e75", + "decimals": 6, + "currencyCode": "XAU", + "moneyLayer": "M2", + "omnlLine": "XAU-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "LiPMG", + "name": "Lithium PMG", + "address": "0x148b0f5c6fc8c5975e9406635654d88b20db2ed6", + "decimals": 6, + "currencyCode": "USD", + "moneyLayer": "M2", + "omnlLine": "USD-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "LiBMG1", + "name": "Lithium BMG1", + "address": "0xc5b802662447d1ae492a1618c3ad7161a449ebc9", + "decimals": 6, + "currencyCode": "USD", + "moneyLayer": "M2", + "omnlLine": "USD-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "LiBMG2", + "name": "Lithium BMG2", + "address": "0x2ca3b3e7f4f216015833b0b334273d44493c0c45", + "decimals": 6, + "currencyCode": "USD", + "moneyLayer": "M2", + "omnlLine": "USD-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + }, + { + "symbol": "LiBMG3", + "name": "Lithium BMG3", + "address": "0xb03d872196c0e84fe6fae841335001db44bda6c7", + "decimals": 6, + "currencyCode": "USD", + "moneyLayer": "M2", + "omnlLine": "USD-M2", + "loadFromGl": "2200", + "swappable": true, + "convertible": true, + "transferableInternal": true, + "transferableExternal": true + } + ] +} \ No newline at end of file diff --git a/config/omnl-supported-chains.v1.json b/config/omnl-supported-chains.v1.json new file mode 100644 index 0000000..ac04f46 --- /dev/null +++ b/config/omnl-supported-chains.v1.json @@ -0,0 +1,2320 @@ +{ + "$schema": "OMNL Bank supported chains — 128-chain production registry", + "version": "1.0.0", + "brand": "OMNL Central Bank", + "maxCapacity": 128, + "primaryChainId": 138, + "mirrorChainId": 651940, + "settlementOfficeId": 24, + "generatedAt": "2026-06-28T15:22:27.822Z", + "chains": [ + { + "chainId": 138, + "name": "DeFi Oracle Meta Mainnet", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "primary", + "omnlRole": "settlement-hub", + "status": "active", + "rpcEnv": "CHAIN_138_RPC_URL", + "explorerUrl": "https://explorer.d-bis.org", + "rpcDefault": "http://192.168.11.221:8545", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_138" + }, + { + "chainId": 651940, + "name": "ALL Mainnet", + "type": "evm", + "nativeSymbol": "ALL", + "tier": "primary", + "omnlRole": "mirror-target", + "status": "active", + "rpcEnv": "CHAIN_651940_RPC_URL", + "explorerUrl": "https://alltra.global", + "rpcDefault": "https://mainnet-rpc.alltra.global", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_651940" + }, + { + "chainId": 1, + "name": "Ethereum", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "l1", + "omnlRole": "reserve", + "status": "active", + "rpcEnv": "CHAIN_1_RPC_URL", + "explorerUrl": "https://etherscan.io", + "rpcDefault": "https://eth.llamarpc.com", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_1" + }, + { + "chainId": 56, + "name": "BNB Smart Chain", + "type": "evm", + "nativeSymbol": "BNB", + "tier": "l1", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_56_RPC_URL", + "explorerUrl": "https://bscscan.com", + "rpcDefault": "https://bsc-dataseed.binance.org", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_56" + }, + { + "chainId": 137, + "name": "Polygon", + "type": "evm", + "nativeSymbol": "POL", + "tier": "l1", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_137_RPC_URL", + "explorerUrl": "https://polygonscan.com", + "rpcDefault": "https://polygon-bor-rpc.publicnode.com", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_137" + }, + { + "chainId": 43114, + "name": "Avalanche C-Chain", + "type": "evm", + "nativeSymbol": "AVAX", + "tier": "l1", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_43114_RPC_URL", + "explorerUrl": "https://snowtrace.io", + "rpcDefault": "https://api.avax.network/ext/bc/C/rpc", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_43114" + }, + { + "chainId": 42161, + "name": "Arbitrum One", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "l2", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_42161_RPC_URL", + "explorerUrl": "https://arbiscan.io", + "rpcDefault": "https://arb1.arbitrum.io/rpc", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_42161" + }, + { + "chainId": 10, + "name": "Optimism", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "l2", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_10_RPC_URL", + "explorerUrl": "https://optimistic.etherscan.io", + "rpcDefault": "https://mainnet.optimism.io", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_10" + }, + { + "chainId": 8453, + "name": "Base", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "l2", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_8453_RPC_URL", + "explorerUrl": "https://basescan.org", + "rpcDefault": "https://mainnet.base.org", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_8453" + }, + { + "chainId": 100, + "name": "Gnosis", + "type": "evm", + "nativeSymbol": "xDAI", + "tier": "l1", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_100_RPC_URL", + "explorerUrl": "https://gnosisscan.io", + "rpcDefault": "https://rpc.gnosischain.com", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_100" + }, + { + "chainId": 25, + "name": "Cronos", + "type": "evm", + "nativeSymbol": "CRO", + "tier": "l1", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_25_RPC_URL", + "explorerUrl": "https://cronoscan.com", + "rpcDefault": "https://evm.cronos.org", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_25" + }, + { + "chainId": 42220, + "name": "Celo", + "type": "evm", + "nativeSymbol": "CELO", + "tier": "l1", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_42220_RPC_URL", + "explorerUrl": "https://celoscan.io", + "rpcDefault": "https://forno.celo.org", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_42220" + }, + { + "chainId": 1111, + "name": "Wemix", + "type": "evm", + "nativeSymbol": "WEMIX", + "tier": "l1", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_1111_RPC_URL", + "explorerUrl": "https://scan.wemix.com", + "rpcDefault": "https://api.wemix.com", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_1111" + }, + { + "chainId": 250, + "name": "Fantom", + "type": "evm", + "nativeSymbol": "FTM", + "tier": "l1", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_250_RPC_URL", + "explorerUrl": "https://ftmscan.com", + "rpcDefault": "https://rpc.ftm.tools", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_250" + }, + { + "chainId": 1284, + "name": "Moonbeam", + "type": "evm", + "nativeSymbol": "GLMR", + "tier": "l1", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_1284_RPC_URL", + "explorerUrl": "https://moonscan.io", + "rpcDefault": "https://rpc.api.moonbeam.network", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_1284" + }, + { + "chainId": 1285, + "name": "Moonriver", + "type": "evm", + "nativeSymbol": "MOVR", + "tier": "l1", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_1285_RPC_URL", + "explorerUrl": "https://moonriver.moonscan.io", + "rpcDefault": "https://rpc.api.moonriver.moonbeam.network", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_1285" + }, + { + "chainId": 324, + "name": "zkSync Era", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "l2", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_324_RPC_URL", + "explorerUrl": "https://explorer.zksync.io", + "rpcDefault": "https://mainnet.era.zksync.io", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_324" + }, + { + "chainId": 59144, + "name": "Linea", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "l2", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_59144_RPC_URL", + "explorerUrl": "https://lineascan.build", + "rpcDefault": "https://rpc.linea.build", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_59144" + }, + { + "chainId": 534352, + "name": "Scroll", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "l2", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_534352_RPC_URL", + "explorerUrl": "https://scrollscan.com", + "rpcDefault": "https://rpc.scroll.io", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_534352" + }, + { + "chainId": 81457, + "name": "Blast", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "l2", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_81457_RPC_URL", + "explorerUrl": "https://blastscan.io", + "rpcDefault": "https://rpc.blast.io", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_81457" + }, + { + "chainId": 5000, + "name": "Mantle", + "type": "evm", + "nativeSymbol": "MNT", + "tier": "l2", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_5000_RPC_URL", + "explorerUrl": "https://explorer.mantle.xyz", + "rpcDefault": "https://rpc.mantle.xyz", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_5000" + }, + { + "chainId": 169, + "name": "Manta Pacific", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "l2", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_169_RPC_URL", + "explorerUrl": "https://pacific-explorer.manta.network", + "rpcDefault": "https://pacific-rpc.manta.network/http", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_169" + }, + { + "chainId": 1101, + "name": "Polygon zkEVM", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "l2", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_1101_RPC_URL", + "explorerUrl": "https://zkevm.polygonscan.com", + "rpcDefault": "https://zkevm-rpc.com", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_1101" + }, + { + "chainId": 34443, + "name": "Mode", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "l2", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_34443_RPC_URL", + "explorerUrl": "https://explorer.mode.network", + "rpcDefault": "https://mainnet.mode.network", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_34443" + }, + { + "chainId": 7777777, + "name": "Zora", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "l2", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_7777777_RPC_URL", + "explorerUrl": "https://explorer.zora.energy", + "rpcDefault": "https://rpc.zora.energy", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_7777777" + }, + { + "chainId": 204, + "name": "opBNB", + "type": "evm", + "nativeSymbol": "BNB", + "tier": "l2", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_204_RPC_URL", + "explorerUrl": "https://opbnbscan.com", + "rpcDefault": "https://opbnb-mainnet-rpc.bnbchain.org", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_204" + }, + { + "chainId": 1088, + "name": "Metis", + "type": "evm", + "nativeSymbol": "METIS", + "tier": "l2", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_1088_RPC_URL", + "explorerUrl": "https://explorer.metis.io", + "rpcDefault": "https://andromeda.metis.io/?owner=1088", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_1088" + }, + { + "chainId": 122, + "name": "Fuse", + "type": "evm", + "nativeSymbol": "FUSE", + "tier": "l1", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_122_RPC_URL", + "explorerUrl": "https://explorer.fuse.io", + "rpcDefault": "https://rpc.fuse.io", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_122" + }, + { + "chainId": 288, + "name": "Boba", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "l2", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_288_RPC_URL", + "explorerUrl": "https://bobascan.com", + "rpcDefault": "https://mainnet.boba.network", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_288" + }, + { + "chainId": 1313161554, + "name": "Aurora", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "l2", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_1313161554_RPC_URL", + "explorerUrl": "https://explorer.aurora.dev", + "rpcDefault": "https://mainnet.aurora.dev", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_1313161554" + }, + { + "chainId": 8217, + "name": "Klaytn", + "type": "evm", + "nativeSymbol": "KLAY", + "tier": "l1", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_8217_RPC_URL", + "explorerUrl": "https://scope.klaytn.com", + "rpcDefault": "https://public-node-api.klaytnapi.io/v1/cypress", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_8217" + }, + { + "chainId": 1666600000, + "name": "Harmony", + "type": "evm", + "nativeSymbol": "ONE", + "tier": "l1", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_1666600000_RPC_URL", + "explorerUrl": "https://explorer.harmony.one", + "rpcDefault": "https://api.harmony.one", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_1666600000" + }, + { + "chainId": 4689, + "name": "IoTeX", + "type": "evm", + "nativeSymbol": "IOTX", + "tier": "l1", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_4689_RPC_URL", + "explorerUrl": "https://iotexscan.io", + "rpcDefault": "https://babel-api.mainnet.iotex.io", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_4689" + }, + { + "chainId": 9001, + "name": "Evmos", + "type": "evm", + "nativeSymbol": "EVMOS", + "tier": "l1", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_9001_RPC_URL", + "explorerUrl": "https://escan.live", + "rpcDefault": "https://evmos.lava.build", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_9001" + }, + { + "chainId": 321, + "name": "KCC", + "type": "evm", + "nativeSymbol": "KCS", + "tier": "l1", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_321_RPC_URL", + "explorerUrl": "https://explorer.kcc.io", + "rpcDefault": "https://rpc-mainnet.kcc.network", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_321" + }, + { + "chainId": 1030, + "name": "Conflux eSpace", + "type": "evm", + "nativeSymbol": "CFX", + "tier": "l1", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_1030_RPC_URL", + "explorerUrl": "https://evm.confluxscan.net", + "rpcDefault": "https://evm.confluxrpc.com", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_1030" + }, + { + "chainId": 7700, + "name": "Canto", + "type": "evm", + "nativeSymbol": "CANTO", + "tier": "l1", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_7700_RPC_URL", + "explorerUrl": "https://tuber.build", + "rpcDefault": "https://canto.gravitychain.io", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_7700" + }, + { + "chainId": 592, + "name": "Astar", + "type": "evm", + "nativeSymbol": "ASTR", + "tier": "l1", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_592_RPC_URL", + "explorerUrl": "https://astar.subscan.io", + "rpcDefault": "https://evm.astar.network", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_592" + }, + { + "chainId": 1818, + "name": "Cube", + "type": "evm", + "nativeSymbol": "CUBE", + "tier": "l1", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_1818_RPC_URL", + "explorerUrl": "https://www.cubescan.network", + "rpcDefault": "https://http-mainnet.cube.network", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_1818" + }, + { + "chainId": 888, + "name": "Wanchain", + "type": "evm", + "nativeSymbol": "WAN", + "tier": "l1", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_888_RPC_URL", + "explorerUrl": "https://wanscan.org", + "rpcDefault": "https://gwan-ssl.wandevs.org:56891", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_888" + }, + { + "chainId": 61, + "name": "Ethereum Classic", + "type": "evm", + "nativeSymbol": "ETC", + "tier": "l1", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_61_RPC_URL", + "explorerUrl": "https://blockscout.com/etc/mainnet", + "rpcDefault": "https://etc.rivet.link", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_61" + }, + { + "chainId": 520, + "name": "XT Smart Chain", + "type": "evm", + "nativeSymbol": "XT", + "tier": "l1", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_520_RPC_URL", + "explorerUrl": "https://www.xscan.ai", + "rpcDefault": "https://datarpc1.xsc.pub", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_520" + }, + { + "chainId": 23294, + "name": "Oasis Sapphire", + "type": "evm", + "nativeSymbol": "ROSE", + "tier": "l1", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_23294_RPC_URL", + "explorerUrl": "https://explorer.oasis.io", + "rpcDefault": "https://sapphire.oasis.io", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_23294" + }, + { + "chainId": 71402, + "name": "Godwoken", + "type": "evm", + "nativeSymbol": "CKB", + "tier": "l2", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_71402_RPC_URL", + "explorerUrl": "https://v1.gwscan.com", + "rpcDefault": "https://v1.mainnet.godwoken.io/rpc", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_71402" + }, + { + "chainId": 1116, + "name": "Core", + "type": "evm", + "nativeSymbol": "CORE", + "tier": "l1", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_1116_RPC_URL", + "explorerUrl": "https://scan.coredao.org", + "rpcDefault": "https://rpc.coredao.org", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_1116" + }, + { + "chainId": 42793, + "name": "Etherlink", + "type": "evm", + "nativeSymbol": "XTZ", + "tier": "l2", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_42793_RPC_URL", + "explorerUrl": "https://explorer.etherlink.com", + "rpcDefault": "https://node.mainnet.etherlink.com", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_42793" + }, + { + "chainId": 1135, + "name": "Lisk", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "l2", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_1135_RPC_URL", + "explorerUrl": "https://blockscout.lisk.com", + "rpcDefault": "https://rpc.api.lisk.com", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_1135" + }, + { + "chainId": 252, + "name": "Fraxtal", + "type": "evm", + "nativeSymbol": "FRAX", + "tier": "l2", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_252_RPC_URL", + "explorerUrl": "https://fraxscan.com", + "rpcDefault": "https://rpc.frax.com", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_252" + }, + { + "chainId": 480, + "name": "World Chain", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "l2", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_480_RPC_URL", + "explorerUrl": "https://worldscan.org", + "rpcDefault": "https://worldchain-mainnet.g.alchemy.com/public", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_480" + }, + { + "chainId": 1480, + "name": "Vana", + "type": "evm", + "nativeSymbol": "VANA", + "tier": "l1", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_1480_RPC_URL", + "explorerUrl": "https://vanascan.io", + "rpcDefault": "https://rpc.vana.org", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_1480" + }, + { + "chainId": 80094, + "name": "Berachain", + "type": "evm", + "nativeSymbol": "BERA", + "tier": "l1", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_80094_RPC_URL", + "explorerUrl": "https://berascan.com", + "rpcDefault": "https://rpc.berachain.com", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_80094" + }, + { + "chainId": 130, + "name": "Unichain", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "l2", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_130_RPC_URL", + "explorerUrl": "https://uniscan.xyz", + "rpcDefault": "https://mainnet.unichain.org", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_130" + }, + { + "chainId": 196, + "name": "X Layer", + "type": "evm", + "nativeSymbol": "OKB", + "tier": "l2", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_196_RPC_URL", + "explorerUrl": "https://www.oklink.com/xlayer", + "rpcDefault": "https://rpc.xlayer.tech", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_196" + }, + { + "chainId": 199, + "name": "BitTorrent Chain", + "type": "evm", + "nativeSymbol": "BTT", + "tier": "l1", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_199_RPC_URL", + "explorerUrl": "https://bttcscan.com", + "rpcDefault": "https://rpc.bittorrentchain.io", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_199" + }, + { + "chainId": 106, + "name": "Velas", + "type": "evm", + "nativeSymbol": "VLX", + "tier": "l1", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_106_RPC_URL", + "explorerUrl": "https://evmexplorer.velas.com", + "rpcDefault": "https://evmexplorer.velas.com/rpc", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_106" + }, + { + "chainId": 1000, + "name": "GTON", + "type": "evm", + "nativeSymbol": "GCD", + "tier": "l1", + "omnlRole": "staged", + "status": "active", + "rpcEnv": "CHAIN_1000_RPC_URL", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_1000" + }, + { + "chainId": 2222, + "name": "Kava EVM", + "type": "evm", + "nativeSymbol": "KAVA", + "tier": "l1", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_2222_RPC_URL", + "explorerUrl": "https://explorer.kava.io", + "rpcDefault": "https://evm.kava.io", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_2222" + }, + { + "chainId": 1996, + "name": "Sanko", + "type": "evm", + "nativeSymbol": "DMT", + "tier": "l1", + "omnlRole": "staged", + "status": "active", + "rpcEnv": "CHAIN_1996_RPC_URL", + "explorerUrl": "https://sanko-arbiscan.io", + "rpcDefault": "https://mainnet.sanko.xyz", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_1996" + }, + { + "chainId": 167000, + "name": "Taiko", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "l2", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_167000_RPC_URL", + "explorerUrl": "https://taikoscan.io", + "rpcDefault": "https://rpc.mainnet.taiko.xyz", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_167000" + }, + { + "chainId": 690, + "name": "Redstone", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "l2", + "omnlRole": "liquidity", + "status": "active", + "rpcEnv": "CHAIN_690_RPC_URL", + "explorerUrl": "https://explorer.redstone.xyz", + "rpcDefault": "https://rpc.redstonechain.com", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_690" + }, + { + "chainId": 810180, + "name": "zkLink Nova", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "l2", + "omnlRole": "staged", + "status": "active", + "rpcEnv": "CHAIN_810180_RPC_URL", + "explorerUrl": "https://explorer.zklink.io", + "rpcDefault": "https://rpc.zklink.io", + "settlement": { + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_810180" + }, + { + "chainId": 534351, + "name": "Scroll Sepolia", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "testnet", + "omnlRole": "test", + "status": "staged", + "rpcEnv": "CHAIN_534351_RPC_URL", + "explorerUrl": "https://sepolia.scrollscan.com", + "rpcDefault": "https://sepolia-rpc.scroll.io", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_534351" + }, + { + "chainId": 11155111, + "name": "Ethereum Sepolia", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "testnet", + "omnlRole": "test", + "status": "staged", + "rpcEnv": "CHAIN_11155111_RPC_URL", + "explorerUrl": "https://sepolia.etherscan.io", + "rpcDefault": "https://rpc.sepolia.org", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_11155111" + }, + { + "chainId": 2138, + "name": "Defi Oracle Meta Testnet", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "testnet", + "omnlRole": "test", + "status": "staged", + "rpcEnv": "CHAIN_2138_RPC_URL", + "explorerUrl": "https://public-2138.defi-oracle.io", + "rpcDefault": "https://rpc.public-2138.defi-oracle.io", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_2138" + }, + { + "chainId": 900001, + "name": "OMNL Expansion Slot 1", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900001_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900001" + }, + { + "chainId": 900002, + "name": "OMNL Expansion Slot 2", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900002_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900002" + }, + { + "chainId": 900003, + "name": "OMNL Expansion Slot 3", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900003_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900003" + }, + { + "chainId": 900004, + "name": "OMNL Expansion Slot 4", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900004_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900004" + }, + { + "chainId": 900005, + "name": "OMNL Expansion Slot 5", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900005_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900005" + }, + { + "chainId": 900006, + "name": "OMNL Expansion Slot 6", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900006_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900006" + }, + { + "chainId": 900007, + "name": "OMNL Expansion Slot 7", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900007_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900007" + }, + { + "chainId": 900008, + "name": "OMNL Expansion Slot 8", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900008_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900008" + }, + { + "chainId": 900009, + "name": "OMNL Expansion Slot 9", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900009_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900009" + }, + { + "chainId": 900010, + "name": "OMNL Expansion Slot 10", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900010_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900010" + }, + { + "chainId": 900011, + "name": "OMNL Expansion Slot 11", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900011_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900011" + }, + { + "chainId": 900012, + "name": "OMNL Expansion Slot 12", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900012_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900012" + }, + { + "chainId": 900013, + "name": "OMNL Expansion Slot 13", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900013_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900013" + }, + { + "chainId": 900014, + "name": "OMNL Expansion Slot 14", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900014_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900014" + }, + { + "chainId": 900015, + "name": "OMNL Expansion Slot 15", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900015_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900015" + }, + { + "chainId": 900016, + "name": "OMNL Expansion Slot 16", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900016_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900016" + }, + { + "chainId": 900017, + "name": "OMNL Expansion Slot 17", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900017_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900017" + }, + { + "chainId": 900018, + "name": "OMNL Expansion Slot 18", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900018_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900018" + }, + { + "chainId": 900019, + "name": "OMNL Expansion Slot 19", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900019_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900019" + }, + { + "chainId": 900020, + "name": "OMNL Expansion Slot 20", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900020_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900020" + }, + { + "chainId": 900021, + "name": "OMNL Expansion Slot 21", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900021_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900021" + }, + { + "chainId": 900022, + "name": "OMNL Expansion Slot 22", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900022_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900022" + }, + { + "chainId": 900023, + "name": "OMNL Expansion Slot 23", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900023_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900023" + }, + { + "chainId": 900024, + "name": "OMNL Expansion Slot 24", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900024_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900024" + }, + { + "chainId": 900025, + "name": "OMNL Expansion Slot 25", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900025_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900025" + }, + { + "chainId": 900026, + "name": "OMNL Expansion Slot 26", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900026_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900026" + }, + { + "chainId": 900027, + "name": "OMNL Expansion Slot 27", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900027_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900027" + }, + { + "chainId": 900028, + "name": "OMNL Expansion Slot 28", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900028_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900028" + }, + { + "chainId": 900029, + "name": "OMNL Expansion Slot 29", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900029_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900029" + }, + { + "chainId": 900030, + "name": "OMNL Expansion Slot 30", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900030_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900030" + }, + { + "chainId": 900031, + "name": "OMNL Expansion Slot 31", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900031_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900031" + }, + { + "chainId": 900032, + "name": "OMNL Expansion Slot 32", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900032_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900032" + }, + { + "chainId": 900033, + "name": "OMNL Expansion Slot 33", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900033_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900033" + }, + { + "chainId": 900034, + "name": "OMNL Expansion Slot 34", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900034_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900034" + }, + { + "chainId": 900035, + "name": "OMNL Expansion Slot 35", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900035_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900035" + }, + { + "chainId": 900036, + "name": "OMNL Expansion Slot 36", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900036_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900036" + }, + { + "chainId": 900037, + "name": "OMNL Expansion Slot 37", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900037_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900037" + }, + { + "chainId": 900038, + "name": "OMNL Expansion Slot 38", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900038_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900038" + }, + { + "chainId": 900039, + "name": "OMNL Expansion Slot 39", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900039_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900039" + }, + { + "chainId": 900040, + "name": "OMNL Expansion Slot 40", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900040_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900040" + }, + { + "chainId": 900041, + "name": "OMNL Expansion Slot 41", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900041_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900041" + }, + { + "chainId": 900042, + "name": "OMNL Expansion Slot 42", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900042_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900042" + }, + { + "chainId": 900043, + "name": "OMNL Expansion Slot 43", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900043_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900043" + }, + { + "chainId": 900044, + "name": "OMNL Expansion Slot 44", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900044_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900044" + }, + { + "chainId": 900045, + "name": "OMNL Expansion Slot 45", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900045_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900045" + }, + { + "chainId": 900046, + "name": "OMNL Expansion Slot 46", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900046_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900046" + }, + { + "chainId": 900047, + "name": "OMNL Expansion Slot 47", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900047_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900047" + }, + { + "chainId": 900048, + "name": "OMNL Expansion Slot 48", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900048_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900048" + }, + { + "chainId": 900049, + "name": "OMNL Expansion Slot 49", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900049_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900049" + }, + { + "chainId": 900050, + "name": "OMNL Expansion Slot 50", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900050_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900050" + }, + { + "chainId": 900051, + "name": "OMNL Expansion Slot 51", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900051_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900051" + }, + { + "chainId": 900052, + "name": "OMNL Expansion Slot 52", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900052_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900052" + }, + { + "chainId": 900053, + "name": "OMNL Expansion Slot 53", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900053_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900053" + }, + { + "chainId": 900054, + "name": "OMNL Expansion Slot 54", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900054_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900054" + }, + { + "chainId": 900055, + "name": "OMNL Expansion Slot 55", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900055_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900055" + }, + { + "chainId": 900056, + "name": "OMNL Expansion Slot 56", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900056_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900056" + }, + { + "chainId": 900057, + "name": "OMNL Expansion Slot 57", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900057_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900057" + }, + { + "chainId": 900058, + "name": "OMNL Expansion Slot 58", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900058_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900058" + }, + { + "chainId": 900059, + "name": "OMNL Expansion Slot 59", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900059_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900059" + }, + { + "chainId": 900060, + "name": "OMNL Expansion Slot 60", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900060_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900060" + }, + { + "chainId": 900061, + "name": "OMNL Expansion Slot 61", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900061_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900061" + }, + { + "chainId": 900062, + "name": "OMNL Expansion Slot 62", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900062_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900062" + }, + { + "chainId": 900063, + "name": "OMNL Expansion Slot 63", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900063_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900063" + }, + { + "chainId": 900064, + "name": "OMNL Expansion Slot 64", + "type": "evm", + "nativeSymbol": "ETH", + "tier": "reserved", + "omnlRole": "expansion", + "status": "reserved", + "rpcEnv": "CHAIN_900064_RPC_URL", + "settlement": { + "m2LoadEnabled": false, + "swapEnabled": false, + "bridgeEnabled": false, + "officeId": 24 + }, + "complianceEnv": "OMNL_COMPLIANCE_CORE_900064" + } + ], + "stats": { + "total": 128, + "active": 61, + "staged": 3, + "reserved": 64 + } +} \ No newline at end of file diff --git a/config/settlement-middleware.production.v1.json b/config/settlement-middleware.production.v1.json new file mode 100644 index 0000000..078a05a --- /dev/null +++ b/config/settlement-middleware.production.v1.json @@ -0,0 +1,35 @@ +{ + "$schema": "OMNL central-bank settlement — production profile", + "version": "1.0.0", + "omnlLegalName": "ORGANISATION MONDIALE DU NUMERIQUE L.P.B.C.", + "defaultOfficeId": 24, + "enforceSingleOffice": true, + "settlementOffice": { + "officeId": 24, + "externalId": "HOSPITALLERS-ALI-IRAQ-IRAN", + "name": "HOSPITALLERS Ali Iraq-Iran", + "profilePath": "config/offices/office-24-settlement.v1.json" + }, + "defaultCurrency": "USD", + "chainRegistry": "config/omnl-supported-chains.v1.json", + "maxSupportedChains": 128, + "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-M2" }, + "multiChain": { "enabled": true, "registryPath": "config/omnl-supported-chains.v1.json", "primaryChainId": 138 } + }, + "moneySupply": { + "glM0": "1050", + "glM1": "2100", + "glM2": "2200", + "glSettlement": "1000" + }, + "production": { + "requireApiKey": true, + "allowHybxProduction": true, + "allowChainMintExecute": true, + "onlineBankMode": true + } +} + \ No newline at end of file diff --git a/config/settlement-middleware.v1.json b/config/settlement-middleware.v1.json new file mode 100644 index 0000000..2d7e7d6 --- /dev/null +++ b/config/settlement-middleware.v1.json @@ -0,0 +1,40 @@ +{ + "$schema": "OMNL central-bank settlement middleware — locked to Office 24", + "version": "1.0.0", + "omnlLegalName": "ORGANISATION MONDIALE DU NUMERIQUE L.P.B.C.", + "defaultOfficeId": 24, + "enforceSingleOffice": true, + "settlementOffice": { + "officeId": 24, + "externalId": "HOSPITALLERS-ALI-IRAQ-IRAN", + "name": "HOSPITALLERS Ali Iraq-Iran", + "profilePath": "config/offices/office-24-settlement.v1.json" + }, + "defaultCurrency": "USD", + "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" + } + }, + "moneySupply": { + "glM0": "1050", + "glM1": "2100", + "glM2": "2200", + "glSettlement": "1000" + }, + "production": { + "requireApiKey": true, + "allowHybxProduction": false, + "allowChainMintExecute": false + } +} diff --git a/config/trade-finance/instruments.v1.json b/config/trade-finance/instruments.v1.json new file mode 100644 index 0000000..79fc9e9 --- /dev/null +++ b/config/trade-finance/instruments.v1.json @@ -0,0 +1,30 @@ +{ + "version": "1.0.0", + "instruments": { + "SBLC": { + "description": "Standby Letter of Credit", + "swiftMessages": ["MT103", "MT202"], + "contingentGl": "1410" + }, + "DLC": { + "description": "Documentary Letter of Credit", + "swiftMessages": ["MT103", "MT102"], + "contingentGl": "1410" + }, + "BG": { + "description": "Bank Guarantee", + "swiftMessages": ["MT103"], + "contingentGl": "1410" + }, + "LS": { + "description": "Liquidity Support / Lombard-style facility", + "swiftMessages": ["MT102", "MT103"], + "settlementGl": "1000", + "m2Gl": "2200" + } + }, + "iban": { + "requireMod97": true, + "omnlBic": "OMNLUS33" + } +} diff --git a/deploy/nginx/frontend-spa.conf b/deploy/nginx/frontend-spa.conf new file mode 100644 index 0000000..e864e54 --- /dev/null +++ b/deploy/nginx/frontend-spa.conf @@ -0,0 +1,14 @@ +server { + listen 80; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location /assets/ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +} diff --git a/deploy/nginx/omnl-bank.conf b/deploy/nginx/omnl-bank.conf new file mode 100644 index 0000000..01176f0 --- /dev/null +++ b/deploy/nginx/omnl-bank.conf @@ -0,0 +1,70 @@ +# OMNL Bank online — nginx reverse proxy (Central Bank style production) +# TLS: mount certs at deploy/nginx/certs/fullchain.pem + privkey.pem + +upstream omnl_api { + server token-aggregation:3000; +} + +upstream omnl_settlement { + server settlement-middleware:3011; +} + +upstream omnl_exchange { + server dbis-exchange:3012; +} + +upstream omnl_frontend { + server frontend:80; +} + +server { + listen 80; + server_name bank.omnl.hybxfinance.io localhost; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2; + server_name bank.omnl.hybxfinance.io localhost; + + ssl_certificate /etc/nginx/certs/fullchain.pem; + ssl_certificate_key /etc/nginx/certs/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + + add_header X-OMNL-Bank "online" always; + add_header X-OMNL-Chains "128" always; + + # Frontend — Central Bank / Office 24 / DBIS Trade + location / { + proxy_pass http://omnl_frontend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + + # OMNL API (token-aggregation) + location /api/v1/ { + proxy_pass http://omnl_api/api/v1/; + proxy_set_header Host $host; + proxy_read_timeout 120s; + } + + # Settlement middleware + location /settlement/ { + proxy_pass http://omnl_settlement/api/v1/settlement/; + proxy_set_header Host $host; + proxy_set_header Authorization $http_authorization; + proxy_read_timeout 120s; + } + + # DBIS Exchange + location /exchange/ { + proxy_pass http://omnl_exchange/api/v1/exchange/; + proxy_set_header Host $host; + proxy_read_timeout 120s; + } + + location /health { + return 200 'OMNL Bank online — 128 chains'; + add_header Content-Type text/plain; + } +} diff --git a/docker-compose.omnl-production.yml b/docker-compose.omnl-production.yml new file mode 100644 index 0000000..329c23e --- /dev/null +++ b/docker-compose.omnl-production.yml @@ -0,0 +1,99 @@ +# OMNL Bank — full production stack (128-chain support) +# Usage: docker compose -f docker-compose.omnl-production.yml up -d + +services: + token-aggregation: + build: + context: ./services/token-aggregation + dockerfile: Dockerfile + ports: + - "3000:3000" + env_file: + - ./config/deployment-omnl.production.env + environment: + - NODE_ENV=production + - PORT=3000 + - OMNL_SUPPORTED_CHAINS_CONFIG=/app/config/omnl-supported-chains.v1.json + volumes: + - ./config:/app/config:ro + restart: unless-stopped + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health',r=>process.exit(r.statusCode===200?0:1))"] + interval: 30s + timeout: 5s + retries: 3 + + settlement-middleware: + build: + context: ./services/settlement-middleware + dockerfile: Dockerfile + ports: + - "3011:3011" + env_file: + - ./config/deployment-omnl.production.env + environment: + - NODE_ENV=production + - SETTLEMENT_MIDDLEWARE_PORT=3011 + - SETTLEMENT_MIDDLEWARE_CONFIG=/app/config/settlement-middleware.production.v1.json + - OMNL_SUPPORTED_CHAINS_CONFIG=/app/config/omnl-supported-chains.v1.json + - TOKEN_AGGREGATION_URL=http://token-aggregation:3000 + volumes: + - ./config:/app/config:ro + - settlement-data:/app/data + depends_on: + token-aggregation: + condition: service_healthy + restart: unless-stopped + + dbis-exchange: + build: + context: ./services/dbis-exchange + dockerfile: Dockerfile + ports: + - "3012:3012" + env_file: + - ./config/deployment-omnl.production.env + environment: + - NODE_ENV=production + - DBIS_EXCHANGE_PORT=3012 + - DBIS_EXCHANGE_CONFIG=/app/config/dbis-exchange.production.v1.json + - OMNL_SUPPORTED_CHAINS_CONFIG=/app/config/omnl-supported-chains.v1.json + - OMNL_M2_TOKEN_REGISTRY=/app/config/omnl-m2-token-registry.v1.json + - TOKEN_AGGREGATION_URL=http://token-aggregation:3000 + - SETTLEMENT_MIDDLEWARE_URL=http://settlement-middleware:3011 + volumes: + - ./config:/app/config:ro + depends_on: + settlement-middleware: + condition: service_started + restart: unless-stopped + + frontend: + build: + context: ./frontend-dapp + dockerfile: Dockerfile.production + ports: + - "3002:80" + depends_on: + - dbis-exchange + - settlement-middleware + - token-aggregation + restart: unless-stopped + + nginx: + image: nginx:alpine + ports: + - "443:443" + - "80:80" + volumes: + - ./deploy/nginx/omnl-bank.conf:/etc/nginx/conf.d/default.conf:ro + - ./deploy/nginx/certs:/etc/nginx/certs:ro + depends_on: + - frontend + - token-aggregation + - settlement-middleware + - dbis-exchange + restart: unless-stopped + +volumes: + settlement-data: diff --git a/frontend-dapp/Dockerfile.production b/frontend-dapp/Dockerfile.production new file mode 100644 index 0000000..d4434ed --- /dev/null +++ b/frontend-dapp/Dockerfile.production @@ -0,0 +1,20 @@ +# OMNL Bank production frontend +FROM node:20-alpine AS builder +WORKDIR /app +RUN corepack enable +COPY package.json pnpm-lock.yaml* ./ +RUN pnpm install --frozen-lockfile 2>/dev/null || pnpm install +COPY . . +ARG VITE_TOKEN_AGGREGATION_URL=/api/v1 +ARG VITE_SETTLEMENT_MIDDLEWARE_URL=/settlement +ARG VITE_DBIS_EXCHANGE_URL=/exchange +ENV VITE_TOKEN_AGGREGATION_URL=$VITE_TOKEN_AGGREGATION_URL +ENV VITE_SETTLEMENT_MIDDLEWARE_URL=$VITE_SETTLEMENT_MIDDLEWARE_URL +ENV VITE_DBIS_EXCHANGE_URL=$VITE_DBIS_EXCHANGE_URL +RUN pnpm run build + +FROM nginx:alpine +COPY --from=builder /app/dist /usr/share/nginx/html +COPY deploy/nginx/frontend-spa.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend-dapp/src/App.tsx b/frontend-dapp/src/App.tsx index ea4e046..fd32c86 100644 --- a/frontend-dapp/src/App.tsx +++ b/frontend-dapp/src/App.tsx @@ -7,27 +7,27 @@ import { AdminProvider } from './contexts/AdminContext' import { ErrorBoundary } from './components/ErrorBoundary' import BridgePage from './pages/BridgePage' import SwapPage from './pages/SwapPage' +import TradePage from './pages/TradePage' +import CentralBankPage from './pages/CentralBankPage' +import Office24Page from './pages/Office24Page' import ReservePage from './pages/ReservePage' import HistoryPage from './pages/HistoryPage' import AdminPanel from './pages/AdminPanel' import DocsPage from './pages/DocsPage' import WalletsDemoPage from './pages/WalletsDemoPage' import Layout from './components/layout/Layout' +import OmnlProductLayout from './components/layout/OmnlProductLayout' import ToastProvider from './components/ui/ToastProvider' -// Configure QueryClient to handle contract revert errors gracefully -// Don't retry on contract revert errors (expected when contracts aren't deployed) const queryClient = new QueryClient({ defaultOptions: { queries: { - retry: (failureCount, error: any) => { - // Don't retry on contract revert errors (CALL_EXCEPTION) - // These are expected when contracts aren't deployed at the specified addresses - if (error?.code === 'CALL_EXCEPTION' || error?.message?.includes('call revert exception')) { - return false; + retry: (failureCount, error: unknown) => { + const err = error as { code?: string; message?: string } + if (err?.code === 'CALL_EXCEPTION' || err?.message?.includes('call revert exception')) { + return false } - // Retry other errors up to 2 times - return failureCount < 2; + return failureCount < 2 }, }, }, @@ -48,8 +48,13 @@ function App() { > - - + + }> + } /> + } /> + } /> + + }> } /> } /> } /> @@ -57,8 +62,8 @@ function App() { } /> } /> } /> - - + + @@ -69,4 +74,3 @@ function App() { } export default App - diff --git a/frontend-dapp/src/components/bridge/BridgeButtons.tsx b/frontend-dapp/src/components/bridge/BridgeButtons.tsx index c71249f..e344e8b 100644 --- a/frontend-dapp/src/components/bridge/BridgeButtons.tsx +++ b/frontend-dapp/src/components/bridge/BridgeButtons.tsx @@ -25,7 +25,6 @@ import CopyButton from '../ui/CopyButton'; import ConfirmationModal from '../ui/ConfirmationModal'; import Tooltip from '../ui/Tooltip'; import LoadingSkeleton from '../ui/LoadingSkeleton'; -import ChainIcon from '../ui/ChainIcon'; import TokenIcon from '../ui/TokenIcon'; interface BridgeButtonsProps { @@ -384,38 +383,28 @@ function BridgeButtonsConnected({ /> )}
-
-
-

- Bridge to - {destination && ( - <> - - {destination.name} - - )} - {!destination && Ethereum Mainnet} -

- - - -
-

- Wrap ETH, approve tokens, and bridge WETH9 via CCIP +

+

+ Wrap ETH, approve tokens, and bridge WETH9 via CCIP to{' '} + {destination?.name ?? 'Ethereum Mainnet'}

+ + +
-
-
-
+
-

Balances & Fees

+

Balances & Fees

{address && ( Copy Address @@ -520,49 +505,49 @@ function BridgeButtonsConnected({ )}
-
- +
+ ETH Balance: - - {ethBalance ? ethBalance.displayValue : } ETH + + {ethBalance ? ethBalance.displayValue : } ETH
-
- +
+ WETH9 Balance: - + {address && weth9Balance !== undefined ? `${ethers.utils.formatEther(weth9Balance.toString())}` : address ? - : '0'} WETH9 + : '0'} WETH9
-
- +
+ LINK Balance: - + {address && linkBalance !== undefined ? `${ethers.utils.formatEther(linkBalance.toString())}` : address ? - : '0'} LINK + : '0'} LINK
{ccipFee != null && ccipFee.gt(0) && ( -
- +
+ CCIP Fee: - - {ethers.utils.formatEther(ccipFee)} LINK + + {ethers.utils.formatEther(ccipFee)} LINK
)} @@ -573,7 +558,7 @@ function BridgeButtonsConnected({ + + +
+

Money layers (Office {office?.officeId ?? 24})

+
+ {(office?.settlement?.moneyLayers ?? ['M0', 'M1', 'M2']).map((layer) => ( + + {layer} + + ))} +
+
+
+
GL M0
+
{office?.settlement?.glM0 ?? '1050'}
+
+
+
GL M1
+
{office?.settlement?.glM1 ?? '2100'}
+
+
+
GL M2
+
{office?.settlement?.glM2 ?? '2200'}
+
+
+
+ +
+

Recent exchange activity

+
    + {((cb.monitor as { swaps?: { status: string; tokenIn: string; createdAt: string }[] })?.swaps ?? []) + .slice(0, 8) + .map((s, i) => ( +
  • + {s.status} + {s.tokenIn?.slice(0, 10)}… + {s.createdAt?.slice(0, 16)} +
  • + ))} + {!((cb.monitor as { swaps?: unknown[] })?.swaps?.length) && ( +
  • No swaps recorded yet
  • + )} +
+

+ DBIS Exchange: {DBIS_EXCHANGE_URL} · Token-load & transfers via settlement middleware +

+
+ + +
+
+ ); +} diff --git a/frontend-dapp/src/features/central-bank/ChainMatrixPanel.tsx b/frontend-dapp/src/features/central-bank/ChainMatrixPanel.tsx new file mode 100644 index 0000000..c354aa3 --- /dev/null +++ b/frontend-dapp/src/features/central-bank/ChainMatrixPanel.tsx @@ -0,0 +1,100 @@ +import { useEffect, useState } from 'react'; +import { fetchOmnlChainRegistry, type ChainRegistryResponse, type OmnlChainSummary } from '../../config/supported-chains'; + +type Filter = 'all' | 'active' | 'primary' | 'l2'; + +export default function ChainMatrixPanel() { + const [registry, setRegistry] = useState(null); + const [filter, setFilter] = useState('active'); + const [search, setSearch] = useState(''); + + useEffect(() => { + fetchOmnlChainRegistry().then(setRegistry); + }, []); + + const chains = (registry?.chains ?? []).filter((c) => { + if (filter === 'active' && c.status !== 'active') return false; + if (filter === 'primary' && c.omnlRole !== 'settlement-hub' && c.omnlRole !== 'mirror-target') return false; + if (filter === 'l2' && c.tier !== 'l2') return false; + if (search) { + const q = search.toLowerCase(); + return c.name.toLowerCase().includes(q) || String(c.chainId).includes(q) || c.nativeSymbol.toLowerCase().includes(q); + } + return true; + }); + + return ( +
+
+
+

128-chain network matrix

+

+ {registry?.stats?.active ?? 0} active · {registry?.stats?.staged ?? 0} staged ·{' '} + {registry?.stats?.reserved ?? 0} reserved · primary {registry?.primaryChainId ?? 138} +

+
+ setSearch(e.target.value)} + className="bg-[#2b3139] border border-[#474d57] rounded px-3 py-1.5 text-sm w-48" + /> +
+ +
+ {(['all', 'active', 'primary', 'l2'] as Filter[]).map((f) => ( + + ))} +
+ +
+ + + + + + + + + + + + + {chains.map((c: OmnlChainSummary) => ( + + + + + + + + + ))} + +
IDNetworkRoleStatusSwapBridge
{c.chainId}{c.name}{c.omnlRole} + + {c.status} + + {c.settlement?.swapEnabled ? '✓' : '—'}{c.settlement?.bridgeEnabled ? '✓' : '—'}
+
+
+ ); +} diff --git a/frontend-dapp/src/features/central-bank/useCentralBank.ts b/frontend-dapp/src/features/central-bank/useCentralBank.ts new file mode 100644 index 0000000..3ec084d --- /dev/null +++ b/frontend-dapp/src/features/central-bank/useCentralBank.ts @@ -0,0 +1,60 @@ +import { useCallback, useEffect, useState } from 'react'; +import { DBIS_EXCHANGE_URL, SETTLEMENT_MIDDLEWARE_URL } from '../../config/dex'; + +export type MoneySupply = { + asOf?: string; + officeId?: number; + M0?: { gl1050?: string; backingRatio?: number }; + M1?: { circulating?: string; gl2100?: string }; + M2?: { broadMoney?: string; gl2200?: string; metaFiatPending?: string }; +}; + +export function useCentralBank() { + const [health, setHealth] = useState | null>(null); + const [office, setOffice] = useState | null>(null); + const [moneySupply, setMoneySupply] = useState(null); + const [tokenCount, setTokenCount] = useState(0); + const [monitor, setMonitor] = useState | null>(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const settlement = SETTLEMENT_MIDDLEWARE_URL.replace(/\/$/, ''); + const exchange = DBIS_EXCHANGE_URL.replace(/\/$/, ''); + + const refresh = useCallback(async () => { + setLoading(true); + setError(null); + try { + const [h, o, m2, mon] = await Promise.all([ + fetch(`${settlement}/api/v1/settlement/health`).then((r) => r.json()), + fetch(`${settlement}/api/v1/settlement/office`).then((r) => r.json()), + fetch(`${settlement}/api/v1/settlement/tokens/m2`).then((r) => r.json()), + fetch(`${exchange}/api/v1/exchange/swap/monitor?limit=10`).then((r) => r.json()), + ]); + setHealth(h); + setOffice(o); + setTokenCount(Array.isArray(m2.tokens) ? m2.tokens.length : 0); + setMonitor(mon); + + const apiKey = import.meta.env.VITE_OMNL_API_KEY; + if (apiKey) { + const ms = await fetch(`${settlement}/api/v1/settlement/money-supply`, { + headers: { Authorization: `Bearer ${apiKey}` }, + }); + if (ms.ok) setMoneySupply(await ms.json()); + } + } catch (e) { + setError(e instanceof Error ? e.message : 'Failed to load central bank data'); + } finally { + setLoading(false); + } + }, [settlement, exchange]); + + useEffect(() => { + refresh(); + const id = setInterval(refresh, 30000); + return () => clearInterval(id); + }, [refresh]); + + return { health, office, moneySupply, tokenCount, monitor, loading, error, refresh }; +} diff --git a/frontend-dapp/src/features/office24/Office24Dashboard.tsx b/frontend-dapp/src/features/office24/Office24Dashboard.tsx new file mode 100644 index 0000000..1daea32 --- /dev/null +++ b/frontend-dapp/src/features/office24/Office24Dashboard.tsx @@ -0,0 +1,123 @@ +import { Link } from 'react-router-dom'; +import { useOffice24 } from './useOffice24'; + +export default function Office24Dashboard() { + const { office, tokens, health, loading, refresh } = useOffice24(); + const profile = office as { + officeId?: number; + externalId?: string; + name?: string; + locked?: boolean; + tenantUser?: string; + settlement?: { defaultCurrency?: string; moneyLayers?: string[] }; + rails?: { swift?: { enabled?: boolean }; hybx?: { enabled?: boolean }; chain138?: { enabled?: boolean } }; + } | null; + + return ( +
+
+
+
+ + Office 24 · Locked + + {profile?.locked && Single-office enforcement} +
+

{profile?.name ?? 'HOSPITALLERS Ali Iraq-Iran'}

+

{profile?.externalId ?? 'HOSPITALLERS-ALI-IRAQ-IRAN'}

+
+ + Open DBIS Trade → + +
+ +
+
+

Office ID

+

{profile?.officeId ?? 24}

+
+
+

Default currency

+

{profile?.settlement?.defaultCurrency ?? 'USD'}

+
+
+

M2 tradable tokens

+

{tokens.length}

+
+
+ +
+
+

Settlement rails

+
    +
  • + SWIFT + + {profile?.rails?.swift?.enabled ? 'Enabled' : 'Off'} + +
  • +
  • + HYBX + + {profile?.rails?.hybx?.enabled ? 'Enabled' : 'Off'} + +
  • +
  • + Chain 138 + + {profile?.rails?.chain138?.enabled ? 'Enabled' : 'Off'} + +
  • +
  • + Exchange + {(health as { status?: string })?.status ?? 'ok'} +
  • +
+

Tenant: {profile?.tenantUser ?? 'ali_hospitallers_tenant'}

+
+ +
+
+

M2 token registry — swap · convert · transfer

+ +
+
+ + + + + + + + + + + + {tokens.map((t) => ( + + + + + + + + ))} + +
SymbolLineSwapConvertXfer
{t.symbol}{t.omnlLine ?? 'M2'}{t.swappable !== false ? '✓' : '—'}{t.convertible !== false ? '✓' : '—'} + {t.transferableInternal && t.transferableExternal ? 'Int+Ext' : '✓'} +
+
+
+
+
+ ); +} diff --git a/frontend-dapp/src/features/office24/useOffice24.ts b/frontend-dapp/src/features/office24/useOffice24.ts new file mode 100644 index 0000000..2011711 --- /dev/null +++ b/frontend-dapp/src/features/office24/useOffice24.ts @@ -0,0 +1,42 @@ +import { useCallback, useEffect, useState } from 'react'; +import { DBIS_EXCHANGE_URL, SETTLEMENT_MIDDLEWARE_URL, type DexToken } from '../../config/dex'; + +export type M2Token = DexToken & { + moneyLayer?: string; + swappable?: boolean; + convertible?: boolean; + transferableInternal?: boolean; + transferableExternal?: boolean; +}; + +export function useOffice24() { + const [office, setOffice] = useState | null>(null); + const [tokens, setTokens] = useState([]); + const [health, setHealth] = useState | null>(null); + const [loading, setLoading] = useState(true); + + const settlement = SETTLEMENT_MIDDLEWARE_URL.replace(/\/$/, ''); + const exchange = DBIS_EXCHANGE_URL.replace(/\/$/, ''); + + const refresh = useCallback(async () => { + setLoading(true); + try { + const [o, t, h] = await Promise.all([ + fetch(`${settlement}/api/v1/settlement/office`).then((r) => r.json()), + fetch(`${settlement}/api/v1/settlement/tokens/m2`).then((r) => r.json()), + fetch(`${exchange}/api/v1/exchange/health`).then((r) => r.json()), + ]); + setOffice(o); + setTokens(Array.isArray(t.tokens) ? t.tokens : []); + setHealth(h); + } finally { + setLoading(false); + } + }, [settlement, exchange]); + + useEffect(() => { + refresh(); + }, [refresh]); + + return { office, tokens, health, loading, refresh }; +} diff --git a/frontend-dapp/src/features/swap/DBISSwapPanel.tsx b/frontend-dapp/src/features/swap/DBISSwapPanel.tsx new file mode 100644 index 0000000..bb2f8d3 --- /dev/null +++ b/frontend-dapp/src/features/swap/DBISSwapPanel.tsx @@ -0,0 +1,298 @@ +import { useEffect } from 'react'; +import { useDbisSwap } from './useDbisSwap'; +import { formatAmountFromWei } from '../../config/dex'; + +function TokenSelect({ + label, + value, + onChange, + tokens, +}: { + label: string; + value: { symbol: string; address: string }; + onChange: (t: (typeof tokens)[0]) => void; + tokens: ReturnType['tokens']; +}) { + return ( + + ); +} + +export default function DBISSwapPanel() { + const swap = useDbisSwap(); + + useEffect(() => { + swap.loadMonitor(); + }, [swap.loadMonitor]); + + useEffect(() => { + if (swap.isConnected) swap.loadLedgers(); + }, [swap.isConnected, swap.address, swap.loadLedgers]); + + const outDisplay = + swap.plan?.amountOut && swap.tokenOut + ? formatAmountFromWei(String(swap.plan.amountOut), swap.tokenOut.decimals) + : swap.plan?.plannerResponse?.routePlan?.minAmountOut + ? formatAmountFromWei(String(swap.plan.plannerResponse.routePlan.minAmountOut), swap.tokenOut.decimals) + : '—'; + + return ( +
+
+
+
+

DBIS Exchange

+ Office 24 · M2 · {swap.tokens.length} tokens +
+ +
+ +
+ +
+ + + + +
+ Slippage + {[25, 50, 100].map((bps) => ( + + ))} +
+ +
+

Expected output

+

+ {outDisplay} {swap.tokenOut.symbol} +

+ {swap.plan?.plannerResponse?.routePlan?.legs && ( +

+ Route:{' '} + {swap.plan.plannerResponse.routePlan.legs + .map((l: { provider?: string }) => l.provider) + .filter(Boolean) + .join(' → ')} +

+ )} +
+ + {swap.error &&

{swap.error}

} + {swap.txHash && ( +

+ Tx:{' '} + + {swap.txHash} + +

+ )} + +
+ + +
+
+
+
+ +
+
+

OMNL Office 24 ledger

+ {!swap.isConnected ? ( +

Connect wallet for crypto balances

+ ) : swap.ledger ? ( +
+

+ M1 circulating:{' '} + {(swap.ledger as { fundLedger?: { M1?: { circulating?: string } } }).fundLedger?.M1?.circulating ?? '—'} +

+

+ M2 broad:{' '} + {(swap.ledger as { fundLedger?: { M2?: { broadMoney?: string } } }).fundLedger?.M2?.broadMoney ?? '—'} +

+ {(swap.ledger as { cryptoLedger?: { balances?: Record } }).cryptoLedger?.balances && ( +
+

Wallet balances

+ {Object.entries( + (swap.ledger as { cryptoLedger: { balances: Record } }).cryptoLedger.balances, + ).map(([sym, bal]) => ( +

+ {sym}: {Number(bal).toFixed(4)} +

+ ))} +
+ )} +
+ ) : ( + + )} +
+ +
+

M2 token load

+

Load from M2 fiat (GL 2200) → on-chain · swappable · convertible

+ swap.setLoadAmount(e.target.value)} + className="ui-input w-full mb-2 text-sm" + /> + +
+ +
+

Transfer

+ swap.setTransferTo(e.target.value)} + className="ui-input w-full mb-2 text-sm font-mono" + /> + swap.setTransferAmount(e.target.value)} + className="ui-input w-full mb-2 text-sm" + /> +
+ +
+ swap.setExternalIban(e.target.value)} + className="ui-input w-full mb-2 text-sm font-mono" + /> + +
+ + {swap.lastSettlement && ( +
+

Last settlement

+

+ {(swap.lastSettlement as { phase?: string }).phase ?? 'OK'} ·{' '} + {(swap.lastSettlement as { settlementId?: string }).settlementId ?? '—'} +

+
+ )} + +
+

Swap monitor

+ {swap.monitor ? ( +
+

+ Total: {(swap.monitor as { stats?: { total?: number } }).stats?.total ?? 0} · 24h:{' '} + {(swap.monitor as { stats?: { last24h?: number } }).stats?.last24h ?? 0} +

+
    + {((swap.monitor as { swaps?: { tokenIn: string; status: string; createdAt: string }[] }).swaps ?? []) + .slice(0, 5) + .map((s, i) => ( +
  • + {s.status} · {s.createdAt.slice(0, 16)} +
  • + ))} +
+
+ ) : ( +

Loading…

+ )} +
+
+
+ ); +} diff --git a/frontend-dapp/src/features/swap/useDbisSwap.ts b/frontend-dapp/src/features/swap/useDbisSwap.ts new file mode 100644 index 0000000..7075df2 --- /dev/null +++ b/frontend-dapp/src/features/swap/useDbisSwap.ts @@ -0,0 +1,297 @@ +import { useCallback, useEffect, useState } from 'react'; +import { useAccount, useSendTransaction } from 'wagmi'; +import { + DBIS_EXCHANGE_URL, + DEFAULT_TOKENS, + fetchExchangeTokens, + parseAmountToWei, + type DexToken, +} from '../../config/dex'; + +export type SwapPlan = { + amountOut?: string; + plannerResponse?: { + routePlan?: { minAmountOut?: string; legs?: { provider?: string }[] }; + }; + execution?: { contractAddress: string; encodedCalldata: string }; + error?: string; +}; + +export function useDbisSwap() { + const { address, isConnected } = useAccount(); + const { sendTransactionAsync } = useSendTransaction(); + const [tokens, setTokens] = useState(DEFAULT_TOKENS); + const [tokenIn, setTokenIn] = useState(DEFAULT_TOKENS[4]); + const [tokenOut, setTokenOut] = useState(DEFAULT_TOKENS[5]); + const [amountIn, setAmountIn] = useState(''); + const [slippageBps, setSlippageBps] = useState(50); + const [plan, setPlan] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [txHash, setTxHash] = useState(); + const [ledger, setLedger] = useState | null>(null); + const [monitor, setMonitor] = useState | null>(null); + const [transferTo, setTransferTo] = useState(''); + const [transferAmount, setTransferAmount] = useState(''); + const [externalIban, setExternalIban] = useState(''); + const [loadAmount, setLoadAmount] = useState(''); + const [lastSettlement, setLastSettlement] = useState | null>(null); + + const baseUrl = DBIS_EXCHANGE_URL.replace(/\/$/, ''); + + useEffect(() => { + fetchExchangeTokens().then((list) => { + setTokens(list); + setTokenIn(list.find((t) => t.symbol === 'cUSDT') ?? list[4] ?? list[0]); + setTokenOut(list.find((t) => t.symbol === 'cUSDC') ?? list[5] ?? list[1]); + }); + }, []); + + const loadLedgers = useCallback(async () => { + if (!address) return; + try { + const q = new URLSearchParams({ wallet: address }); + const res = await fetch(`${baseUrl}/api/v1/exchange/ledgers?${q}`); + if (res.ok) setLedger(await res.json()); + } catch { + /* optional */ + } + }, [address, baseUrl]); + + const fetchQuote = useCallback(async () => { + if (!amountIn) return; + setLoading(true); + setError(null); + try { + const wei = parseAmountToWei(amountIn, tokenIn.decimals); + const q = new URLSearchParams({ + tokenIn: tokenIn.address, + tokenOut: tokenOut.address, + amountIn: wei, + }); + const res = await fetch(`${baseUrl}/api/v1/exchange/quote?${q}`); + if (!res.ok) throw new Error(await res.text()); + const quote = await res.json(); + setPlan({ amountOut: quote.amountOut ?? quote.expectedOutput }); + } catch (e) { + setError(e instanceof Error ? e.message : 'Quote failed'); + } finally { + setLoading(false); + } + }, [amountIn, tokenIn, tokenOut, baseUrl]); + + const fetchPlan = useCallback(async () => { + if (!amountIn || !address) return; + setLoading(true); + setError(null); + try { + const wei = parseAmountToWei(amountIn, tokenIn.decimals); + const res = await fetch(`${baseUrl}/api/v1/exchange/execute-plan`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + tokenIn: tokenIn.address, + tokenOut: tokenOut.address, + amountIn: wei, + recipient: address, + maxSlippageBps: slippageBps, + }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Plan failed'); + setPlan(data); + } catch (e) { + setError(e instanceof Error ? e.message : 'Plan failed'); + } finally { + setLoading(false); + } + }, [amountIn, tokenIn, tokenOut, address, slippageBps, baseUrl]); + + const settleSwap = useCallback( + async (hash: string) => { + try { + const res = await fetch(`${baseUrl}/api/v1/exchange/swap/settle`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + amount: amountIn, + recipientAddress: address, + currency: tokenIn.currencyCode ?? 'USD', + tokenLineId: tokenOut.omnlLine ?? `${tokenOut.symbol}-M2`, + tokenSymbol: tokenOut.symbol, + remittanceInfo: `Swap ${tokenIn.symbol}→${tokenOut.symbol} tx ${hash}`, + }), + }); + if (res.ok) setLastSettlement(await res.json()); + } catch { + /* optional M2 audit */ + } + }, + [baseUrl, amountIn, address, tokenIn, tokenOut], + ); + + const executeSwap = useCallback(async () => { + if (!plan?.execution || !address) { + await fetchPlan(); + return; + } + setLoading(true); + setError(null); + try { + const hash = await sendTransactionAsync({ + to: plan.execution.contractAddress as `0x${string}`, + data: plan.execution.encodedCalldata as `0x${string}`, + }); + setTxHash(hash); + await fetch(`${baseUrl}/api/v1/exchange/swap/record`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + tokenIn: tokenIn.address, + tokenOut: tokenOut.address, + amountIn: parseAmountToWei(amountIn, tokenIn.decimals), + wallet: address, + txHash: hash, + status: 'SUBMITTED', + routeProviders: plan.plannerResponse?.routePlan?.legs?.map((l) => l.provider).filter(Boolean), + }), + }); + await settleSwap(hash); + } catch (e) { + setError(e instanceof Error ? e.message : 'Swap failed'); + } finally { + setLoading(false); + } + }, [plan, address, sendTransactionAsync, fetchPlan, tokenIn, tokenOut, amountIn, baseUrl, settleSwap]); + + const loadM2Token = useCallback(async () => { + if (!address || !loadAmount) return; + setLoading(true); + setError(null); + try { + const res = await fetch(`${baseUrl}/api/v1/exchange/token-load`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + idempotencyKey: `LOAD-${Date.now()}`, + tokenAddress: tokenOut.address, + symbol: tokenOut.symbol, + lineId: tokenOut.omnlLine ?? 'USD-M2', + amount: loadAmount, + recipientAddress: address, + currency: tokenOut.currencyCode ?? 'USD', + }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || data.errors?.join(', ') || 'Token load failed'); + setLastSettlement(data); + await loadLedgers(); + } catch (e) { + setError(e instanceof Error ? e.message : 'M2 token load failed'); + } finally { + setLoading(false); + } + }, [address, loadAmount, tokenOut, baseUrl, loadLedgers]); + + const internalTransfer = useCallback(async () => { + if (!address || !transferTo || !transferAmount) return; + setLoading(true); + setError(null); + try { + const res = await fetch(`${baseUrl}/api/v1/exchange/transfer/internal`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + idempotencyKey: `XFER-INT-${Date.now()}`, + tokenAddress: tokenIn.address, + tokenSymbol: tokenIn.symbol, + amount: transferAmount, + recipientAddress: transferTo, + senderAddress: address, + }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || data.errors?.join(', ') || 'Transfer failed'); + setLastSettlement(data); + } catch (e) { + setError(e instanceof Error ? e.message : 'Internal transfer failed'); + } finally { + setLoading(false); + } + }, [address, transferTo, transferAmount, tokenIn, baseUrl]); + + const externalTransfer = useCallback(async () => { + if (!address || !transferTo || !transferAmount || !externalIban) return; + setLoading(true); + setError(null); + try { + const res = await fetch(`${baseUrl}/api/v1/exchange/transfer/external`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + idempotencyKey: `XFER-EXT-${Date.now()}`, + tokenAddress: tokenIn.address, + tokenSymbol: tokenIn.symbol, + amount: transferAmount, + recipientAddress: transferTo, + creditorIban: externalIban, + remittanceInfo: `OMNL M2 external ${tokenIn.symbol}`, + }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || data.errors?.join(', ') || 'External transfer failed'); + setLastSettlement(data); + } catch (e) { + setError(e instanceof Error ? e.message : 'External transfer failed'); + } finally { + setLoading(false); + } + }, [address, transferTo, transferAmount, externalIban, tokenIn, baseUrl]); + + const loadMonitor = useCallback(async () => { + try { + const res = await fetch(`${baseUrl}/api/v1/exchange/swap/monitor?limit=20`); + if (res.ok) setMonitor(await res.json()); + } catch { + /* optional */ + } + }, [baseUrl]); + + return { + address, + isConnected, + tokens, + tokenIn, + setTokenIn, + tokenOut, + setTokenOut, + amountIn, + setAmountIn, + slippageBps, + setSlippageBps, + plan, + loading, + error, + txHash, + ledger, + monitor, + transferTo, + setTransferTo, + transferAmount, + setTransferAmount, + externalIban, + setExternalIban, + loadAmount, + setLoadAmount, + lastSettlement, + fetchQuote, + fetchPlan, + executeSwap, + loadM2Token, + internalTransfer, + externalTransfer, + loadLedgers, + loadMonitor, + parseAmountToWei, + }; +} diff --git a/frontend-dapp/src/features/trade/BinanceTradePanel.tsx b/frontend-dapp/src/features/trade/BinanceTradePanel.tsx new file mode 100644 index 0000000..3a35692 --- /dev/null +++ b/frontend-dapp/src/features/trade/BinanceTradePanel.tsx @@ -0,0 +1,325 @@ +import { useEffect, useMemo, useState } from 'react'; +import { useDbisSwap } from '../swap/useDbisSwap'; +import { formatAmountFromWei, type DexToken } from '../../config/dex'; +import TokenIcon from '../../components/ui/TokenIcon'; + +type Side = 'buy' | 'sell'; + +function MarketRow({ + token, + quote, + active, + onClick, +}: { + token: DexToken; + quote: string; + active: boolean; + onClick: () => void; +}) { + return ( + + ); +} + +function OrderBookMock({ price }: { price: string }) { + const base = parseFloat(price) || 1; + const rows = Array.from({ length: 8 }, (_, i) => { + const p = base * (1 + (4 - i) * 0.001); + const amt = (Math.random() * 2 + 0.1).toFixed(4); + return { price: p.toFixed(4), amount: amt, side: i < 4 ? 'ask' : 'bid' }; + }); + return ( +
+
+ Price + Amount +
+ {rows.slice(0, 4).map((r) => ( +
+ {r.price} + {r.amount} +
+ ))} +
+ {price || '—'} +
+ {rows.slice(4).map((r) => ( +
+ {r.price} + {r.amount} +
+ ))} +
+ ); +} + +export default function BinanceTradePanel() { + const swap = useDbisSwap(); + const [side, setSide] = useState('buy'); + const [marketSearch, setMarketSearch] = useState(''); + const [orderType, setOrderType] = useState<'market' | 'limit'>('market'); + + useEffect(() => { + swap.loadMonitor(); + }, [swap.loadMonitor]); + + useEffect(() => { + if (swap.isConnected) swap.loadLedgers(); + }, [swap.isConnected, swap.address, swap.loadLedgers]); + + const quoteSymbol = 'cUSDT'; + const quoteToken = swap.tokens.find((t) => t.symbol === quoteSymbol) ?? swap.tokens.find((t) => t.symbol.includes('USDT')); + + const markets = useMemo(() => { + const q = marketSearch.trim().toLowerCase(); + return swap.tokens.filter( + (t) => + !t.native && + t.symbol !== quoteToken?.symbol && + (!q || t.symbol.toLowerCase().includes(q) || t.name.toLowerCase().includes(q)), + ); + }, [swap.tokens, marketSearch, quoteToken]); + + const selectMarket = (base: DexToken) => { + if (side === 'buy') { + swap.setTokenIn(quoteToken ?? swap.tokenIn); + swap.setTokenOut(base); + } else { + swap.setTokenIn(base); + swap.setTokenOut(quoteToken ?? swap.tokenOut); + } + }; + + const outDisplay = + swap.plan?.amountOut && swap.tokenOut + ? formatAmountFromWei(String(swap.plan.amountOut), swap.tokenOut.decimals) + : '—'; + + const displayPrice = + swap.amountIn && outDisplay !== '—' + ? (parseFloat(outDisplay) / parseFloat(swap.amountIn || '1')).toFixed(6) + : '—'; + + return ( +
+ {/* Markets sidebar */} + + + {/* Chart + order book */} +
+
+
+ +

+ {swap.tokenOut.symbol}/{quoteToken?.symbol ?? 'USDT'} +

+
+ {displayPrice} + Office 24 · Chain 138 + 37 M2 tokens · tradable & swappable +
+ +
+
+
+ {Array.from({ length: 24 }, (_, i) => ( +
+ ))} +
+

Live chart · connect WebSocket feed for production

+
+
+

Order book

+ +
+
+ +
+

Recent trades

+
+ {((swap.monitor as { swaps?: { status: string; amountIn: string; createdAt: string }[] })?.swaps ?? []) + .slice(0, 6) + .map((s, i) => ( + + {s.status} + {s.amountIn?.slice(0, 12)} + {s.createdAt?.slice(11, 19)} + + ))} +
+
+
+ + {/* Trade panel */} + +
+ ); +} diff --git a/frontend-dapp/src/pages/CentralBankPage.tsx b/frontend-dapp/src/pages/CentralBankPage.tsx new file mode 100644 index 0000000..cdd635a --- /dev/null +++ b/frontend-dapp/src/pages/CentralBankPage.tsx @@ -0,0 +1,5 @@ +import CentralBankDashboard from '../features/central-bank/CentralBankDashboard'; + +export default function CentralBankPage() { + return ; +} diff --git a/frontend-dapp/src/pages/Office24Page.tsx b/frontend-dapp/src/pages/Office24Page.tsx new file mode 100644 index 0000000..29aa6f1 --- /dev/null +++ b/frontend-dapp/src/pages/Office24Page.tsx @@ -0,0 +1,5 @@ +import Office24Dashboard from '../features/office24/Office24Dashboard'; + +export default function Office24Page() { + return ; +} diff --git a/frontend-dapp/src/pages/SwapPage.tsx b/frontend-dapp/src/pages/SwapPage.tsx index 93446ac..14d8fb5 100644 --- a/frontend-dapp/src/pages/SwapPage.tsx +++ b/frontend-dapp/src/pages/SwapPage.tsx @@ -1,9 +1,13 @@ +import PageShell from '../components/ui/PageShell' +import DBISSwapPanel from '../features/swap/DBISSwapPanel' + export default function SwapPage() { return ( -
-

Swap

-

DEX swap interface coming soon...

-
+ + + ) } - diff --git a/frontend-dapp/src/pages/TradePage.tsx b/frontend-dapp/src/pages/TradePage.tsx new file mode 100644 index 0000000..fb4a658 --- /dev/null +++ b/frontend-dapp/src/pages/TradePage.tsx @@ -0,0 +1,5 @@ +import BinanceTradePanel from '../features/trade/BinanceTradePanel'; + +export default function TradePage() { + return ; +} diff --git a/frontend-dapp/src/styles/omnl-apps.css b/frontend-dapp/src/styles/omnl-apps.css new file mode 100644 index 0000000..63e00d1 --- /dev/null +++ b/frontend-dapp/src/styles/omnl-apps.css @@ -0,0 +1,58 @@ +/* OMNL product UIs — Central Bank, Office 24, Binance-style trade */ + +.cb-page, +.o24-page { + min-height: calc(100vh - 56px); +} + +.cb-card, +.o24-card { + background: #1e2329; + border: 1px solid #2b3139; + border-radius: 4px; + padding: 1.25rem; +} + +/* Binance-style trade terminal */ +.trade-layout { + display: grid; + grid-template-columns: 240px 1fr; + grid-template-rows: 1fr; + min-height: calc(100vh - 56px); + background: #0b0e11; +} + +@media (max-width: 1023px) { + .trade-layout { + grid-template-columns: 1fr; + grid-template-rows: auto 1fr auto; + } +} + +@media (min-width: 1024px) { + .trade-layout { + grid-template-columns: 240px 1fr 320px; + } +} + +.trade-center { + min-height: 0; +} + +.trade-market-row:hover { + background: #2b3139; +} + +.trade-market-row--active { + background: #2b3139; + border-left: 2px solid #f0b90b; +} + +.trade-orderbook { + max-height: 360px; + overflow-y: auto; +} + +.omnl-app-header { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35); +} diff --git a/packages/settlement-core/dist/chain-registry.d.ts b/packages/settlement-core/dist/chain-registry.d.ts new file mode 100644 index 0000000..c9a4239 --- /dev/null +++ b/packages/settlement-core/dist/chain-registry.d.ts @@ -0,0 +1,39 @@ +export type OmnlChainEntry = { + chainId: number; + name: string; + type: 'evm' | 'non-evm'; + nativeSymbol: string; + tier: string; + omnlRole: string; + status: 'active' | 'staged' | 'reserved'; + rpcEnv: string; + explorerUrl?: string; + rpcDefault?: string; + settlement: { + m2LoadEnabled: boolean; + swapEnabled: boolean; + bridgeEnabled: boolean; + officeId: number; + }; + complianceEnv?: string; +}; +export type OmnlChainRegistry = { + version: string; + brand: string; + maxCapacity: number; + primaryChainId: number; + mirrorChainId: number; + settlementOfficeId: number; + chains: OmnlChainEntry[]; + stats?: { + total: number; + active: number; + staged: number; + reserved: number; + }; +}; +export declare function resolveChainRegistryPath(): string; +export declare function loadOmnlChainRegistry(): OmnlChainRegistry; +export declare function getActiveChains(): OmnlChainEntry[]; +export declare function getChainById(chainId: number): OmnlChainEntry | undefined; +export declare function getSupportedChainIds128(): number[]; diff --git a/packages/settlement-core/dist/chain-registry.js b/packages/settlement-core/dist/chain-registry.js new file mode 100644 index 0000000..3628deb --- /dev/null +++ b/packages/settlement-core/dist/chain-registry.js @@ -0,0 +1,43 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.resolveChainRegistryPath = resolveChainRegistryPath; +exports.loadOmnlChainRegistry = loadOmnlChainRegistry; +exports.getActiveChains = getActiveChains; +exports.getChainById = getChainById; +exports.getSupportedChainIds128 = getSupportedChainIds128; +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +let cached = null; +function resolveChainRegistryPath() { + if (process.env.OMNL_SUPPORTED_CHAINS_CONFIG) + return process.env.OMNL_SUPPORTED_CHAINS_CONFIG; + const candidates = [ + path_1.default.resolve(process.cwd(), 'config/omnl-supported-chains.v1.json'), + path_1.default.resolve(__dirname, '../../../config/omnl-supported-chains.v1.json'), + path_1.default.resolve(__dirname, '../../../../config/omnl-supported-chains.v1.json'), + ]; + for (const p of candidates) { + if (fs_1.default.existsSync(p)) + return p; + } + return candidates[1]; +} +function loadOmnlChainRegistry() { + if (cached) + return cached; + const p = resolveChainRegistryPath(); + cached = JSON.parse(fs_1.default.readFileSync(p, 'utf8')); + return cached; +} +function getActiveChains() { + return loadOmnlChainRegistry().chains.filter((c) => c.status === 'active'); +} +function getChainById(chainId) { + return loadOmnlChainRegistry().chains.find((c) => c.chainId === chainId); +} +function getSupportedChainIds128() { + return loadOmnlChainRegistry().chains.map((c) => c.chainId); +} diff --git a/packages/settlement-core/dist/index.d.ts b/packages/settlement-core/dist/index.d.ts new file mode 100644 index 0000000..e31b7fa --- /dev/null +++ b/packages/settlement-core/dist/index.d.ts @@ -0,0 +1,7 @@ +export * from './types'; +export * from './money-supply'; +export * from './instruments'; +export * from './verbiage'; +export * from './state-machine'; +export * from './m2-token-registry'; +export * from './chain-registry'; diff --git a/packages/settlement-core/dist/index.js b/packages/settlement-core/dist/index.js new file mode 100644 index 0000000..519b4d9 --- /dev/null +++ b/packages/settlement-core/dist/index.js @@ -0,0 +1,23 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +__exportStar(require("./types"), exports); +__exportStar(require("./money-supply"), exports); +__exportStar(require("./instruments"), exports); +__exportStar(require("./verbiage"), exports); +__exportStar(require("./state-machine"), exports); +__exportStar(require("./m2-token-registry"), exports); +__exportStar(require("./chain-registry"), exports); diff --git a/packages/settlement-core/dist/instruments.d.ts b/packages/settlement-core/dist/instruments.d.ts new file mode 100644 index 0000000..aaf8a22 --- /dev/null +++ b/packages/settlement-core/dist/instruments.d.ts @@ -0,0 +1,13 @@ +import type { SwiftMessageKind, TradeFinanceInstrument } from './types'; +/** SWIFT / trade-finance instrument routing */ +export declare const INSTRUMENT_ROUTING: Record; +export declare function resolveInstrumentGl(instrument: TradeFinanceInstrument): { + debitGl: string; + creditGl: string; + swiftKinds: SwiftMessageKind[]; +}; +export declare function isIbanValid(iban: string): boolean; diff --git a/packages/settlement-core/dist/instruments.js b/packages/settlement-core/dist/instruments.js new file mode 100644 index 0000000..fb8014b --- /dev/null +++ b/packages/settlement-core/dist/instruments.js @@ -0,0 +1,28 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.INSTRUMENT_ROUTING = void 0; +exports.resolveInstrumentGl = resolveInstrumentGl; +exports.isIbanValid = isIbanValid; +/** SWIFT / trade-finance instrument routing */ +exports.INSTRUMENT_ROUTING = { + SBLC: { swift: ['MT103', 'MT202'], glDebit: '1410', glCredit: '2100' }, + DLC: { swift: ['MT103', 'MT102'], glDebit: '1410', glCredit: '1000' }, + BG: { swift: ['MT103'], glDebit: '1410', glCredit: '2100' }, + LS: { swift: ['MT102', 'MT103'], glDebit: '1000', glCredit: '2200' }, +}; +function resolveInstrumentGl(instrument) { + const r = exports.INSTRUMENT_ROUTING[instrument]; + return { debitGl: r.glDebit, creditGl: r.glCredit, swiftKinds: r.swift }; +} +function isIbanValid(iban) { + const normalized = iban.replace(/\s/g, '').toUpperCase(); + if (!/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(normalized)) + return false; + const rearranged = normalized.slice(4) + normalized.slice(0, 4); + const numeric = rearranged.replace(/[A-Z]/g, (c) => String(c.charCodeAt(0) - 55)); + let remainder = 0; + for (const ch of numeric) { + remainder = (remainder * 10 + parseInt(ch, 10)) % 97; + } + return remainder === 1; +} diff --git a/packages/settlement-core/dist/m2-token-registry.d.ts b/packages/settlement-core/dist/m2-token-registry.d.ts new file mode 100644 index 0000000..8dae52a --- /dev/null +++ b/packages/settlement-core/dist/m2-token-registry.d.ts @@ -0,0 +1,44 @@ +import type { MoneyLayer } from './types'; +export type M2TokenCapabilities = { + symbol: string; + name: string; + address: string; + decimals: number; + native?: boolean; + currencyCode?: string; + moneyLayer: MoneyLayer; + omnlLine: string; + loadFromGl: string; + swappable: boolean; + convertible: boolean; + transferableInternal: boolean; + transferableExternal: boolean; +}; +export type M2TokenRegistryFile = { + version: string; + chainId: number; + defaultMoneyLayer: MoneyLayer; + defaultLoadFromGl: string; + tokens: M2TokenCapabilities[]; +}; +export declare function inferCurrencyCode(symbol: string): string; +export declare function resolveOmnlLineM2(currencyCode?: string, symbol?: string): string; +export declare function withM2Capabilities(token: { + symbol: string; + name: string; + address: string; + decimals: number; + native?: boolean; + currencyCode?: string; + omnlLine?: string; +}, overrides?: Partial): M2TokenCapabilities; +export declare function mergeExchangeTokensWithM2Registry(baseTokens: { + symbol: string; + name: string; + address: string; + decimals: number; + native?: boolean; + omnlLine?: string; +}[], registry: M2TokenRegistryFile): M2TokenCapabilities[]; +export declare function findM2TokenByAddress(registry: M2TokenRegistryFile, address: string): M2TokenCapabilities | undefined; +export declare function findM2TokenBySymbol(registry: M2TokenRegistryFile, symbol: string): M2TokenCapabilities | undefined; diff --git a/packages/settlement-core/dist/m2-token-registry.js b/packages/settlement-core/dist/m2-token-registry.js new file mode 100644 index 0000000..3244ab4 --- /dev/null +++ b/packages/settlement-core/dist/m2-token-registry.js @@ -0,0 +1,86 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.inferCurrencyCode = inferCurrencyCode; +exports.resolveOmnlLineM2 = resolveOmnlLineM2; +exports.withM2Capabilities = withM2Capabilities; +exports.mergeExchangeTokensWithM2Registry = mergeExchangeTokensWithM2Registry; +exports.findM2TokenByAddress = findM2TokenByAddress; +exports.findM2TokenBySymbol = findM2TokenBySymbol; +const money_supply_1 = require("./money-supply"); +const CURRENCY_FROM_SYMBOL = { + USD: 'USD', + EUR: 'EUR', + GBP: 'GBP', + AUD: 'AUD', + JPY: 'JPY', + CHF: 'CHF', + CAD: 'CAD', + XAU: 'XAU', + BTC: 'BTC', + ETH: 'ETH', + BNB: 'BNB', + POL: 'POL', + AVAX: 'AVAX', + CRO: 'CRO', + XDAI: 'XDAI', + CELO: 'CELO', + WEMIX: 'WEMIX', +}; +function inferCurrencyCode(symbol) { + const s = symbol.toUpperCase(); + for (const [key, code] of Object.entries(CURRENCY_FROM_SYMBOL)) { + if (s.includes(key)) + return code; + } + if (s.startsWith('C') && s.length > 1) { + for (const [key, code] of Object.entries(CURRENCY_FROM_SYMBOL)) { + if (s.slice(1).includes(key)) + return code; + } + } + return 'USD'; +} +function resolveOmnlLineM2(currencyCode, symbol) { + const ccy = (currencyCode || inferCurrencyCode(symbol ?? 'USD')).toUpperCase(); + return `${ccy}-M2`; +} +function withM2Capabilities(token, overrides = {}) { + const currencyCode = token.currencyCode ?? inferCurrencyCode(token.symbol); + return { + symbol: token.symbol, + name: token.name, + address: token.address, + decimals: token.decimals, + native: token.native, + currencyCode, + moneyLayer: 'M2', + omnlLine: token.omnlLine ?? resolveOmnlLineM2(currencyCode, token.symbol), + loadFromGl: money_supply_1.GL_CODES.M2_BROAD, + swappable: true, + convertible: true, + transferableInternal: true, + transferableExternal: true, + ...overrides, + }; +} +function mergeExchangeTokensWithM2Registry(baseTokens, registry) { + const byAddress = new Map(); + for (const t of registry.tokens) { + byAddress.set(t.address.toLowerCase(), t); + } + for (const t of baseTokens) { + const key = t.address.toLowerCase(); + if (!byAddress.has(key)) { + byAddress.set(key, withM2Capabilities(t)); + } + } + return Array.from(byAddress.values()).sort((a, b) => a.symbol.localeCompare(b.symbol)); +} +function findM2TokenByAddress(registry, address) { + const lower = address.toLowerCase(); + return registry.tokens.find((t) => t.address.toLowerCase() === lower); +} +function findM2TokenBySymbol(registry, symbol) { + const s = symbol.trim().toLowerCase(); + return registry.tokens.find((t) => t.symbol.toLowerCase() === s); +} diff --git a/packages/settlement-core/dist/money-supply.d.ts b/packages/settlement-core/dist/money-supply.d.ts new file mode 100644 index 0000000..753c55f --- /dev/null +++ b/packages/settlement-core/dist/money-supply.d.ts @@ -0,0 +1,26 @@ +import type { MoneyLayer, MoneySupplySnapshot } from './types'; +/** IPSAS GL mapping for OMNL central-bank money aggregates */ +export declare const GL_CODES: { + readonly M0_RESERVE: "1050"; + readonly M1_LIABILITY: "2000"; + readonly M1_CIRCULATING: "2100"; + readonly M2_BROAD: "2200"; + readonly SETTLEMENT_SUSPENSE: "1000"; + readonly TRADE_FINANCE_CONTINGENT: "1410"; + readonly CRYPTO_TOKEN_LIABILITY: "2300"; +}; +/** Policy caps from hybx-omnl-policy (1.2× M0 reserve → M1 capacity; 5× M1 → M2 broad) */ +export declare const POLICY: { + readonly M0_BACKING_MULTIPLIER: 1.2; + readonly M1_CAP_MULTIPLIER: 5; + readonly M2_META_FIAT_RAIL_CONVERSION: true; +}; +export declare function aggregateMetaFiatLayers(layers: Partial>): { + metaFiatTotal: string; + breakdown: Record; +}; +export declare function buildMoneySupplySnapshot(officeId: number, balances: Partial>): MoneySupplySnapshot; +export declare function m2FiatToTokenLoadAmount(fiatAmount: string, m2Available: string): { + loadAmount: string; + fullyBacked: boolean; +}; diff --git a/packages/settlement-core/dist/money-supply.js b/packages/settlement-core/dist/money-supply.js new file mode 100644 index 0000000..42b169e --- /dev/null +++ b/packages/settlement-core/dist/money-supply.js @@ -0,0 +1,74 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.POLICY = exports.GL_CODES = void 0; +exports.aggregateMetaFiatLayers = aggregateMetaFiatLayers; +exports.buildMoneySupplySnapshot = buildMoneySupplySnapshot; +exports.m2FiatToTokenLoadAmount = m2FiatToTokenLoadAmount; +/** IPSAS GL mapping for OMNL central-bank money aggregates */ +exports.GL_CODES = { + M0_RESERVE: '1050', + M1_LIABILITY: '2000', + M1_CIRCULATING: '2100', + M2_BROAD: '2200', + SETTLEMENT_SUSPENSE: '1000', + TRADE_FINANCE_CONTINGENT: '1410', + CRYPTO_TOKEN_LIABILITY: '2300', +}; +/** Policy caps from hybx-omnl-policy (1.2× M0 reserve → M1 capacity; 5× M1 → M2 broad) */ +exports.POLICY = { + M0_BACKING_MULTIPLIER: 1.2, + M1_CAP_MULTIPLIER: 5, + M2_META_FIAT_RAIL_CONVERSION: true, +}; +function aggregateMetaFiatLayers(layers) { + const breakdown = { + M0: layers.M0 ?? '0', + M1: layers.M1 ?? '0', + M2: layers.M2 ?? '0', + M00: layers.M00 ?? '0', + }; + const sum = [breakdown.M0, breakdown.M1, breakdown.M2] + .map((v) => parseFloat(v) || 0) + .reduce((a, b) => a + b, 0); + return { metaFiatTotal: sum.toFixed(2), breakdown }; +} +function buildMoneySupplySnapshot(officeId, balances) { + const m0 = balances[exports.GL_CODES.M0_RESERVE] ?? '0'; + const m1 = balances[exports.GL_CODES.M1_CIRCULATING] ?? '0'; + const m2 = balances[exports.GL_CODES.M2_BROAD] ?? '0'; + const pending = balances[exports.GL_CODES.SETTLEMENT_SUSPENSE] ?? '0'; + const m0Num = parseFloat(m0) || 0; + const m1Num = parseFloat(m1) || 0; + return { + asOf: new Date().toISOString(), + officeId, + M0: { + gl1050: m0, + backingRatio: exports.POLICY.M0_BACKING_MULTIPLIER, + }, + M1: { + circulating: m1, + gl2000: balances[exports.GL_CODES.M1_LIABILITY] ?? m1, + gl2100: m1, + capMultiple: exports.POLICY.M1_CAP_MULTIPLIER, + }, + M2: { + broadMoney: m2, + gl2200: m2, + metaFiatPending: pending, + }, + metaFiatRealRail: { + pendingSettlement: pending, + inFlightSwift: balances['inFlightSwift'] ?? '0', + }, + }; +} +function m2FiatToTokenLoadAmount(fiatAmount, m2Available) { + const fiat = parseFloat(fiatAmount) || 0; + const m2 = parseFloat(m2Available) || 0; + const load = Math.min(fiat, m2); + return { + loadAmount: load.toFixed(2), + fullyBacked: load >= fiat && fiat > 0, + }; +} diff --git a/packages/settlement-core/dist/state-machine.d.ts b/packages/settlement-core/dist/state-machine.d.ts new file mode 100644 index 0000000..3e31686 --- /dev/null +++ b/packages/settlement-core/dist/state-machine.d.ts @@ -0,0 +1,3 @@ +import type { SettlementPhase } from './types'; +export declare function nextPhase(current: SettlementPhase): SettlementPhase | null; +export declare function canAdvance(from: SettlementPhase, to: SettlementPhase): boolean; diff --git a/packages/settlement-core/dist/state-machine.js b/packages/settlement-core/dist/state-machine.js new file mode 100644 index 0000000..37a08e0 --- /dev/null +++ b/packages/settlement-core/dist/state-machine.js @@ -0,0 +1,29 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.nextPhase = nextPhase; +exports.canAdvance = canAdvance; +const ORDER = [ + 'RECEIVED', + 'VALIDATED', + 'VERBIAGE_ROLLED', + 'FINERACT_POSTED', + 'HYBX_RAIL_DISPATCHED', + 'ISO20022_ARCHIVED', + 'CHAIN_MINT_REQUESTED', + 'SETTLED', +]; +function nextPhase(current) { + if (current === 'FAILED' || current === 'SETTLED') + return null; + const idx = ORDER.indexOf(current); + if (idx < 0 || idx >= ORDER.length - 1) + return null; + return ORDER[idx + 1]; +} +function canAdvance(from, to) { + const fi = ORDER.indexOf(from); + const ti = ORDER.indexOf(to); + if (fi < 0 || ti < 0) + return to === 'FAILED'; + return ti === fi + 1 || to === 'FAILED'; +} diff --git a/packages/settlement-core/dist/types.d.ts b/packages/settlement-core/dist/types.d.ts new file mode 100644 index 0000000..9116375 --- /dev/null +++ b/packages/settlement-core/dist/types.d.ts @@ -0,0 +1,102 @@ +/** OMNL central-bank settlement domain types */ +export type MoneyLayer = 'M0' | 'M1' | 'M2' | 'M00'; +export type PaymentRail = 'SWIFT' | 'SEPA' | 'TARGET2' | 'RTGS' | 'ACH' | 'FEDWIRE' | 'INTERNAL' | 'CHAIN138'; +export type TradeFinanceInstrument = 'SBLC' | 'DLC' | 'BG' | 'LS'; +export type SwiftMessageKind = 'MT103' | 'MT102' | 'MT202' | 'MT910'; +export type SettlementPhase = 'RECEIVED' | 'VALIDATED' | 'VERBIAGE_ROLLED' | 'FINERACT_POSTED' | 'HYBX_RAIL_DISPATCHED' | 'ISO20022_ARCHIVED' | 'CHAIN_MINT_REQUESTED' | 'SETTLED' | 'FAILED'; +export type ExternalTransferRequest = { + idempotencyKey: string; + officeId: number; + valueDate: string; + currency: string; + amount: string; + debtorIban?: string; + creditorIban: string; + creditorBic?: string; + beneficiaryName: string; + orderingName?: string; + remittanceInfo: string; + moneyLayers: MoneyLayer[]; + rail: PaymentRail; + swiftMessageKind?: SwiftMessageKind; + tradeFinance?: { + instrument: TradeFinanceInstrument; + reference: string; + expiryDate?: string; + }; + rwaVerbiage?: { + assetClass: string; + tokenLineId: string; + externalTransferRoll: boolean; + }; + convertToCrypto?: { + enabled: boolean; + targetChainId: number; + tokenLineId: string; + recipientAddress: string; + }; +}; +export type TokenLoadRequest = { + idempotencyKey: string; + officeId?: number; + lineId: string; + symbol?: string; + tokenAddress?: string; + amount: string; + recipientAddress: string; + currency?: string; +}; +export type TransferRequest = { + idempotencyKey: string; + officeId?: number; + tokenAddress: string; + tokenSymbol: string; + amount: string; + recipientAddress: string; + senderAddress?: string; + moneyLayers: MoneyLayer[]; + rail: 'INTERNAL' | 'CHAIN138' | 'SWIFT' | 'RTGS'; + /** External SWIFT/HYBX beneficiary IBAN when rail is external */ + creditorIban?: string; + beneficiaryName?: string; + remittanceInfo?: string; + currency?: string; +}; +export type SettlementRecord = { + settlementId: string; + idempotencyKey: string; + phase: SettlementPhase; + request: ExternalTransferRequest; + fineractJournalRef?: string; + hybxPaymentId?: string; + iso20022MessageId?: string; + chainTxHash?: string; + verbiageDocument?: string; + errors: string[]; + createdAt: string; + updatedAt: string; +}; +export type MoneySupplySnapshot = { + asOf: string; + officeId: number; + M0: { + reserveOz?: string; + gl1050: string; + backingRatio: number; + }; + M1: { + circulating: string; + gl2000: string; + gl2100: string; + capMultiple: number; + }; + M2: { + broadMoney: string; + gl2200: string; + metaFiatPending: string; + }; + metaFiatRealRail: { + pendingSettlement: string; + inFlightSwift: string; + }; +}; diff --git a/packages/settlement-core/dist/types.js b/packages/settlement-core/dist/types.js new file mode 100644 index 0000000..e431a3f --- /dev/null +++ b/packages/settlement-core/dist/types.js @@ -0,0 +1,3 @@ +"use strict"; +/** OMNL central-bank settlement domain types */ +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/packages/settlement-core/dist/verbiage.d.ts b/packages/settlement-core/dist/verbiage.d.ts new file mode 100644 index 0000000..a6f847b --- /dev/null +++ b/packages/settlement-core/dist/verbiage.d.ts @@ -0,0 +1,6 @@ +import type { ExternalTransferRequest, SwiftMessageKind } from './types'; +/** OMNL central-bank external transfer verbiage roll (audit + operator copy) */ +export declare function rollExternalTransferVerbiage(req: ExternalTransferRequest): string; +export declare function buildMt103Narrative(req: ExternalTransferRequest): string; +export declare function buildMt102Narrative(req: ExternalTransferRequest): string; +export declare function swiftKindForRequest(req: ExternalTransferRequest): SwiftMessageKind; diff --git a/packages/settlement-core/dist/verbiage.js b/packages/settlement-core/dist/verbiage.js new file mode 100644 index 0000000..d29d60a --- /dev/null +++ b/packages/settlement-core/dist/verbiage.js @@ -0,0 +1,54 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.rollExternalTransferVerbiage = rollExternalTransferVerbiage; +exports.buildMt103Narrative = buildMt103Narrative; +exports.buildMt102Narrative = buildMt102Narrative; +exports.swiftKindForRequest = swiftKindForRequest; +/** OMNL central-bank external transfer verbiage roll (audit + operator copy) */ +function rollExternalTransferVerbiage(req) { + const lines = [ + '=== OMNL CENTRAL BANK — EXTERNAL TRANSFER VERBIAGE ROLL ===', + `Settlement ref: ${req.idempotencyKey}`, + `Value date: ${req.valueDate}`, + `Amount: ${req.amount} ${req.currency}`, + `Rail: ${req.rail}`, + `Money layers: ${req.moneyLayers.join(' + ')}`, + `Beneficiary: ${req.beneficiaryName}`, + `Creditor IBAN: ${req.creditorIban}`, + ]; + if (req.creditorBic) + lines.push(`Creditor BIC: ${req.creditorBic}`); + if (req.debtorIban) + lines.push(`Debtor IBAN: ${req.debtorIban}`); + if (req.orderingName) + lines.push(`Ordering party: ${req.orderingName}`); + lines.push(`Remittance: ${req.remittanceInfo}`); + if (req.swiftMessageKind) + lines.push(`SWIFT message: ${req.swiftMessageKind}`); + if (req.tradeFinance) { + lines.push(`Trade finance: ${req.tradeFinance.instrument} ref ${req.tradeFinance.reference}`); + } + if (req.rwaVerbiage?.externalTransferRoll) { + lines.push(`RWA roll: ${req.rwaVerbiage.assetClass} → line ${req.rwaVerbiage.tokenLineId}`); + } + if (req.convertToCrypto?.enabled) { + lines.push(`Post-settlement crypto conversion: chain ${req.convertToCrypto.targetChainId} → ${req.convertToCrypto.recipientAddress}`); + } + lines.push('', 'DISCLAIMER: Internal OMNL operator verbiage — not SWIFT NET transmission.', 'Fineract SoR + ISO 20022 archive bind settlement finality.'); + return lines.join('\n'); +} +function buildMt103Narrative(req) { + return `/BNF/${req.beneficiaryName}//${req.remittanceInfo}`.slice(0, 140); +} +function buildMt102Narrative(req) { + const tf = req.tradeFinance ? `/${req.tradeFinance.instrument}/${req.tradeFinance.reference}` : ''; + return `/BNF/${req.beneficiaryName}${tf}//${req.remittanceInfo}`.slice(0, 140); +} +function swiftKindForRequest(req) { + if (req.swiftMessageKind) + return req.swiftMessageKind; + if (req.tradeFinance?.instrument === 'LS' || req.tradeFinance?.instrument === 'DLC') { + return 'MT102'; + } + return 'MT103'; +} diff --git a/packages/settlement-core/package-lock.json b/packages/settlement-core/package-lock.json new file mode 100644 index 0000000..a48134c --- /dev/null +++ b/packages/settlement-core/package-lock.json @@ -0,0 +1,47 @@ +{ + "name": "@dbis/settlement-core", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@dbis/settlement-core", + "version": "0.1.0", + "devDependencies": { + "@types/node": "^20.11.0", + "typescript": "^5.4.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/packages/settlement-core/package.json b/packages/settlement-core/package.json new file mode 100644 index 0000000..2ce48a8 --- /dev/null +++ b/packages/settlement-core/package.json @@ -0,0 +1,17 @@ +{ + "name": "@dbis/settlement-core", + "version": "0.1.0", + "private": true, + "description": "OMNL central-bank settlement: M0/M1/M2 money supply, SWIFT rails, trade-finance instruments, verbiage", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": ["dist"], + "scripts": { + "build": "tsc", + "test": "node --test dist/**/*.test.js" + }, + "devDependencies": { + "@types/node": "^20.11.0", + "typescript": "^5.4.0" + } +} diff --git a/packages/settlement-core/src/chain-registry.ts b/packages/settlement-core/src/chain-registry.ts new file mode 100644 index 0000000..c8f558b --- /dev/null +++ b/packages/settlement-core/src/chain-registry.ts @@ -0,0 +1,67 @@ +import fs from 'fs'; +import path from 'path'; + +export type OmnlChainEntry = { + chainId: number; + name: string; + type: 'evm' | 'non-evm'; + nativeSymbol: string; + tier: string; + omnlRole: string; + status: 'active' | 'staged' | 'reserved'; + rpcEnv: string; + explorerUrl?: string; + rpcDefault?: string; + settlement: { + m2LoadEnabled: boolean; + swapEnabled: boolean; + bridgeEnabled: boolean; + officeId: number; + }; + complianceEnv?: string; +}; + +export type OmnlChainRegistry = { + version: string; + brand: string; + maxCapacity: number; + primaryChainId: number; + mirrorChainId: number; + settlementOfficeId: number; + chains: OmnlChainEntry[]; + stats?: { total: number; active: number; staged: number; reserved: number }; +}; + +let cached: OmnlChainRegistry | null = null; + +export function resolveChainRegistryPath(): string { + if (process.env.OMNL_SUPPORTED_CHAINS_CONFIG) return process.env.OMNL_SUPPORTED_CHAINS_CONFIG; + const candidates = [ + path.resolve(process.cwd(), 'config/omnl-supported-chains.v1.json'), + path.resolve(__dirname, '../../../config/omnl-supported-chains.v1.json'), + path.resolve(__dirname, '../../../../config/omnl-supported-chains.v1.json'), + ]; + for (const p of candidates) { + if (fs.existsSync(p)) return p; + } + return candidates[1]; +} + +export function loadOmnlChainRegistry(): OmnlChainRegistry { + if (cached) return cached; + const p = resolveChainRegistryPath(); + cached = JSON.parse(fs.readFileSync(p, 'utf8')) as OmnlChainRegistry; + return cached; +} + +export function getActiveChains(): OmnlChainEntry[] { + return loadOmnlChainRegistry().chains.filter((c) => c.status === 'active'); +} + +export function getChainById(chainId: number): OmnlChainEntry | undefined { + return loadOmnlChainRegistry().chains.find((c) => c.chainId === chainId); +} + +export function getSupportedChainIds128(): number[] { + return loadOmnlChainRegistry().chains.map((c) => c.chainId); +} diff --git a/packages/settlement-core/src/index.ts b/packages/settlement-core/src/index.ts new file mode 100644 index 0000000..e31b7fa --- /dev/null +++ b/packages/settlement-core/src/index.ts @@ -0,0 +1,7 @@ +export * from './types'; +export * from './money-supply'; +export * from './instruments'; +export * from './verbiage'; +export * from './state-machine'; +export * from './m2-token-registry'; +export * from './chain-registry'; diff --git a/packages/settlement-core/src/instruments.ts b/packages/settlement-core/src/instruments.ts new file mode 100644 index 0000000..5ed17d9 --- /dev/null +++ b/packages/settlement-core/src/instruments.ts @@ -0,0 +1,31 @@ +import type { SwiftMessageKind, TradeFinanceInstrument } from './types'; + +/** SWIFT / trade-finance instrument routing */ +export const INSTRUMENT_ROUTING: Record< + TradeFinanceInstrument, + { swift: SwiftMessageKind[]; glDebit: string; glCredit: string } +> = { + SBLC: { swift: ['MT103', 'MT202'], glDebit: '1410', glCredit: '2100' }, + DLC: { swift: ['MT103', 'MT102'], glDebit: '1410', glCredit: '1000' }, + BG: { swift: ['MT103'], glDebit: '1410', glCredit: '2100' }, + LS: { swift: ['MT102', 'MT103'], glDebit: '1000', glCredit: '2200' }, +}; + +export function resolveInstrumentGl( + instrument: TradeFinanceInstrument, +): { debitGl: string; creditGl: string; swiftKinds: SwiftMessageKind[] } { + const r = INSTRUMENT_ROUTING[instrument]; + return { debitGl: r.glDebit, creditGl: r.glCredit, swiftKinds: r.swift }; +} + +export function isIbanValid(iban: string): boolean { + const normalized = iban.replace(/\s/g, '').toUpperCase(); + if (!/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(normalized)) return false; + const rearranged = normalized.slice(4) + normalized.slice(0, 4); + const numeric = rearranged.replace(/[A-Z]/g, (c) => String(c.charCodeAt(0) - 55)); + let remainder = 0; + for (const ch of numeric) { + remainder = (remainder * 10 + parseInt(ch, 10)) % 97; + } + return remainder === 1; +} diff --git a/packages/settlement-core/src/m2-token-registry.ts b/packages/settlement-core/src/m2-token-registry.ts new file mode 100644 index 0000000..b485908 --- /dev/null +++ b/packages/settlement-core/src/m2-token-registry.ts @@ -0,0 +1,135 @@ +import type { MoneyLayer } from './types'; +import { GL_CODES } from './money-supply'; + +export type M2TokenCapabilities = { + symbol: string; + name: string; + address: string; + decimals: number; + native?: boolean; + currencyCode?: string; + moneyLayer: MoneyLayer; + omnlLine: string; + loadFromGl: string; + swappable: boolean; + convertible: boolean; + transferableInternal: boolean; + transferableExternal: boolean; +}; + +export type M2TokenRegistryFile = { + version: string; + chainId: number; + defaultMoneyLayer: MoneyLayer; + defaultLoadFromGl: string; + tokens: M2TokenCapabilities[]; +}; + +const CURRENCY_FROM_SYMBOL: Record = { + USD: 'USD', + EUR: 'EUR', + GBP: 'GBP', + AUD: 'AUD', + JPY: 'JPY', + CHF: 'CHF', + CAD: 'CAD', + XAU: 'XAU', + BTC: 'BTC', + ETH: 'ETH', + BNB: 'BNB', + POL: 'POL', + AVAX: 'AVAX', + CRO: 'CRO', + XDAI: 'XDAI', + CELO: 'CELO', + WEMIX: 'WEMIX', +}; + +export function inferCurrencyCode(symbol: string): string { + const s = symbol.toUpperCase(); + for (const [key, code] of Object.entries(CURRENCY_FROM_SYMBOL)) { + if (s.includes(key)) return code; + } + if (s.startsWith('C') && s.length > 1) { + for (const [key, code] of Object.entries(CURRENCY_FROM_SYMBOL)) { + if (s.slice(1).includes(key)) return code; + } + } + return 'USD'; +} + +export function resolveOmnlLineM2(currencyCode?: string, symbol?: string): string { + const ccy = (currencyCode || inferCurrencyCode(symbol ?? 'USD')).toUpperCase(); + return `${ccy}-M2`; +} + +export function withM2Capabilities( + token: { + symbol: string; + name: string; + address: string; + decimals: number; + native?: boolean; + currencyCode?: string; + omnlLine?: string; + }, + overrides: Partial = {}, +): M2TokenCapabilities { + const currencyCode = token.currencyCode ?? inferCurrencyCode(token.symbol); + return { + symbol: token.symbol, + name: token.name, + address: token.address, + decimals: token.decimals, + native: token.native, + currencyCode, + moneyLayer: 'M2', + omnlLine: token.omnlLine ?? resolveOmnlLineM2(currencyCode, token.symbol), + loadFromGl: GL_CODES.M2_BROAD, + swappable: true, + convertible: true, + transferableInternal: true, + transferableExternal: true, + ...overrides, + }; +} + +export function mergeExchangeTokensWithM2Registry( + baseTokens: { + symbol: string; + name: string; + address: string; + decimals: number; + native?: boolean; + omnlLine?: string; + }[], + registry: M2TokenRegistryFile, +): M2TokenCapabilities[] { + const byAddress = new Map(); + for (const t of registry.tokens) { + byAddress.set(t.address.toLowerCase(), t); + } + for (const t of baseTokens) { + const key = t.address.toLowerCase(); + if (!byAddress.has(key)) { + byAddress.set(key, withM2Capabilities(t)); + } + } + return Array.from(byAddress.values()).sort((a, b) => a.symbol.localeCompare(b.symbol)); +} + +export function findM2TokenByAddress( + registry: M2TokenRegistryFile, + address: string, +): M2TokenCapabilities | undefined { + const lower = address.toLowerCase(); + return registry.tokens.find((t) => t.address.toLowerCase() === lower); +} + +export function findM2TokenBySymbol( + registry: M2TokenRegistryFile, + symbol: string, +): M2TokenCapabilities | undefined { + const s = symbol.trim().toLowerCase(); + return registry.tokens.find((t) => t.symbol.toLowerCase() === s); +} diff --git a/packages/settlement-core/src/money-supply.ts b/packages/settlement-core/src/money-supply.ts new file mode 100644 index 0000000..8ca9638 --- /dev/null +++ b/packages/settlement-core/src/money-supply.ts @@ -0,0 +1,82 @@ +import type { MoneyLayer, MoneySupplySnapshot } from './types'; + +/** IPSAS GL mapping for OMNL central-bank money aggregates */ +export const GL_CODES = { + M0_RESERVE: '1050', + M1_LIABILITY: '2000', + M1_CIRCULATING: '2100', + M2_BROAD: '2200', + SETTLEMENT_SUSPENSE: '1000', + TRADE_FINANCE_CONTINGENT: '1410', + CRYPTO_TOKEN_LIABILITY: '2300', +} as const; + +/** Policy caps from hybx-omnl-policy (1.2× M0 reserve → M1 capacity; 5× M1 → M2 broad) */ +export const POLICY = { + M0_BACKING_MULTIPLIER: 1.2, + M1_CAP_MULTIPLIER: 5, + M2_META_FIAT_RAIL_CONVERSION: true, +} as const; + +export function aggregateMetaFiatLayers( + layers: Partial>, +): { metaFiatTotal: string; breakdown: Record } { + const breakdown: Record = { + M0: layers.M0 ?? '0', + M1: layers.M1 ?? '0', + M2: layers.M2 ?? '0', + M00: layers.M00 ?? '0', + }; + const sum = [breakdown.M0, breakdown.M1, breakdown.M2] + .map((v) => parseFloat(v) || 0) + .reduce((a, b) => a + b, 0); + return { metaFiatTotal: sum.toFixed(2), breakdown }; +} + +export function buildMoneySupplySnapshot( + officeId: number, + balances: Partial>, +): MoneySupplySnapshot { + const m0 = balances[GL_CODES.M0_RESERVE] ?? '0'; + const m1 = balances[GL_CODES.M1_CIRCULATING] ?? '0'; + const m2 = balances[GL_CODES.M2_BROAD] ?? '0'; + const pending = balances[GL_CODES.SETTLEMENT_SUSPENSE] ?? '0'; + const m0Num = parseFloat(m0) || 0; + const m1Num = parseFloat(m1) || 0; + return { + asOf: new Date().toISOString(), + officeId, + M0: { + gl1050: m0, + backingRatio: POLICY.M0_BACKING_MULTIPLIER, + }, + M1: { + circulating: m1, + gl2000: balances[GL_CODES.M1_LIABILITY] ?? m1, + gl2100: m1, + capMultiple: POLICY.M1_CAP_MULTIPLIER, + }, + M2: { + broadMoney: m2, + gl2200: m2, + metaFiatPending: pending, + }, + metaFiatRealRail: { + pendingSettlement: pending, + inFlightSwift: balances['inFlightSwift'] ?? '0', + }, + }; +} + +export function m2FiatToTokenLoadAmount(fiatAmount: string, m2Available: string): { + loadAmount: string; + fullyBacked: boolean; +} { + const fiat = parseFloat(fiatAmount) || 0; + const m2 = parseFloat(m2Available) || 0; + const load = Math.min(fiat, m2); + return { + loadAmount: load.toFixed(2), + fullyBacked: load >= fiat && fiat > 0, + }; +} diff --git a/packages/settlement-core/src/state-machine.ts b/packages/settlement-core/src/state-machine.ts new file mode 100644 index 0000000..9908e95 --- /dev/null +++ b/packages/settlement-core/src/state-machine.ts @@ -0,0 +1,26 @@ +import type { SettlementPhase } from './types'; + +const ORDER: SettlementPhase[] = [ + 'RECEIVED', + 'VALIDATED', + 'VERBIAGE_ROLLED', + 'FINERACT_POSTED', + 'HYBX_RAIL_DISPATCHED', + 'ISO20022_ARCHIVED', + 'CHAIN_MINT_REQUESTED', + 'SETTLED', +]; + +export function nextPhase(current: SettlementPhase): SettlementPhase | null { + if (current === 'FAILED' || current === 'SETTLED') return null; + const idx = ORDER.indexOf(current); + if (idx < 0 || idx >= ORDER.length - 1) return null; + return ORDER[idx + 1]; +} + +export function canAdvance(from: SettlementPhase, to: SettlementPhase): boolean { + const fi = ORDER.indexOf(from); + const ti = ORDER.indexOf(to); + if (fi < 0 || ti < 0) return to === 'FAILED'; + return ti === fi + 1 || to === 'FAILED'; +} diff --git a/packages/settlement-core/src/types.ts b/packages/settlement-core/src/types.ts new file mode 100644 index 0000000..f7bfa10 --- /dev/null +++ b/packages/settlement-core/src/types.ts @@ -0,0 +1,113 @@ +/** OMNL central-bank settlement domain types */ + +export type MoneyLayer = 'M0' | 'M1' | 'M2' | 'M00'; + +export type PaymentRail = + | 'SWIFT' + | 'SEPA' + | 'TARGET2' + | 'RTGS' + | 'ACH' + | 'FEDWIRE' + | 'INTERNAL' + | 'CHAIN138'; + +export type TradeFinanceInstrument = 'SBLC' | 'DLC' | 'BG' | 'LS'; + +export type SwiftMessageKind = 'MT103' | 'MT102' | 'MT202' | 'MT910'; + +export type SettlementPhase = + | 'RECEIVED' + | 'VALIDATED' + | 'VERBIAGE_ROLLED' + | 'FINERACT_POSTED' + | 'HYBX_RAIL_DISPATCHED' + | 'ISO20022_ARCHIVED' + | 'CHAIN_MINT_REQUESTED' + | 'SETTLED' + | 'FAILED'; + +export type ExternalTransferRequest = { + idempotencyKey: string; + officeId: number; + valueDate: string; + currency: string; + amount: string; + debtorIban?: string; + creditorIban: string; + creditorBic?: string; + beneficiaryName: string; + orderingName?: string; + remittanceInfo: string; + moneyLayers: MoneyLayer[]; + rail: PaymentRail; + swiftMessageKind?: SwiftMessageKind; + tradeFinance?: { + instrument: TradeFinanceInstrument; + reference: string; + expiryDate?: string; + }; + rwaVerbiage?: { + assetClass: string; + tokenLineId: string; + externalTransferRoll: boolean; + }; + convertToCrypto?: { + enabled: boolean; + targetChainId: number; + tokenLineId: string; + recipientAddress: string; + }; +}; + +export type TokenLoadRequest = { + idempotencyKey: string; + officeId?: number; + lineId: string; + symbol?: string; + tokenAddress?: string; + amount: string; + recipientAddress: string; + currency?: string; +}; + +export type TransferRequest = { + idempotencyKey: string; + officeId?: number; + tokenAddress: string; + tokenSymbol: string; + amount: string; + recipientAddress: string; + senderAddress?: string; + moneyLayers: MoneyLayer[]; + rail: 'INTERNAL' | 'CHAIN138' | 'SWIFT' | 'RTGS'; + /** External SWIFT/HYBX beneficiary IBAN when rail is external */ + creditorIban?: string; + beneficiaryName?: string; + remittanceInfo?: string; + currency?: string; +}; + +export type SettlementRecord = { + settlementId: string; + idempotencyKey: string; + phase: SettlementPhase; + request: ExternalTransferRequest; + fineractJournalRef?: string; + hybxPaymentId?: string; + iso20022MessageId?: string; + chainTxHash?: string; + verbiageDocument?: string; + errors: string[]; + createdAt: string; + updatedAt: string; +}; + +export type MoneySupplySnapshot = { + asOf: string; + officeId: number; + M0: { reserveOz?: string; gl1050: string; backingRatio: number }; + M1: { circulating: string; gl2000: string; gl2100: string; capMultiple: number }; + M2: { broadMoney: string; gl2200: string; metaFiatPending: string }; + metaFiatRealRail: { pendingSettlement: string; inFlightSwift: string }; +}; diff --git a/packages/settlement-core/src/verbiage.ts b/packages/settlement-core/src/verbiage.ts new file mode 100644 index 0000000..9fe22eb --- /dev/null +++ b/packages/settlement-core/src/verbiage.ts @@ -0,0 +1,58 @@ +import type { ExternalTransferRequest, SwiftMessageKind, TradeFinanceInstrument } from './types'; + +/** OMNL central-bank external transfer verbiage roll (audit + operator copy) */ +export function rollExternalTransferVerbiage(req: ExternalTransferRequest): string { + const lines = [ + '=== OMNL CENTRAL BANK — EXTERNAL TRANSFER VERBIAGE ROLL ===', + `Settlement ref: ${req.idempotencyKey}`, + `Value date: ${req.valueDate}`, + `Amount: ${req.amount} ${req.currency}`, + `Rail: ${req.rail}`, + `Money layers: ${req.moneyLayers.join(' + ')}`, + `Beneficiary: ${req.beneficiaryName}`, + `Creditor IBAN: ${req.creditorIban}`, + ]; + if (req.creditorBic) lines.push(`Creditor BIC: ${req.creditorBic}`); + if (req.debtorIban) lines.push(`Debtor IBAN: ${req.debtorIban}`); + if (req.orderingName) lines.push(`Ordering party: ${req.orderingName}`); + lines.push(`Remittance: ${req.remittanceInfo}`); + if (req.swiftMessageKind) lines.push(`SWIFT message: ${req.swiftMessageKind}`); + if (req.tradeFinance) { + lines.push( + `Trade finance: ${req.tradeFinance.instrument} ref ${req.tradeFinance.reference}`, + ); + } + if (req.rwaVerbiage?.externalTransferRoll) { + lines.push( + `RWA roll: ${req.rwaVerbiage.assetClass} → line ${req.rwaVerbiage.tokenLineId}`, + ); + } + if (req.convertToCrypto?.enabled) { + lines.push( + `Post-settlement crypto conversion: chain ${req.convertToCrypto.targetChainId} → ${req.convertToCrypto.recipientAddress}`, + ); + } + lines.push( + '', + 'DISCLAIMER: Internal OMNL operator verbiage — not SWIFT NET transmission.', + 'Fineract SoR + ISO 20022 archive bind settlement finality.', + ); + return lines.join('\n'); +} + +export function buildMt103Narrative(req: ExternalTransferRequest): string { + return `/BNF/${req.beneficiaryName}//${req.remittanceInfo}`.slice(0, 140); +} + +export function buildMt102Narrative(req: ExternalTransferRequest): string { + const tf = req.tradeFinance ? `/${req.tradeFinance.instrument}/${req.tradeFinance.reference}` : ''; + return `/BNF/${req.beneficiaryName}${tf}//${req.remittanceInfo}`.slice(0, 140); +} + +export function swiftKindForRequest(req: ExternalTransferRequest): SwiftMessageKind { + if (req.swiftMessageKind) return req.swiftMessageKind; + if (req.tradeFinance?.instrument === 'LS' || req.tradeFinance?.instrument === 'DLC') { + return 'MT102'; + } + return 'MT103'; +} diff --git a/packages/settlement-core/tsconfig.json b/packages/settlement-core/tsconfig.json new file mode 100644 index 0000000..3cacd25 --- /dev/null +++ b/packages/settlement-core/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["src/**/*"] +} diff --git a/scripts/deployment/deploy-omnl-bank-production.sh b/scripts/deployment/deploy-omnl-bank-production.sh new file mode 100644 index 0000000..824a550 --- /dev/null +++ b/scripts/deployment/deploy-omnl-bank-production.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# OMNL Bank — production deploy on server (128-chain, Office 24) +set -euo pipefail + +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}" + +log() { echo "[$(date -Iseconds)] $*"; } + +mkdir -p "$LOG_DIR" + +if [ ! -d "$REPO_DIR/.git" ]; 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" + +log "Generate 128-chain registry..." +node scripts/generate-omnl-128-chains.mjs + +export SETTLEMENT_MIDDLEWARE_CONFIG="${SETTLEMENT_MIDDLEWARE_CONFIG:-config/settlement-middleware.production.v1.json}" +export DBIS_EXCHANGE_CONFIG="${DBIS_EXCHANGE_CONFIG:-config/dbis-exchange.production.v1.json}" +export OMNL_SUPPORTED_CHAINS_CONFIG="${OMNL_SUPPORTED_CHAINS_CONFIG:-config/omnl-supported-chains.v1.json}" +export OMNL_M2_TOKEN_REGISTRY="${OMNL_M2_TOKEN_REGISTRY:-config/omnl-m2-token-registry.v1.json}" + +if [ -f "$ENV_FILE" ]; then + set -a + # shellcheck disable=SC1090 + source "$ENV_FILE" + set +a +fi + +build_pkg() { + local dir="$1" + log "Building $dir..." + cd "$REPO_DIR/$dir" + npm install --no-fund --no-audit + npm run build +} + +for dir in packages/integration-foundation packages/settlement-core \ + services/token-aggregation services/settlement-middleware services/dbis-exchange; do + build_pkg "$dir" +done + +log "Building frontend..." +cd "$REPO_DIR/frontend-dapp" +if command -v pnpm >/dev/null 2>&1; then + pnpm install --frozen-lockfile 2>/dev/null || pnpm install + pnpm run build +else + npm install + npm run build +fi + +start_svc() { + local name="$1" + local dir="$2" + local port="$3" + local cmd="$4" + log "Starting $name on :$port" + cd "$REPO_DIR/$dir" + pkill -f "node.*$dir" 2>/dev/null || true + nohup bash -c "$cmd" >"$LOG_DIR/$name.log" 2>&1 & + echo $! >"$LOG_DIR/$name.pid" +} + +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 + +FRONTEND_PORT="${OMNL_BANK_FRONTEND_PORT:-3002}" +pkill -f "serve -s.*$FRONTEND_PORT" 2>/dev/null || true +cd "$REPO_DIR/frontend-dapp" +nohup npx --yes serve -s dist -l "$FRONTEND_PORT" >"$LOG_DIR/frontend.log" 2>&1 & +echo $! >"$LOG_DIR/frontend.pid" + +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: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" + +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" +log " DBIS Trade: http://127.0.0.1:$FRONTEND_PORT/trade" +log " 128 chains: http://127.0.0.1:3011/api/v1/settlement/chains" diff --git a/scripts/generate-omnl-128-chains.mjs b/scripts/generate-omnl-128-chains.mjs new file mode 100644 index 0000000..7e789c5 --- /dev/null +++ b/scripts/generate-omnl-128-chains.mjs @@ -0,0 +1,142 @@ +#!/usr/bin/env node +/** + * Generate config/omnl-supported-chains.v1.json — 128-chain OMNL Bank registry. + */ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const outPath = path.resolve(__dirname, '../config/omnl-supported-chains.v1.json'); + +const CORE = [ + { chainId: 138, name: 'DeFi Oracle Meta Mainnet', symbol: 'ETH', tier: 'primary', omnlRole: 'settlement-hub', explorer: 'https://explorer.d-bis.org', rpcDefault: 'http://192.168.11.221:8545' }, + { chainId: 651940, name: 'ALL Mainnet', symbol: 'ALL', tier: 'primary', omnlRole: 'mirror-target', explorer: 'https://alltra.global', rpcDefault: 'https://mainnet-rpc.alltra.global' }, + { chainId: 1, name: 'Ethereum', symbol: 'ETH', tier: 'l1', omnlRole: 'reserve', explorer: 'https://etherscan.io', rpcDefault: 'https://eth.llamarpc.com' }, + { chainId: 56, name: 'BNB Smart Chain', symbol: 'BNB', tier: 'l1', omnlRole: 'liquidity', explorer: 'https://bscscan.com', rpcDefault: 'https://bsc-dataseed.binance.org' }, + { chainId: 137, name: 'Polygon', symbol: 'POL', tier: 'l1', omnlRole: 'liquidity', explorer: 'https://polygonscan.com', rpcDefault: 'https://polygon-bor-rpc.publicnode.com' }, + { chainId: 43114, name: 'Avalanche C-Chain', symbol: 'AVAX', tier: 'l1', omnlRole: 'liquidity', explorer: 'https://snowtrace.io', rpcDefault: 'https://api.avax.network/ext/bc/C/rpc' }, + { chainId: 42161, name: 'Arbitrum One', symbol: 'ETH', tier: 'l2', omnlRole: 'liquidity', explorer: 'https://arbiscan.io', rpcDefault: 'https://arb1.arbitrum.io/rpc' }, + { chainId: 10, name: 'Optimism', symbol: 'ETH', tier: 'l2', omnlRole: 'liquidity', explorer: 'https://optimistic.etherscan.io', rpcDefault: 'https://mainnet.optimism.io' }, + { chainId: 8453, name: 'Base', symbol: 'ETH', tier: 'l2', omnlRole: 'liquidity', explorer: 'https://basescan.org', rpcDefault: 'https://mainnet.base.org' }, + { chainId: 100, name: 'Gnosis', symbol: 'xDAI', tier: 'l1', omnlRole: 'liquidity', explorer: 'https://gnosisscan.io', rpcDefault: 'https://rpc.gnosischain.com' }, + { chainId: 25, name: 'Cronos', symbol: 'CRO', tier: 'l1', omnlRole: 'liquidity', explorer: 'https://cronoscan.com', rpcDefault: 'https://evm.cronos.org' }, + { chainId: 42220, name: 'Celo', symbol: 'CELO', tier: 'l1', omnlRole: 'liquidity', explorer: 'https://celoscan.io', rpcDefault: 'https://forno.celo.org' }, + { chainId: 1111, name: 'Wemix', symbol: 'WEMIX', tier: 'l1', omnlRole: 'liquidity', explorer: 'https://scan.wemix.com', rpcDefault: 'https://api.wemix.com' }, + { chainId: 250, name: 'Fantom', symbol: 'FTM', tier: 'l1', omnlRole: 'liquidity', explorer: 'https://ftmscan.com', rpcDefault: 'https://rpc.ftm.tools' }, + { chainId: 1284, name: 'Moonbeam', symbol: 'GLMR', tier: 'l1', omnlRole: 'liquidity', explorer: 'https://moonscan.io', rpcDefault: 'https://rpc.api.moonbeam.network' }, + { chainId: 1285, name: 'Moonriver', symbol: 'MOVR', tier: 'l1', omnlRole: 'liquidity', explorer: 'https://moonriver.moonscan.io', rpcDefault: 'https://rpc.api.moonriver.moonbeam.network' }, + { chainId: 324, name: 'zkSync Era', symbol: 'ETH', tier: 'l2', omnlRole: 'liquidity', explorer: 'https://explorer.zksync.io', rpcDefault: 'https://mainnet.era.zksync.io' }, + { chainId: 59144, name: 'Linea', symbol: 'ETH', tier: 'l2', omnlRole: 'liquidity', explorer: 'https://lineascan.build', rpcDefault: 'https://rpc.linea.build' }, + { chainId: 534352, name: 'Scroll', symbol: 'ETH', tier: 'l2', omnlRole: 'liquidity', explorer: 'https://scrollscan.com', rpcDefault: 'https://rpc.scroll.io' }, + { chainId: 81457, name: 'Blast', symbol: 'ETH', tier: 'l2', omnlRole: 'liquidity', explorer: 'https://blastscan.io', rpcDefault: 'https://rpc.blast.io' }, + { chainId: 5000, name: 'Mantle', symbol: 'MNT', tier: 'l2', omnlRole: 'liquidity', explorer: 'https://explorer.mantle.xyz', rpcDefault: 'https://rpc.mantle.xyz' }, + { chainId: 169, name: 'Manta Pacific', symbol: 'ETH', tier: 'l2', omnlRole: 'liquidity', explorer: 'https://pacific-explorer.manta.network', rpcDefault: 'https://pacific-rpc.manta.network/http' }, + { chainId: 1101, name: 'Polygon zkEVM', symbol: 'ETH', tier: 'l2', omnlRole: 'liquidity', explorer: 'https://zkevm.polygonscan.com', rpcDefault: 'https://zkevm-rpc.com' }, + { chainId: 34443, name: 'Mode', symbol: 'ETH', tier: 'l2', omnlRole: 'liquidity', explorer: 'https://explorer.mode.network', rpcDefault: 'https://mainnet.mode.network' }, + { chainId: 7777777, name: 'Zora', symbol: 'ETH', tier: 'l2', omnlRole: 'liquidity', explorer: 'https://explorer.zora.energy', rpcDefault: 'https://rpc.zora.energy' }, + { chainId: 204, name: 'opBNB', symbol: 'BNB', tier: 'l2', omnlRole: 'liquidity', explorer: 'https://opbnbscan.com', rpcDefault: 'https://opbnb-mainnet-rpc.bnbchain.org' }, + { chainId: 1088, name: 'Metis', symbol: 'METIS', tier: 'l2', omnlRole: 'liquidity', explorer: 'https://explorer.metis.io', rpcDefault: 'https://andromeda.metis.io/?owner=1088' }, + { chainId: 122, name: 'Fuse', symbol: 'FUSE', tier: 'l1', omnlRole: 'liquidity', explorer: 'https://explorer.fuse.io', rpcDefault: 'https://rpc.fuse.io' }, + { chainId: 288, name: 'Boba', symbol: 'ETH', tier: 'l2', omnlRole: 'liquidity', explorer: 'https://bobascan.com', rpcDefault: 'https://mainnet.boba.network' }, + { chainId: 1313161554, name: 'Aurora', symbol: 'ETH', tier: 'l2', omnlRole: 'liquidity', explorer: 'https://explorer.aurora.dev', rpcDefault: 'https://mainnet.aurora.dev' }, + { chainId: 8217, name: 'Klaytn', symbol: 'KLAY', tier: 'l1', omnlRole: 'liquidity', explorer: 'https://scope.klaytn.com', rpcDefault: 'https://public-node-api.klaytnapi.io/v1/cypress' }, + { chainId: 1666600000, name: 'Harmony', symbol: 'ONE', tier: 'l1', omnlRole: 'liquidity', explorer: 'https://explorer.harmony.one', rpcDefault: 'https://api.harmony.one' }, + { chainId: 4689, name: 'IoTeX', symbol: 'IOTX', tier: 'l1', omnlRole: 'liquidity', explorer: 'https://iotexscan.io', rpcDefault: 'https://babel-api.mainnet.iotex.io' }, + { chainId: 9001, name: 'Evmos', symbol: 'EVMOS', tier: 'l1', omnlRole: 'liquidity', explorer: 'https://escan.live', rpcDefault: 'https://evmos.lava.build' }, + { chainId: 321, name: 'KCC', symbol: 'KCS', tier: 'l1', omnlRole: 'liquidity', explorer: 'https://explorer.kcc.io', rpcDefault: 'https://rpc-mainnet.kcc.network' }, + { chainId: 1030, name: 'Conflux eSpace', symbol: 'CFX', tier: 'l1', omnlRole: 'liquidity', explorer: 'https://evm.confluxscan.net', rpcDefault: 'https://evm.confluxrpc.com' }, + { chainId: 7700, name: 'Canto', symbol: 'CANTO', tier: 'l1', omnlRole: 'liquidity', explorer: 'https://tuber.build', rpcDefault: 'https://canto.gravitychain.io' }, + { chainId: 592, name: 'Astar', symbol: 'ASTR', tier: 'l1', omnlRole: 'liquidity', explorer: 'https://astar.subscan.io', rpcDefault: 'https://evm.astar.network' }, + { chainId: 1818, name: 'Cube', symbol: 'CUBE', tier: 'l1', omnlRole: 'liquidity', explorer: 'https://www.cubescan.network', rpcDefault: 'https://http-mainnet.cube.network' }, + { chainId: 888, name: 'Wanchain', symbol: 'WAN', tier: 'l1', omnlRole: 'liquidity', explorer: 'https://wanscan.org', rpcDefault: 'https://gwan-ssl.wandevs.org:56891' }, + { chainId: 61, name: 'Ethereum Classic', symbol: 'ETC', tier: 'l1', omnlRole: 'liquidity', explorer: 'https://blockscout.com/etc/mainnet', rpcDefault: 'https://etc.rivet.link' }, + { chainId: 520, name: 'XT Smart Chain', symbol: 'XT', tier: 'l1', omnlRole: 'liquidity', explorer: 'https://www.xscan.ai', rpcDefault: 'https://datarpc1.xsc.pub' }, + { chainId: 23294, name: 'Oasis Sapphire', symbol: 'ROSE', tier: 'l1', omnlRole: 'liquidity', explorer: 'https://explorer.oasis.io', rpcDefault: 'https://sapphire.oasis.io' }, + { chainId: 71402, name: 'Godwoken', symbol: 'CKB', tier: 'l2', omnlRole: 'liquidity', explorer: 'https://v1.gwscan.com', rpcDefault: 'https://v1.mainnet.godwoken.io/rpc' }, + { chainId: 1116, name: 'Core', symbol: 'CORE', tier: 'l1', omnlRole: 'liquidity', explorer: 'https://scan.coredao.org', rpcDefault: 'https://rpc.coredao.org' }, + { chainId: 42793, name: 'Etherlink', symbol: 'XTZ', tier: 'l2', omnlRole: 'liquidity', explorer: 'https://explorer.etherlink.com', rpcDefault: 'https://node.mainnet.etherlink.com' }, + { chainId: 1135, name: 'Lisk', symbol: 'ETH', tier: 'l2', omnlRole: 'liquidity', explorer: 'https://blockscout.lisk.com', rpcDefault: 'https://rpc.api.lisk.com' }, + { chainId: 252, name: 'Fraxtal', symbol: 'FRAX', tier: 'l2', omnlRole: 'liquidity', explorer: 'https://fraxscan.com', rpcDefault: 'https://rpc.frax.com' }, + { chainId: 480, name: 'World Chain', symbol: 'ETH', tier: 'l2', omnlRole: 'liquidity', explorer: 'https://worldscan.org', rpcDefault: 'https://worldchain-mainnet.g.alchemy.com/public' }, + { chainId: 1480, name: 'Vana', symbol: 'VANA', tier: 'l1', omnlRole: 'liquidity', explorer: 'https://vanascan.io', rpcDefault: 'https://rpc.vana.org' }, + { chainId: 80094, name: 'Berachain', symbol: 'BERA', tier: 'l1', omnlRole: 'liquidity', explorer: 'https://berascan.com', rpcDefault: 'https://rpc.berachain.com' }, + { chainId: 130, name: 'Unichain', symbol: 'ETH', tier: 'l2', omnlRole: 'liquidity', explorer: 'https://uniscan.xyz', rpcDefault: 'https://mainnet.unichain.org' }, + { chainId: 196, name: 'X Layer', symbol: 'OKB', tier: 'l2', omnlRole: 'liquidity', explorer: 'https://www.oklink.com/xlayer', rpcDefault: 'https://rpc.xlayer.tech' }, + { chainId: 199, name: 'BitTorrent Chain', symbol: 'BTT', tier: 'l1', omnlRole: 'liquidity', explorer: 'https://bttcscan.com', rpcDefault: 'https://rpc.bittorrentchain.io' }, + { chainId: 106, name: 'Velas', symbol: 'VLX', tier: 'l1', omnlRole: 'liquidity', explorer: 'https://evmexplorer.velas.com', rpcDefault: 'https://evmexplorer.velas.com/rpc' }, + { chainId: 1000, name: 'GTON', symbol: 'GCD', tier: 'l1', omnlRole: 'staged', explorer: '', rpcDefault: '' }, + { chainId: 2222, name: 'Kava EVM', symbol: 'KAVA', tier: 'l1', omnlRole: 'liquidity', explorer: 'https://explorer.kava.io', rpcDefault: 'https://evm.kava.io' }, + { chainId: 1996, name: 'Sanko', symbol: 'DMT', tier: 'l1', omnlRole: 'staged', explorer: 'https://sanko-arbiscan.io', rpcDefault: 'https://mainnet.sanko.xyz' }, + { chainId: 167000, name: 'Taiko', symbol: 'ETH', tier: 'l2', omnlRole: 'liquidity', explorer: 'https://taikoscan.io', rpcDefault: 'https://rpc.mainnet.taiko.xyz' }, + { chainId: 690, name: 'Redstone', symbol: 'ETH', tier: 'l2', omnlRole: 'liquidity', explorer: 'https://explorer.redstone.xyz', rpcDefault: 'https://rpc.redstonechain.com' }, + { chainId: 810180, name: 'zkLink Nova', symbol: 'ETH', tier: 'l2', omnlRole: 'staged', explorer: 'https://explorer.zklink.io', rpcDefault: 'https://rpc.zklink.io' }, + { chainId: 534351, name: 'Scroll Sepolia', symbol: 'ETH', tier: 'testnet', omnlRole: 'test', explorer: 'https://sepolia.scrollscan.com', rpcDefault: 'https://sepolia-rpc.scroll.io' }, + { chainId: 11155111, name: 'Ethereum Sepolia', symbol: 'ETH', tier: 'testnet', omnlRole: 'test', explorer: 'https://sepolia.etherscan.io', rpcDefault: 'https://rpc.sepolia.org' }, + { chainId: 2138, name: 'Defi Oracle Meta Testnet', symbol: 'ETH', tier: 'testnet', omnlRole: 'test', explorer: 'https://public-2138.defi-oracle.io', rpcDefault: 'https://rpc.public-2138.defi-oracle.io' }, +]; + +function toEntry(c, status) { + return { + chainId: c.chainId, + name: c.name, + type: 'evm', + nativeSymbol: c.symbol, + tier: c.tier, + omnlRole: c.omnlRole, + status, + rpcEnv: `CHAIN_${c.chainId}_RPC_URL`, + explorerUrl: c.explorer || undefined, + rpcDefault: c.rpcDefault || undefined, + settlement: { + m2LoadEnabled: status === 'active', + swapEnabled: status === 'active', + bridgeEnabled: ['primary', 'l1', 'l2', 'liquidity'].includes(c.omnlRole) || c.tier === 'primary', + officeId: 24, + }, + complianceEnv: `OMNL_COMPLIANCE_CORE_${c.chainId}`, + }; +} + +const chains = CORE.map((c) => + toEntry(c, c.tier === 'staged' || c.tier === 'testnet' ? 'staged' : 'active'), +); + +let nextId = 900001; +while (chains.length < 128) { + const n = chains.length - CORE.length + 1; + chains.push({ + chainId: nextId++, + name: `OMNL Expansion Slot ${n}`, + type: 'evm', + nativeSymbol: 'ETH', + tier: 'reserved', + omnlRole: 'expansion', + status: 'reserved', + rpcEnv: `CHAIN_${nextId - 1}_RPC_URL`, + settlement: { m2LoadEnabled: false, swapEnabled: false, bridgeEnabled: false, officeId: 24 }, + complianceEnv: `OMNL_COMPLIANCE_CORE_${nextId - 1}`, + }); +} + +const doc = { + $schema: 'OMNL Bank supported chains — 128-chain production registry', + version: '1.0.0', + brand: 'OMNL Central Bank', + maxCapacity: 128, + primaryChainId: 138, + mirrorChainId: 651940, + settlementOfficeId: 24, + generatedAt: new Date().toISOString(), + chains, + stats: { + total: chains.length, + active: chains.filter((c) => c.status === 'active').length, + staged: chains.filter((c) => c.status === 'staged').length, + reserved: chains.filter((c) => c.status === 'reserved').length, + }, +}; + +fs.writeFileSync(outPath, JSON.stringify(doc, null, 2)); +console.log(`Wrote ${chains.length} chains to ${outPath}`); +console.log(JSON.stringify(doc.stats)); diff --git a/services/dbis-exchange/Dockerfile b/services/dbis-exchange/Dockerfile new file mode 100644 index 0000000..c2b7032 --- /dev/null +++ b/services/dbis-exchange/Dockerfile @@ -0,0 +1,19 @@ +FROM node:20-alpine AS builder +WORKDIR /app +COPY package*.json ./ +COPY tsconfig.json ./ +RUN npm ci +COPY src ./src +RUN npm run build + +FROM node:20-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm ci --omit=dev +COPY --from=builder /app/dist ./dist +RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001 +USER nodejs +EXPOSE 3012 +HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ + CMD node -e "require('http').get('http://localhost:3012/api/v1/exchange/health',r=>process.exit(r.statusCode===200?0:1))" +CMD ["node", "dist/index.js"] diff --git a/services/dbis-exchange/dist/adapters/settlement.js b/services/dbis-exchange/dist/adapters/settlement.js new file mode 100644 index 0000000..63b2b64 --- /dev/null +++ b/services/dbis-exchange/dist/adapters/settlement.js @@ -0,0 +1,111 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.fetchFundLedger = fetchFundLedger; +exports.fetchOfficeProfile = fetchOfficeProfile; +exports.auditSwapToSettlement = auditSwapToSettlement; +exports.requestM2TokenLoad = requestM2TokenLoad; +exports.requestInternalTransfer = requestInternalTransfer; +exports.requestExternalTransfer = requestExternalTransfer; +exports.fetchCryptoLedger = fetchCryptoLedger; +exports.resolveTokenLine = resolveTokenLine; +const axios_1 = __importDefault(require("axios")); +const ethers_1 = require("ethers"); +const config_1 = require("../config"); +const ERC20_ABI = ['function balanceOf(address) view returns (uint256)', 'function decimals() view returns (uint8)']; +async function fetchFundLedger(officeId, apiKey) { + const headers = {}; + if (apiKey) + headers.Authorization = `Bearer ${apiKey}`; + const { data } = await axios_1.default.get(`${(0, config_1.settlementUrl)()}/api/v1/settlement/money-supply`, { + headers, + timeout: 60000, + }); + return { officeId, fundLedger: data }; +} +async function fetchOfficeProfile() { + const { data } = await axios_1.default.get(`${(0, config_1.settlementUrl)()}/api/v1/settlement/office`, { timeout: 15000 }); + return data; +} +async function auditSwapToSettlement(payload) { + const lineId = payload.tokenLineId ?? `${payload.currency}-M2`; + const headers = { 'Content-Type': 'application/json' }; + if (payload.apiKey) + headers.Authorization = `Bearer ${payload.apiKey}`; + const { data } = await axios_1.default.post(`${(0, config_1.settlementUrl)()}/api/v1/settlement/convert/fiat-to-crypto`, { + idempotencyKey: payload.idempotencyKey, + amount: payload.amount, + currency: payload.currency, + creditorIban: 'GB82WEST12345698765432', + beneficiaryName: 'DBIS Exchange settlement audit', + remittanceInfo: payload.remittanceInfo, + convertToCrypto: { + enabled: true, + targetChainId: 138, + tokenLineId: lineId, + recipientAddress: payload.recipientAddress, + }, + }, { headers, timeout: 120000, validateStatus: () => true }); + return data; +} +async function requestM2TokenLoad(payload) { + const headers = { 'Content-Type': 'application/json' }; + if (payload.apiKey) + headers.Authorization = `Bearer ${payload.apiKey}`; + const { data } = await axios_1.default.post(`${(0, config_1.settlementUrl)()}/api/v1/settlement/token-load`, { + idempotencyKey: payload.idempotencyKey, + lineId: payload.lineId, + symbol: payload.symbol, + tokenAddress: payload.tokenAddress, + amount: payload.amount, + recipientAddress: payload.recipientAddress, + currency: payload.currency, + }, { headers, timeout: 120000, validateStatus: () => true }); + return data; +} +async function requestInternalTransfer(payload) { + const headers = { 'Content-Type': 'application/json' }; + if (payload.apiKey) + headers.Authorization = `Bearer ${payload.apiKey}`; + const { data } = await axios_1.default.post(`${(0, config_1.settlementUrl)()}/api/v1/settlement/transfer/internal`, { + ...payload, + moneyLayers: ['M2'], + }, { headers, timeout: 120000, validateStatus: () => true }); + return data; +} +async function requestExternalTransfer(payload) { + const headers = { 'Content-Type': 'application/json' }; + if (payload.apiKey) + headers.Authorization = `Bearer ${payload.apiKey}`; + const { data } = await axios_1.default.post(`${(0, config_1.settlementUrl)()}/api/v1/settlement/transfer/external`, { + ...payload, + moneyLayers: ['M2'], + rail: 'SWIFT', + }, { headers, timeout: 120000, validateStatus: () => true }); + return data; +} +async function fetchCryptoLedger(rpcUrl, wallet, tokens) { + const provider = new ethers_1.JsonRpcProvider(rpcUrl); + const balances = {}; + for (const t of tokens) { + if (t.native || t.address === '0x0000000000000000000000000000000000000000') { + const wei = await provider.getBalance(wallet); + balances[t.symbol] = (0, ethers_1.formatUnits)(wei, 18); + continue; + } + try { + const c = new ethers_1.Contract(t.address, ERC20_ABI, provider); + const raw = await c.balanceOf(wallet); + balances[t.symbol] = (0, ethers_1.formatUnits)(raw, t.decimals); + } + catch { + balances[t.symbol] = '0'; + } + } + return { wallet, balances }; +} +function resolveTokenLine(token) { + return token.omnlLine; +} diff --git a/services/dbis-exchange/dist/adapters/token-aggregation.js b/services/dbis-exchange/dist/adapters/token-aggregation.js new file mode 100644 index 0000000..c4c2eba --- /dev/null +++ b/services/dbis-exchange/dist/adapters/token-aggregation.js @@ -0,0 +1,52 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.fetchQuote = fetchQuote; +exports.fetchRoutePlan = fetchRoutePlan; +exports.fetchExecutionPlan = fetchExecutionPlan; +exports.fetchProviderCapabilities = fetchProviderCapabilities; +const axios_1 = __importDefault(require("axios")); +const config_1 = require("../config"); +async function fetchQuote(params) { + const base = (0, config_1.tokenAggUrl)(); + const q = new URLSearchParams({ + chainId: String(params.chainId), + tokenIn: params.tokenIn, + tokenOut: params.tokenOut, + amountIn: params.amountIn, + }); + const { data } = await axios_1.default.get(`${base}/api/v1/quote?${q}`, { timeout: 60000 }); + return data; +} +async function fetchRoutePlan(body) { + const { data } = await axios_1.default.post(`${(0, config_1.tokenAggUrl)()}/api/v2/routes/plan`, { + sourceChainId: body.sourceChainId, + destinationChainId: body.destinationChainId, + tokenIn: body.tokenIn, + tokenOut: body.tokenOut, + amountIn: body.amountIn, + recipient: body.recipient, + constraints: { maxSlippageBps: body.maxSlippageBps ?? 50, allowBridge: false }, + }, { timeout: 120000 }); + return data; +} +async function fetchExecutionPlan(body) { + const { data } = await axios_1.default.post(`${(0, config_1.tokenAggUrl)()}/api/v2/routes/internal-execution-plan`, { + sourceChainId: body.sourceChainId, + destinationChainId: body.destinationChainId, + tokenIn: body.tokenIn, + tokenOut: body.tokenOut, + amountIn: body.amountIn, + recipient: body.recipient, + constraints: { maxSlippageBps: body.maxSlippageBps ?? 50, allowBridge: false }, + }, { timeout: 120000 }); + return data; +} +async function fetchProviderCapabilities(chainId) { + const { data } = await axios_1.default.get(`${(0, config_1.tokenAggUrl)()}/api/v2/providers/capabilities?chainId=${chainId}`, { + timeout: 30000, + }); + return data; +} diff --git a/services/dbis-exchange/dist/api/routes/exchange.js b/services/dbis-exchange/dist/api/routes/exchange.js new file mode 100644 index 0000000..9fedb87 --- /dev/null +++ b/services/dbis-exchange/dist/api/routes/exchange.js @@ -0,0 +1,299 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.exchangePort = void 0; +exports.createExchangeRouter = createExchangeRouter; +const express_1 = require("express"); +const config_1 = require("../../config"); +Object.defineProperty(exports, "exchangePort", { enumerable: true, get: function () { return config_1.exchangePort; } }); +const settlement_1 = require("../../adapters/settlement"); +const token_aggregation_1 = require("../../adapters/token-aggregation"); +const swap_monitor_1 = require("../../store/swap-monitor"); +function apiKey(req) { + return req.header('Authorization')?.replace(/^Bearer\s+/i, '') || req.header('X-API-Key') || undefined; +} +function requireKey(req, res) { + const cfg = (0, config_1.loadExchangeConfig)(); + if (!cfg.production.requireApiKey) + return true; + const key = apiKey(req); + if (key && key === process.env.OMNL_API_KEY) + return true; + res.status(401).json({ error: 'Unauthorized' }); + return false; +} +function createExchangeRouter() { + const router = (0, express_1.Router)(); + const cfg = (0, config_1.loadExchangeConfig)(); + router.get('/health', (_req, res) => { + res.json({ + service: 'dbis-exchange', + brand: cfg.brandName, + chainId: cfg.chainId, + officeId: cfg.officeId, + settlementOffice: cfg.settlementOfficeExternalId, + tokenAggregation: (0, config_1.tokenAggUrl)(), + settlementMiddleware: (0, config_1.settlementUrl)(), + status: 'ok', + }); + }); + router.get('/tokens', (_req, res) => { + const tokens = (0, config_1.loadExchangeTokens)(); + res.json({ + chainId: cfg.chainId, + tokens, + count: tokens.length, + moneyLayer: 'M2', + routers: cfg.routers, + }); + }); + router.get('/tokens/capabilities', (_req, res) => { + const registry = (0, config_1.loadM2TokenRegistry)(); + res.json(registry); + }); + router.get('/providers', async (_req, res) => { + try { + res.json(await (0, token_aggregation_1.fetchProviderCapabilities)(cfg.chainId)); + } + catch (e) { + res.status(502).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + router.get('/quote', async (req, res) => { + try { + const tokenIn = String(req.query.tokenIn || ''); + const tokenOut = String(req.query.tokenOut || ''); + const amountIn = String(req.query.amountIn || ''); + if (!tokenIn || !tokenOut || !amountIn) { + res.status(400).json({ error: 'tokenIn, tokenOut, amountIn required' }); + return; + } + const quote = await (0, token_aggregation_1.fetchQuote)({ + chainId: cfg.chainId, + tokenIn, + tokenOut, + amountIn, + }); + if (cfg.production.monitorSwaps) { + (0, swap_monitor_1.recordSwap)({ + officeId: cfg.officeId, + chainId: cfg.chainId, + tokenIn, + tokenOut, + amountIn, + amountOut: quote.amountOut ?? quote.expectedOutput, + status: 'QUOTED', + }); + } + res.json(quote); + } + catch (e) { + res.status(502).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + router.post('/plan', async (req, res) => { + try { + const { tokenIn, tokenOut, amountIn, recipient, maxSlippageBps } = req.body; + if (!tokenIn || !tokenOut || !amountIn) { + res.status(400).json({ error: 'tokenIn, tokenOut, amountIn required' }); + return; + } + const plan = await (0, token_aggregation_1.fetchRoutePlan)({ + sourceChainId: cfg.chainId, + destinationChainId: cfg.chainId, + tokenIn: String(tokenIn), + tokenOut: String(tokenOut), + amountIn: String(amountIn), + recipient: recipient ? String(recipient) : undefined, + maxSlippageBps: maxSlippageBps ? Number(maxSlippageBps) : cfg.defaultSlippageBps, + }); + res.json(plan); + } + catch (e) { + res.status(502).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + router.post('/execute-plan', async (req, res) => { + try { + const { tokenIn, tokenOut, amountIn, recipient, maxSlippageBps } = req.body; + if (!tokenIn || !tokenOut || !amountIn || !recipient) { + res.status(400).json({ error: 'tokenIn, tokenOut, amountIn, recipient required' }); + return; + } + const plan = await (0, token_aggregation_1.fetchExecutionPlan)({ + sourceChainId: cfg.chainId, + destinationChainId: cfg.chainId, + tokenIn: String(tokenIn), + tokenOut: String(tokenOut), + amountIn: String(amountIn), + recipient: String(recipient), + maxSlippageBps: maxSlippageBps ? Number(maxSlippageBps) : cfg.defaultSlippageBps, + }); + res.json(plan); + } + catch (e) { + res.status(502).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + router.get('/ledgers', async (req, res) => { + try { + const wallet = req.query.wallet ? String(req.query.wallet) : undefined; + const serverKey = process.env.OMNL_API_KEY; + const rpc = process.env.RPC_URL_138_PUBLIC || + process.env.RPC_URL_138 || + 'https://rpc-http-pub.d-bis.org'; + let fund = null; + let office = null; + if (serverKey) { + [fund, office] = await Promise.all([ + (0, settlement_1.fetchFundLedger)(cfg.officeId, serverKey), + (0, settlement_1.fetchOfficeProfile)(), + ]); + } + let crypto; + if (wallet) { + crypto = await (0, settlement_1.fetchCryptoLedger)(rpc, wallet, (0, config_1.loadExchangeTokens)()); + } + res.json({ + officeId: cfg.officeId, + office, + fundLedger: fund?.fundLedger ?? null, + cryptoLedger: crypto ?? null, + asOf: new Date().toISOString(), + }); + } + catch (e) { + res.status(502).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + router.get('/swap/monitor', (_req, res) => { + res.json({ stats: (0, swap_monitor_1.swapStats)(), swaps: (0, swap_monitor_1.listSwaps)(Number(_req.query.limit ?? 50)) }); + }); + router.post('/swap/record', (req, res) => { + const body = req.body; + const rec = (0, swap_monitor_1.recordSwap)({ + officeId: cfg.officeId, + chainId: cfg.chainId, + tokenIn: String(body.tokenIn ?? ''), + tokenOut: String(body.tokenOut ?? ''), + amountIn: String(body.amountIn ?? ''), + amountOut: body.amountOut ? String(body.amountOut) : undefined, + txHash: body.txHash ? String(body.txHash) : undefined, + wallet: body.wallet ? String(body.wallet) : undefined, + routeProviders: Array.isArray(body.routeProviders) + ? body.routeProviders.map(String) + : undefined, + status: body.status ?? 'SUBMITTED', + settlementRef: body.settlementRef ? String(body.settlementRef) : undefined, + }); + res.json(rec); + }); + router.patch('/swap/record/:id', (req, res) => { + const updated = (0, swap_monitor_1.updateSwap)(req.params.id, req.body); + if (!updated) { + res.status(404).json({ error: 'Not found' }); + return; + } + res.json(updated); + }); + router.post('/swap/settle', async (req, res) => { + if (!requireKey(req, res)) + return; + if (!cfg.production.postSwapSettlementAudit) { + res.status(403).json({ error: 'postSwapSettlementAudit disabled' }); + return; + } + try { + const { swapId, amount, recipientAddress, currency, tokenLineId, tokenSymbol } = req.body; + const tokens = (0, config_1.loadExchangeTokens)(); + const token = tokens.find((t) => t.symbol === tokenSymbol); + const lineId = tokenLineId + ? String(tokenLineId) + : token + ? (0, settlement_1.resolveTokenLine)(token) + : `${String(currency ?? 'USD')}-M2`; + const audit = await (0, settlement_1.auditSwapToSettlement)({ + idempotencyKey: `SWAP-${String(swapId ?? Date.now())}`, + amount: String(amount ?? '0'), + currency: String(currency ?? 'USD'), + recipientAddress: String(recipientAddress ?? ''), + remittanceInfo: `DBIS Exchange M2 swap settlement Office ${cfg.officeId}`, + tokenLineId: lineId, + apiKey: apiKey(req), + }); + if (swapId) + (0, swap_monitor_1.updateSwap)(String(swapId), { settlementRef: audit.settlementId, status: 'CONFIRMED' }); + res.json(audit); + } + catch (e) { + res.status(502).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + router.post('/token-load', async (req, res) => { + if (!requireKey(req, res)) + return; + try { + const body = req.body; + const tokens = (0, config_1.loadExchangeTokens)(); + const addr = String(body.tokenAddress ?? ''); + const token = tokens.find((t) => t.address.toLowerCase() === addr.toLowerCase()); + const result = await (0, settlement_1.requestM2TokenLoad)({ + idempotencyKey: String(body.idempotencyKey ?? `LOAD-${Date.now()}`), + lineId: String(body.lineId ?? token?.omnlLine ?? 'USD-M2'), + symbol: String(body.symbol ?? token?.symbol ?? ''), + tokenAddress: addr, + amount: String(body.amount ?? '0'), + recipientAddress: String(body.recipientAddress ?? ''), + currency: String(body.currency ?? token?.currencyCode ?? 'USD'), + apiKey: apiKey(req) ?? process.env.OMNL_API_KEY, + }); + res.status(result.phase === 'FAILED' ? 422 : 200).json(result); + } + catch (e) { + res.status(502).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + router.post('/transfer/internal', async (req, res) => { + if (!requireKey(req, res)) + return; + try { + const body = req.body; + const result = await (0, settlement_1.requestInternalTransfer)({ + idempotencyKey: String(body.idempotencyKey ?? `XFER-INT-${Date.now()}`), + tokenAddress: String(body.tokenAddress ?? ''), + tokenSymbol: String(body.tokenSymbol ?? ''), + amount: String(body.amount ?? '0'), + recipientAddress: String(body.recipientAddress ?? ''), + senderAddress: body.senderAddress ? String(body.senderAddress) : undefined, + apiKey: apiKey(req) ?? process.env.OMNL_API_KEY, + }); + res.status(result.phase === 'FAILED' ? 422 : 200).json(result); + } + catch (e) { + res.status(502).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + router.post('/transfer/external', async (req, res) => { + if (!requireKey(req, res)) + return; + try { + const body = req.body; + const result = await (0, settlement_1.requestExternalTransfer)({ + idempotencyKey: String(body.idempotencyKey ?? `XFER-EXT-${Date.now()}`), + tokenAddress: String(body.tokenAddress ?? ''), + tokenSymbol: String(body.tokenSymbol ?? ''), + amount: String(body.amount ?? '0'), + recipientAddress: String(body.recipientAddress ?? ''), + creditorIban: String(body.creditorIban ?? ''), + beneficiaryName: body.beneficiaryName ? String(body.beneficiaryName) : undefined, + currency: body.currency ? String(body.currency) : undefined, + remittanceInfo: body.remittanceInfo ? String(body.remittanceInfo) : undefined, + apiKey: apiKey(req) ?? process.env.OMNL_API_KEY, + }); + res.status(result.phase === 'FAILED' ? 422 : 200).json(result); + } + catch (e) { + res.status(502).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + return router; +} diff --git a/services/dbis-exchange/dist/config.js b/services/dbis-exchange/dist/config.js new file mode 100644 index 0000000..2748311 --- /dev/null +++ b/services/dbis-exchange/dist/config.js @@ -0,0 +1,45 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.loadM2TokenRegistry = loadM2TokenRegistry; +exports.loadExchangeTokens = loadExchangeTokens; +exports.loadExchangeConfig = loadExchangeConfig; +exports.exchangePort = exchangePort; +exports.tokenAggUrl = tokenAggUrl; +exports.settlementUrl = settlementUrl; +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const settlement_core_1 = require("@dbis/settlement-core"); +let cached = null; +let cachedM2 = null; +function loadM2TokenRegistry() { + if (cachedM2) + return cachedM2; + const p = process.env.OMNL_M2_TOKEN_REGISTRY || + path_1.default.resolve(__dirname, '../../../config/omnl-m2-token-registry.v1.json'); + cachedM2 = JSON.parse(fs_1.default.readFileSync(p, 'utf8')); + return cachedM2; +} +function loadExchangeTokens() { + const cfg = loadExchangeConfig(); + return (0, settlement_core_1.mergeExchangeTokensWithM2Registry)(cfg.tokens, loadM2TokenRegistry()); +} +function loadExchangeConfig() { + if (cached) + return cached; + const p = process.env.DBIS_EXCHANGE_CONFIG || + path_1.default.resolve(__dirname, '../../../config/dbis-exchange.v1.json'); + cached = JSON.parse(fs_1.default.readFileSync(p, 'utf8')); + return cached; +} +function exchangePort() { + return parseInt(process.env.DBIS_EXCHANGE_PORT || '3012', 10); +} +function tokenAggUrl() { + return (process.env.TOKEN_AGGREGATION_URL || 'http://localhost:3000').replace(/\/$/, ''); +} +function settlementUrl() { + return (process.env.SETTLEMENT_MIDDLEWARE_URL || 'http://localhost:3011').replace(/\/$/, ''); +} diff --git a/services/dbis-exchange/dist/index.js b/services/dbis-exchange/dist/index.js new file mode 100644 index 0000000..4954e3e --- /dev/null +++ b/services/dbis-exchange/dist/index.js @@ -0,0 +1,47 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const dotenv = __importStar(require("dotenv")); +const path_1 = __importDefault(require("path")); +const fs_1 = require("fs"); +const server_1 = require("./server"); +const rootEnv = path_1.default.resolve(__dirname, '../../../.env'); +if ((0, fs_1.existsSync)(rootEnv)) + dotenv.config({ path: rootEnv }); +dotenv.config(); +(0, server_1.startServer)(); diff --git a/services/dbis-exchange/dist/server.js b/services/dbis-exchange/dist/server.js new file mode 100644 index 0000000..71fe133 --- /dev/null +++ b/services/dbis-exchange/dist/server.js @@ -0,0 +1,19 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.startServer = startServer; +const express_1 = __importDefault(require("express")); +const cors_1 = __importDefault(require("cors")); +const exchange_1 = require("./api/routes/exchange"); +function startServer() { + const app = (0, express_1.default)(); + app.use((0, cors_1.default)()); + app.use(express_1.default.json()); + app.use('/api/v1/exchange', (0, exchange_1.createExchangeRouter)()); + const p = (0, exchange_1.exchangePort)(); + app.listen(p, () => { + console.log(`DBIS Exchange (Pancake-style) on http://localhost:${p}/api/v1/exchange`); + }); +} diff --git a/services/dbis-exchange/dist/store/swap-monitor.js b/services/dbis-exchange/dist/store/swap-monitor.js new file mode 100644 index 0000000..fd38f9d --- /dev/null +++ b/services/dbis-exchange/dist/store/swap-monitor.js @@ -0,0 +1,52 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.recordSwap = recordSwap; +exports.updateSwap = updateSwap; +exports.listSwaps = listSwaps; +exports.swapStats = swapStats; +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const uuid_1 = require("uuid"); +const base = process.env.DBIS_SWAP_MONITOR_DIR || + path_1.default.resolve(__dirname, '../../../data/dbis-exchange/swap-monitor'); +function dir() { + if (!fs_1.default.existsSync(base)) + fs_1.default.mkdirSync(base, { recursive: true }); + return base; +} +function recordSwap(entry) { + const now = new Date().toISOString(); + const rec = { id: (0, uuid_1.v4)(), createdAt: now, updatedAt: now, ...entry }; + fs_1.default.writeFileSync(path_1.default.join(dir(), `${rec.id}.json`), JSON.stringify(rec, null, 2)); + return rec; +} +function updateSwap(id, patch) { + const p = path_1.default.join(dir(), `${id}.json`); + if (!fs_1.default.existsSync(p)) + return null; + const rec = JSON.parse(fs_1.default.readFileSync(p, 'utf8')); + const updated = { ...rec, ...patch, updatedAt: new Date().toISOString() }; + fs_1.default.writeFileSync(p, JSON.stringify(updated, null, 2)); + return updated; +} +function listSwaps(limit = 100) { + return fs_1.default + .readdirSync(dir()) + .filter((f) => f.endsWith('.json')) + .slice(0, limit) + .map((f) => JSON.parse(fs_1.default.readFileSync(path_1.default.join(dir(), f), 'utf8'))) + .sort((a, b) => b.createdAt.localeCompare(a.createdAt)); +} +function swapStats() { + const all = listSwaps(5000); + const dayAgo = Date.now() - 86400000; + return { + total: all.length, + confirmed: all.filter((s) => s.status === 'CONFIRMED').length, + failed: all.filter((s) => s.status === 'FAILED').length, + last24h: all.filter((s) => new Date(s.createdAt).getTime() > dayAgo).length, + }; +} diff --git a/services/dbis-exchange/package-lock.json b/services/dbis-exchange/package-lock.json new file mode 100644 index 0000000..1f7405f --- /dev/null +++ b/services/dbis-exchange/package-lock.json @@ -0,0 +1,1527 @@ +{ + "name": "dbis-exchange", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dbis-exchange", + "version": "1.0.0", + "dependencies": { + "@dbis/settlement-core": "file:../../packages/settlement-core", + "axios": "^1.15.2", + "cors": "^2.8.6", + "dotenv": "^16.6.1", + "ethers": "^6.16.0", + "express": "^5.1.0", + "uuid": "^11.1.0" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "@types/node": "^20.19.33", + "@types/uuid": "^10.0.0", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" + } + }, + "../../packages/settlement-core": { + "name": "@dbis/settlement-core", + "version": "0.1.0", + "devDependencies": { + "@types/node": "^20.11.0", + "typescript": "^5.4.0" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "license": "MIT" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@dbis/settlement-core": { + "resolved": "../../packages/settlement-core", + "link": true + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "license": "MIT" + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ethers": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.17.0.tgz", + "integrity": "sha512-BpyrpIPJ3ydEVow8zGaz1DuPS7YU8DcWxuBnY9a0UA/lvAPwrMr+EPXsfrul628SRaekPNeIM4UFh/91GWZang==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "1.11.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.21.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/ethers/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uuid": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/services/dbis-exchange/package.json b/services/dbis-exchange/package.json new file mode 100644 index 0000000..4440c64 --- /dev/null +++ b/services/dbis-exchange/package.json @@ -0,0 +1,28 @@ +{ + "name": "dbis-exchange", + "version": "1.0.0", + "description": "DBIS Pancake-style exchange on Chain 138 — OMNL Office 24 settlement + ledger + swap monitoring", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "dev": "ts-node src/index.ts" + }, + "dependencies": { + "@dbis/settlement-core": "file:../../packages/settlement-core", + "axios": "^1.15.2", + "cors": "^2.8.6", + "dotenv": "^16.6.1", + "ethers": "^6.16.0", + "express": "^5.1.0", + "uuid": "^11.1.0" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "@types/node": "^20.19.33", + "@types/uuid": "^10.0.0", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" + } +} diff --git a/services/dbis-exchange/src/adapters/settlement.ts b/services/dbis-exchange/src/adapters/settlement.ts new file mode 100644 index 0000000..5be9667 --- /dev/null +++ b/services/dbis-exchange/src/adapters/settlement.ts @@ -0,0 +1,159 @@ +import axios from 'axios'; +import { JsonRpcProvider, Contract, formatUnits } from 'ethers'; +import { settlementUrl } from '../config'; +import type { M2TokenCapabilities } from '@dbis/settlement-core'; + +const ERC20_ABI = ['function balanceOf(address) view returns (uint256)', 'function decimals() view returns (uint8)']; + +export async function fetchFundLedger(officeId: number, apiKey?: string) { + const headers: Record = {}; + if (apiKey) headers.Authorization = `Bearer ${apiKey}`; + const { data } = await axios.get(`${settlementUrl()}/api/v1/settlement/money-supply`, { + headers, + timeout: 60000, + }); + return { officeId, fundLedger: data }; +} + +export async function fetchOfficeProfile() { + const { data } = await axios.get(`${settlementUrl()}/api/v1/settlement/office`, { timeout: 15000 }); + return data; +} + +export async function auditSwapToSettlement(payload: { + idempotencyKey: string; + amount: string; + currency: string; + recipientAddress: string; + remittanceInfo: string; + tokenLineId?: string; + apiKey?: string; +}) { + const lineId = payload.tokenLineId ?? `${payload.currency}-M2`; + const headers: Record = { 'Content-Type': 'application/json' }; + if (payload.apiKey) headers.Authorization = `Bearer ${payload.apiKey}`; + const { data } = await axios.post( + `${settlementUrl()}/api/v1/settlement/convert/fiat-to-crypto`, + { + idempotencyKey: payload.idempotencyKey, + amount: payload.amount, + currency: payload.currency, + creditorIban: 'GB82WEST12345698765432', + beneficiaryName: 'DBIS Exchange settlement audit', + remittanceInfo: payload.remittanceInfo, + convertToCrypto: { + enabled: true, + targetChainId: 138, + tokenLineId: lineId, + recipientAddress: payload.recipientAddress, + }, + }, + { headers, timeout: 120000, validateStatus: () => true }, + ); + return data; +} + +export async function requestM2TokenLoad(payload: { + idempotencyKey: string; + lineId: string; + symbol: string; + tokenAddress: string; + amount: string; + recipientAddress: string; + currency?: string; + apiKey?: string; +}) { + const headers: Record = { 'Content-Type': 'application/json' }; + if (payload.apiKey) headers.Authorization = `Bearer ${payload.apiKey}`; + const { data } = await axios.post( + `${settlementUrl()}/api/v1/settlement/token-load`, + { + idempotencyKey: payload.idempotencyKey, + lineId: payload.lineId, + symbol: payload.symbol, + tokenAddress: payload.tokenAddress, + amount: payload.amount, + recipientAddress: payload.recipientAddress, + currency: payload.currency, + }, + { headers, timeout: 120000, validateStatus: () => true }, + ); + return data; +} + +export async function requestInternalTransfer(payload: { + idempotencyKey: string; + tokenAddress: string; + tokenSymbol: string; + amount: string; + recipientAddress: string; + senderAddress?: string; + apiKey?: string; +}) { + const headers: Record = { 'Content-Type': 'application/json' }; + if (payload.apiKey) headers.Authorization = `Bearer ${payload.apiKey}`; + const { data } = await axios.post( + `${settlementUrl()}/api/v1/settlement/transfer/internal`, + { + ...payload, + moneyLayers: ['M2'], + }, + { headers, timeout: 120000, validateStatus: () => true }, + ); + return data; +} + +export async function requestExternalTransfer(payload: { + idempotencyKey: string; + tokenAddress: string; + tokenSymbol: string; + amount: string; + recipientAddress: string; + creditorIban: string; + beneficiaryName?: string; + currency?: string; + remittanceInfo?: string; + apiKey?: string; +}) { + const headers: Record = { 'Content-Type': 'application/json' }; + if (payload.apiKey) headers.Authorization = `Bearer ${payload.apiKey}`; + const { data } = await axios.post( + `${settlementUrl()}/api/v1/settlement/transfer/external`, + { + ...payload, + moneyLayers: ['M2'], + rail: 'SWIFT', + }, + { headers, timeout: 120000, validateStatus: () => true }, + ); + return data; +} + +export async function fetchCryptoLedger( + rpcUrl: string, + wallet: string, + tokens: { address: string; symbol: string; decimals: number; native?: boolean }[], +) { + const provider = new JsonRpcProvider(rpcUrl); + const balances: Record = {}; + for (const t of tokens) { + if (t.native || t.address === '0x0000000000000000000000000000000000000000') { + const wei = await provider.getBalance(wallet); + balances[t.symbol] = formatUnits(wei, 18); + continue; + } + try { + const c = new Contract(t.address, ERC20_ABI, provider); + const raw = await c.balanceOf(wallet); + balances[t.symbol] = formatUnits(raw, t.decimals); + } catch { + balances[t.symbol] = '0'; + } + } + return { wallet, balances }; +} + +export function resolveTokenLine(token: M2TokenCapabilities): string { + return token.omnlLine; +} + \ No newline at end of file diff --git a/services/dbis-exchange/src/adapters/token-aggregation.ts b/services/dbis-exchange/src/adapters/token-aggregation.ts new file mode 100644 index 0000000..187fa5f --- /dev/null +++ b/services/dbis-exchange/src/adapters/token-aggregation.ts @@ -0,0 +1,76 @@ +import axios from 'axios'; +import { tokenAggUrl } from '../config'; + +export async function fetchQuote(params: { + chainId: number; + tokenIn: string; + tokenOut: string; + amountIn: string; +}) { + const base = tokenAggUrl(); + const q = new URLSearchParams({ + chainId: String(params.chainId), + tokenIn: params.tokenIn, + tokenOut: params.tokenOut, + amountIn: params.amountIn, + }); + const { data } = await axios.get(`${base}/api/v1/quote?${q}`, { timeout: 60000 }); + return data; +} + +export async function fetchRoutePlan(body: { + sourceChainId: number; + destinationChainId: number; + tokenIn: string; + tokenOut: string; + amountIn: string; + recipient?: string; + maxSlippageBps?: number; +}) { + const { data } = await axios.post( + `${tokenAggUrl()}/api/v2/routes/plan`, + { + sourceChainId: body.sourceChainId, + destinationChainId: body.destinationChainId, + tokenIn: body.tokenIn, + tokenOut: body.tokenOut, + amountIn: body.amountIn, + recipient: body.recipient, + constraints: { maxSlippageBps: body.maxSlippageBps ?? 50, allowBridge: false }, + }, + { timeout: 120000 }, + ); + return data; +} + +export async function fetchExecutionPlan(body: { + sourceChainId: number; + destinationChainId: number; + tokenIn: string; + tokenOut: string; + amountIn: string; + recipient: string; + maxSlippageBps?: number; +}) { + const { data } = await axios.post( + `${tokenAggUrl()}/api/v2/routes/internal-execution-plan`, + { + sourceChainId: body.sourceChainId, + destinationChainId: body.destinationChainId, + tokenIn: body.tokenIn, + tokenOut: body.tokenOut, + amountIn: body.amountIn, + recipient: body.recipient, + constraints: { maxSlippageBps: body.maxSlippageBps ?? 50, allowBridge: false }, + }, + { timeout: 120000 }, + ); + return data; +} + +export async function fetchProviderCapabilities(chainId: number) { + const { data } = await axios.get(`${tokenAggUrl()}/api/v2/providers/capabilities?chainId=${chainId}`, { + timeout: 30000, + }); + return data; +} diff --git a/services/dbis-exchange/src/api/routes/exchange.ts b/services/dbis-exchange/src/api/routes/exchange.ts new file mode 100644 index 0000000..2cb7cf0 --- /dev/null +++ b/services/dbis-exchange/src/api/routes/exchange.ts @@ -0,0 +1,322 @@ +import { Router, type Request, type Response } from 'express'; +import { + loadExchangeConfig, + loadExchangeTokens, + loadM2TokenRegistry, + exchangePort, + tokenAggUrl, + settlementUrl, +} from '../../config'; +import { + auditSwapToSettlement, + fetchCryptoLedger, + fetchFundLedger, + fetchOfficeProfile, + requestExternalTransfer, + requestInternalTransfer, + requestM2TokenLoad, + resolveTokenLine, +} from '../../adapters/settlement'; +import { + fetchQuote, + fetchRoutePlan, + fetchExecutionPlan, + fetchProviderCapabilities, +} from '../../adapters/token-aggregation'; +import { listSwaps, recordSwap, swapStats, updateSwap } from '../../store/swap-monitor'; + +function apiKey(req: Request): string | undefined { + return req.header('Authorization')?.replace(/^Bearer\s+/i, '') || req.header('X-API-Key') || undefined; +} + +function requireKey(req: Request, res: Response): boolean { + const cfg = loadExchangeConfig(); + if (!cfg.production.requireApiKey) return true; + const key = apiKey(req); + if (key && key === process.env.OMNL_API_KEY) return true; + res.status(401).json({ error: 'Unauthorized' }); + return false; +} + +export function createExchangeRouter(): Router { + const router = Router(); + const cfg = loadExchangeConfig(); + + router.get('/health', (_req, res) => { + res.json({ + service: 'dbis-exchange', + brand: cfg.brandName, + chainId: cfg.chainId, + officeId: cfg.officeId, + settlementOffice: cfg.settlementOfficeExternalId, + tokenAggregation: tokenAggUrl(), + settlementMiddleware: settlementUrl(), + status: 'ok', + }); + }); + + router.get('/tokens', (_req, res) => { + const tokens = loadExchangeTokens(); + res.json({ + chainId: cfg.chainId, + tokens, + count: tokens.length, + moneyLayer: 'M2', + routers: cfg.routers, + }); + }); + + router.get('/tokens/capabilities', (_req, res) => { + const registry = loadM2TokenRegistry(); + res.json(registry); + }); + + router.get('/providers', async (_req, res) => { + try { + res.json(await fetchProviderCapabilities(cfg.chainId)); + } catch (e) { + res.status(502).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + + router.get('/quote', async (req, res) => { + try { + const tokenIn = String(req.query.tokenIn || ''); + const tokenOut = String(req.query.tokenOut || ''); + const amountIn = String(req.query.amountIn || ''); + if (!tokenIn || !tokenOut || !amountIn) { + res.status(400).json({ error: 'tokenIn, tokenOut, amountIn required' }); + return; + } + const quote = await fetchQuote({ + chainId: cfg.chainId, + tokenIn, + tokenOut, + amountIn, + }); + if (cfg.production.monitorSwaps) { + recordSwap({ + officeId: cfg.officeId, + chainId: cfg.chainId, + tokenIn, + tokenOut, + amountIn, + amountOut: quote.amountOut ?? quote.expectedOutput, + status: 'QUOTED', + }); + } + res.json(quote); + } catch (e) { + res.status(502).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + + router.post('/plan', async (req, res) => { + try { + const { tokenIn, tokenOut, amountIn, recipient, maxSlippageBps } = 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, + tokenIn: String(tokenIn), + tokenOut: String(tokenOut), + amountIn: String(amountIn), + recipient: recipient ? String(recipient) : undefined, + maxSlippageBps: maxSlippageBps ? Number(maxSlippageBps) : cfg.defaultSlippageBps, + }); + res.json(plan); + } catch (e) { + res.status(502).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + + router.post('/execute-plan', async (req, res) => { + try { + const { tokenIn, tokenOut, amountIn, recipient, maxSlippageBps } = 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, + tokenIn: String(tokenIn), + tokenOut: String(tokenOut), + amountIn: String(amountIn), + recipient: String(recipient), + maxSlippageBps: maxSlippageBps ? Number(maxSlippageBps) : cfg.defaultSlippageBps, + }); + res.json(plan); + } catch (e) { + res.status(502).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + + router.get('/ledgers', async (req, res) => { + try { + const wallet = req.query.wallet ? String(req.query.wallet) : undefined; + const serverKey = process.env.OMNL_API_KEY; + const rpc = + process.env.RPC_URL_138_PUBLIC || + process.env.RPC_URL_138 || + 'https://rpc-http-pub.d-bis.org'; + let fund = null; + let office = null; + if (serverKey) { + [fund, office] = await Promise.all([ + fetchFundLedger(cfg.officeId, serverKey), + fetchOfficeProfile(), + ]); + } + let crypto; + if (wallet) { + crypto = await fetchCryptoLedger(rpc, wallet, loadExchangeTokens()); + } + res.json({ + officeId: cfg.officeId, + office, + fundLedger: fund?.fundLedger ?? null, + cryptoLedger: crypto ?? null, + asOf: new Date().toISOString(), + }); + } catch (e) { + res.status(502).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + + router.get('/swap/monitor', (_req, res) => { + res.json({ stats: swapStats(), swaps: listSwaps(Number(_req.query.limit ?? 50)) }); + }); + + router.post('/swap/record', (req, res) => { + const body = req.body as Record; + const rec = recordSwap({ + officeId: cfg.officeId, + chainId: cfg.chainId, + tokenIn: String(body.tokenIn ?? ''), + tokenOut: String(body.tokenOut ?? ''), + amountIn: String(body.amountIn ?? ''), + amountOut: body.amountOut ? String(body.amountOut) : undefined, + txHash: body.txHash ? String(body.txHash) : undefined, + wallet: body.wallet ? String(body.wallet) : undefined, + routeProviders: Array.isArray(body.routeProviders) + ? body.routeProviders.map(String) + : undefined, + status: (body.status as 'SUBMITTED' | 'CONFIRMED' | 'FAILED') ?? 'SUBMITTED', + settlementRef: body.settlementRef ? String(body.settlementRef) : undefined, + }); + res.json(rec); + }); + + router.patch('/swap/record/:id', (req, res) => { + const updated = updateSwap(req.params.id, req.body as Record); + if (!updated) { + res.status(404).json({ error: 'Not found' }); + return; + } + res.json(updated); + }); + + router.post('/swap/settle', async (req, res) => { + if (!requireKey(req, res)) return; + if (!cfg.production.postSwapSettlementAudit) { + res.status(403).json({ error: 'postSwapSettlementAudit disabled' }); + return; + } + try { + const { swapId, amount, recipientAddress, currency, tokenLineId, tokenSymbol } = + req.body as Record; + const tokens = loadExchangeTokens(); + const token = tokens.find((t) => t.symbol === tokenSymbol); + const lineId = tokenLineId + ? String(tokenLineId) + : token + ? resolveTokenLine(token) + : `${String(currency ?? 'USD')}-M2`; + const audit = await auditSwapToSettlement({ + idempotencyKey: `SWAP-${String(swapId ?? Date.now())}`, + amount: String(amount ?? '0'), + currency: String(currency ?? 'USD'), + recipientAddress: String(recipientAddress ?? ''), + remittanceInfo: `DBIS Exchange M2 swap settlement Office ${cfg.officeId}`, + tokenLineId: lineId, + apiKey: apiKey(req), + }); + if (swapId) updateSwap(String(swapId), { settlementRef: audit.settlementId, status: 'CONFIRMED' }); + res.json(audit); + } catch (e) { + res.status(502).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + + router.post('/token-load', async (req, res) => { + if (!requireKey(req, res)) return; + try { + const body = req.body as Record; + const tokens = loadExchangeTokens(); + const addr = String(body.tokenAddress ?? ''); + const token = tokens.find((t) => t.address.toLowerCase() === addr.toLowerCase()); + const result = await requestM2TokenLoad({ + idempotencyKey: String(body.idempotencyKey ?? `LOAD-${Date.now()}`), + lineId: String(body.lineId ?? token?.omnlLine ?? 'USD-M2'), + symbol: String(body.symbol ?? token?.symbol ?? ''), + tokenAddress: addr, + amount: String(body.amount ?? '0'), + recipientAddress: String(body.recipientAddress ?? ''), + currency: String(body.currency ?? token?.currencyCode ?? 'USD'), + apiKey: apiKey(req) ?? process.env.OMNL_API_KEY, + }); + res.status(result.phase === 'FAILED' ? 422 : 200).json(result); + } catch (e) { + res.status(502).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + + router.post('/transfer/internal', async (req, res) => { + if (!requireKey(req, res)) return; + try { + const body = req.body as Record; + const result = await requestInternalTransfer({ + idempotencyKey: String(body.idempotencyKey ?? `XFER-INT-${Date.now()}`), + tokenAddress: String(body.tokenAddress ?? ''), + tokenSymbol: String(body.tokenSymbol ?? ''), + amount: String(body.amount ?? '0'), + recipientAddress: String(body.recipientAddress ?? ''), + senderAddress: body.senderAddress ? String(body.senderAddress) : undefined, + apiKey: apiKey(req) ?? process.env.OMNL_API_KEY, + }); + res.status(result.phase === 'FAILED' ? 422 : 200).json(result); + } catch (e) { + res.status(502).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + + router.post('/transfer/external', async (req, res) => { + if (!requireKey(req, res)) return; + try { + const body = req.body as Record; + const result = await requestExternalTransfer({ + idempotencyKey: String(body.idempotencyKey ?? `XFER-EXT-${Date.now()}`), + tokenAddress: String(body.tokenAddress ?? ''), + tokenSymbol: String(body.tokenSymbol ?? ''), + amount: String(body.amount ?? '0'), + recipientAddress: String(body.recipientAddress ?? ''), + creditorIban: String(body.creditorIban ?? ''), + beneficiaryName: body.beneficiaryName ? String(body.beneficiaryName) : undefined, + currency: body.currency ? String(body.currency) : undefined, + remittanceInfo: body.remittanceInfo ? String(body.remittanceInfo) : undefined, + apiKey: apiKey(req) ?? process.env.OMNL_API_KEY, + }); + res.status(result.phase === 'FAILED' ? 422 : 200).json(result); + } catch (e) { + res.status(502).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + + return router; +} + +export { exchangePort }; diff --git a/services/dbis-exchange/src/config.ts b/services/dbis-exchange/src/config.ts new file mode 100644 index 0000000..7e271a4 --- /dev/null +++ b/services/dbis-exchange/src/config.ts @@ -0,0 +1,75 @@ +import fs from 'fs'; +import path from 'path'; +import { + mergeExchangeTokensWithM2Registry, + type M2TokenCapabilities, + type M2TokenRegistryFile, +} from '@dbis/settlement-core'; + +export type ExchangeConfig = { + version: string; + chainId: number; + brandName: string; + officeId: number; + settlementOfficeExternalId: string; + routers: Record; + factory: Record; + tokens: { + symbol: string; + name: string; + address: string; + decimals: number; + native?: boolean; + omnlLine?: string; + moneyLayer?: string; + swappable?: boolean; + convertible?: boolean; + transferableInternal?: boolean; + transferableExternal?: boolean; + }[]; + defaultSlippageBps: number; + production: { + requireApiKey: boolean; + monitorSwaps: boolean; + postSwapSettlementAudit: boolean; + }; +}; + +let cached: ExchangeConfig | null = null; +let cachedM2: M2TokenRegistryFile | null = null; + +export function loadM2TokenRegistry(): M2TokenRegistryFile { + if (cachedM2) return cachedM2; + const p = + process.env.OMNL_M2_TOKEN_REGISTRY || + path.resolve(__dirname, '../../../config/omnl-m2-token-registry.v1.json'); + cachedM2 = JSON.parse(fs.readFileSync(p, 'utf8')) as M2TokenRegistryFile; + return cachedM2; +} + +export function loadExchangeTokens(): M2TokenCapabilities[] { + const cfg = loadExchangeConfig(); + return mergeExchangeTokensWithM2Registry(cfg.tokens, loadM2TokenRegistry()); +} + +export function loadExchangeConfig(): ExchangeConfig { + if (cached) return cached; + const p = + process.env.DBIS_EXCHANGE_CONFIG || + path.resolve(__dirname, '../../../config/dbis-exchange.v1.json'); + cached = JSON.parse(fs.readFileSync(p, 'utf8')) as ExchangeConfig; + return cached; +} + +export function exchangePort(): number { + return parseInt(process.env.DBIS_EXCHANGE_PORT || '3012', 10); +} + +export function tokenAggUrl(): string { + return (process.env.TOKEN_AGGREGATION_URL || 'http://localhost:3000').replace(/\/$/, ''); +} + +export function settlementUrl(): string { + return (process.env.SETTLEMENT_MIDDLEWARE_URL || 'http://localhost:3011').replace(/\/$/, ''); +} + \ No newline at end of file diff --git a/services/dbis-exchange/src/index.ts b/services/dbis-exchange/src/index.ts new file mode 100644 index 0000000..919126c --- /dev/null +++ b/services/dbis-exchange/src/index.ts @@ -0,0 +1,10 @@ +import * as dotenv from 'dotenv'; +import path from 'path'; +import { existsSync } from 'fs'; +import { startServer } from './server'; + +const rootEnv = path.resolve(__dirname, '../../../.env'); +if (existsSync(rootEnv)) dotenv.config({ path: rootEnv }); +dotenv.config(); + +startServer(); diff --git a/services/dbis-exchange/src/server.ts b/services/dbis-exchange/src/server.ts new file mode 100644 index 0000000..e591ce2 --- /dev/null +++ b/services/dbis-exchange/src/server.ts @@ -0,0 +1,14 @@ +import express from 'express'; +import cors from 'cors'; +import { createExchangeRouter, exchangePort } from './api/routes/exchange'; + +export function startServer(): void { + const app = express(); + app.use(cors()); + app.use(express.json()); + app.use('/api/v1/exchange', createExchangeRouter()); + const p = exchangePort(); + app.listen(p, () => { + console.log(`DBIS Exchange (Pancake-style) on http://localhost:${p}/api/v1/exchange`); + }); +} diff --git a/services/dbis-exchange/src/store/swap-monitor.ts b/services/dbis-exchange/src/store/swap-monitor.ts new file mode 100644 index 0000000..abfc7fc --- /dev/null +++ b/services/dbis-exchange/src/store/swap-monitor.ts @@ -0,0 +1,65 @@ +import fs from 'fs'; +import path from 'path'; +import { v4 as uuidv4 } from 'uuid'; + +export type SwapMonitorRecord = { + id: string; + officeId: number; + chainId: number; + tokenIn: string; + tokenOut: string; + amountIn: string; + amountOut?: string; + txHash?: string; + wallet?: string; + routeProviders?: string[]; + status: 'QUOTED' | 'SUBMITTED' | 'CONFIRMED' | 'FAILED'; + settlementRef?: string; + createdAt: string; + updatedAt: string; +}; + +const base = + process.env.DBIS_SWAP_MONITOR_DIR || + path.resolve(__dirname, '../../../data/dbis-exchange/swap-monitor'); + +function dir(): string { + if (!fs.existsSync(base)) fs.mkdirSync(base, { recursive: true }); + return base; +} + +export function recordSwap(entry: Omit): SwapMonitorRecord { + const now = new Date().toISOString(); + const rec: SwapMonitorRecord = { id: uuidv4(), createdAt: now, updatedAt: now, ...entry }; + fs.writeFileSync(path.join(dir(), `${rec.id}.json`), JSON.stringify(rec, null, 2)); + return rec; +} + +export function updateSwap(id: string, patch: Partial): SwapMonitorRecord | null { + const p = path.join(dir(), `${id}.json`); + if (!fs.existsSync(p)) return null; + const rec = JSON.parse(fs.readFileSync(p, 'utf8')) as SwapMonitorRecord; + const updated = { ...rec, ...patch, updatedAt: new Date().toISOString() }; + fs.writeFileSync(p, JSON.stringify(updated, null, 2)); + return updated; +} + +export function listSwaps(limit = 100): SwapMonitorRecord[] { + return fs + .readdirSync(dir()) + .filter((f) => f.endsWith('.json')) + .slice(0, limit) + .map((f) => JSON.parse(fs.readFileSync(path.join(dir(), f), 'utf8')) as SwapMonitorRecord) + .sort((a, b) => b.createdAt.localeCompare(a.createdAt)); +} + +export function swapStats(): { total: number; confirmed: number; failed: number; last24h: number } { + const all = listSwaps(5000); + const dayAgo = Date.now() - 86400000; + return { + total: all.length, + confirmed: all.filter((s) => s.status === 'CONFIRMED').length, + failed: all.filter((s) => s.status === 'FAILED').length, + last24h: all.filter((s) => new Date(s.createdAt).getTime() > dayAgo).length, + }; +} diff --git a/services/dbis-exchange/tsconfig.json b/services/dbis-exchange/tsconfig.json new file mode 100644 index 0000000..e92074a --- /dev/null +++ b/services/dbis-exchange/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"] +} diff --git a/services/settlement-middleware/Dockerfile b/services/settlement-middleware/Dockerfile new file mode 100644 index 0000000..8241e57 --- /dev/null +++ b/services/settlement-middleware/Dockerfile @@ -0,0 +1,19 @@ +FROM node:20-alpine AS builder +WORKDIR /app +COPY package*.json ./ +COPY tsconfig.json ./ +RUN npm ci +COPY src ./src +RUN npm run build + +FROM node:20-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm ci --omit=dev +COPY --from=builder /app/dist ./dist +RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001 +USER nodejs +EXPOSE 3011 +HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ + CMD node -e "require('http').get('http://localhost:3011/api/v1/settlement/health',r=>process.exit(r.statusCode===200?0:1))" +CMD ["node", "dist/index.js"] diff --git a/services/settlement-middleware/dist/adapters/fineract.js b/services/settlement-middleware/dist/adapters/fineract.js new file mode 100644 index 0000000..809efa1 --- /dev/null +++ b/services/settlement-middleware/dist/adapters/fineract.js @@ -0,0 +1,52 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.postJournalEntry = postJournalEntry; +exports.fetchGlBalances = fetchGlBalances; +const axios_1 = __importDefault(require("axios")); +function authHeaders() { + const base = (process.env.OMNL_FINERACT_BASE_URL || '').replace(/\/$/, ''); + const tenant = process.env.OMNL_FINERACT_TENANT || 'omnl'; + const user = process.env.OMNL_FINERACT_USER?.trim() || + process.env.OMNL_FINERACT_USERNAME?.trim() || + 'ali_hospitallers_tenant'; + const pass = process.env.OMNL_FINERACT_PASSWORD || ''; + if (!base || !pass) + throw new Error('Fineract credentials required'); + const auth = Buffer.from(`${user}:${pass}`).toString('base64'); + return { + Authorization: `Basic ${auth}`, + 'Fineract-Platform-TenantId': tenant, + 'Content-Type': 'application/json', + }; +} +async function postJournalEntry(entry) { + const base = (process.env.OMNL_FINERACT_BASE_URL || '').replace(/\/$/, ''); + const { data } = await axios_1.default.post(`${base}/journalentries`, entry, { + headers: authHeaders(), + timeout: 60000, + }); + return { resourceId: data.resourceId ?? data.entityId ?? 0 }; +} +async function fetchGlBalances(officeId) { + const base = (process.env.OMNL_FINERACT_BASE_URL || '').replace(/\/$/, ''); + 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); + } + return out; + } + catch { + return {}; + } +} diff --git a/services/settlement-middleware/dist/adapters/hybx-production.js b/services/settlement-middleware/dist/adapters/hybx-production.js new file mode 100644 index 0000000..9d81229 --- /dev/null +++ b/services/settlement-middleware/dist/adapters/hybx-production.js @@ -0,0 +1,51 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.HybxProductionRail = void 0; +const axios_1 = __importDefault(require("axios")); +const integration_foundation_1 = require("@dbis/integration-foundation"); +/** Production HYBX rail client for real settlement dispatch */ +class HybxProductionRail { + baseUrl; + apiKey; + clientSecret; + constructor() { + const cfg = (0, integration_foundation_1.loadHybxConfig)({ testMode: false }); + if (cfg.environment === 'production' && process.env.SETTLEMENT_ALLOW_HYBX_PRODUCTION !== '1') { + throw new Error('SETTLEMENT_ALLOW_HYBX_PRODUCTION=1 required for live HYBX rails'); + } + this.baseUrl = cfg.baseUrl.replace(/\/$/, ''); + this.apiKey = cfg.apiKey; + this.clientSecret = cfg.clientSecret; + } + async dispatchPayment(payload) { + const sidecar = process.env.HYBX_MIFOS_FINERACT_SIDECAR_URL?.replace(/\/$/, ''); + const url = sidecar ? `${sidecar}/v1/rtgs/payments` : `${this.baseUrl}/v1/payments`; + const { data } = await axios_1.default.post(url, { + externalId: payload.idempotencyKey, + amount: payload.amount, + currency: payload.currency, + beneficiary: { + iban: payload.creditorIban, + bic: payload.creditorBic, + name: payload.beneficiaryName, + }, + remittanceInformation: payload.remittanceInfo, + rail: payload.rail, + }, { + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': this.apiKey, + Authorization: `Bearer ${this.clientSecret}`, + }, + timeout: 120000, + }); + return { + paymentId: String(data.paymentId ?? data.id ?? payload.idempotencyKey), + status: String(data.status ?? 'DISPATCHED'), + }; + } +} +exports.HybxProductionRail = HybxProductionRail; diff --git a/services/settlement-middleware/dist/adapters/omnl.js b/services/settlement-middleware/dist/adapters/omnl.js new file mode 100644 index 0000000..a38b470 --- /dev/null +++ b/services/settlement-middleware/dist/adapters/omnl.js @@ -0,0 +1,44 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.archiveIso20022 = archiveIso20022; +exports.requestTokenMint = requestTokenMint; +const axios_1 = __importDefault(require("axios")); +async function archiveIso20022(payload) { + const base = (process.env.TOKEN_AGGREGATION_URL || 'http://localhost:3000').replace(/\/$/, ''); + const key = process.env.OMNL_API_KEY || ''; + const headers = { 'Content-Type': 'application/json' }; + if (key) + headers.Authorization = `Bearer ${key}`; + const { data } = await axios_1.default.post(`${base}/api/v1/omnl/iso20022/messages`, { + messageType: payload.messageType, + direction: payload.direction, + payload: payload.raw, + metadata: { settlementRef: payload.settlementRef }, + }, { headers, timeout: 60000 }); + return { messageId: String(data.messageId ?? data.id ?? payload.settlementRef) }; +} +async function requestTokenMint(payload) { + const base = (process.env.TOKEN_AGGREGATION_URL || 'http://localhost:3000').replace(/\/$/, ''); + const key = process.env.OMNL_API_KEY || ''; + const headers = { 'Content-Type': 'application/json' }; + if (key) + headers.Authorization = `Bearer ${key}`; + try { + const { data } = await axios_1.default.post(`${base}/api/v1/omnl/settlement/token-load`, payload, { headers, timeout: 120000 }); + return { + txHash: data.txHash, + status: String(data.status ?? (payload.dryRun ? 'DRY_RUN' : 'SUBMITTED')), + }; + } + catch (err) { + if (axios_1.default.isAxiosError(err) && err.response?.status === 404) { + return { + status: payload.dryRun ? 'DRY_RUN' : 'PENDING_CHAIN_ENDPOINT', + }; + } + throw err; + } +} diff --git a/services/settlement-middleware/dist/adapters/swift.js b/services/settlement-middleware/dist/adapters/swift.js new file mode 100644 index 0000000..ef2f9ff --- /dev/null +++ b/services/settlement-middleware/dist/adapters/swift.js @@ -0,0 +1,42 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.forwardSwiftToListener = forwardSwiftToListener; +exports.buildMt103Stub = buildMt103Stub; +exports.buildMt102Stub = buildMt102Stub; +const axios_1 = __importDefault(require("axios")); +async function forwardSwiftToListener(raw) { + const url = (process.env.SWIFT_LISTENER_URL || 'http://192.168.11.114:8788').replace(/\/$/, ''); + const token = process.env.SWIFT_LISTENER_WEBHOOK_TOKEN || ''; + const headers = { 'Content-Type': 'text/plain' }; + if (token) + headers.Authorization = `Bearer ${token}`; + await axios_1.default.post(`${url}/inbound`, raw, { headers, timeout: 60000 }); + return { accepted: true }; +} +function buildMt103Stub(fields) { + const bicLine = fields.bic ? `:57A:${fields.bic}\n` : ''; + return `{1:F01OMNLUS33XXXX0000000000}{2:I103BANKUS33XXXXN}{4: +:20:${fields.senderRef} +:23B:CRED +:32A:${fields.valueDate.replace(/-/g, '').slice(2)}${fields.currency}${fields.amount.replace('.', ',')} +:50K:${fields.orderingCustomer} +:59:/${fields.iban} +${fields.beneficiary} +${bicLine}:70:${fields.remittance} +:71A:OUR +-}`; +} +function buildMt102Stub(fields) { + return `{1:F01OMNLUS33XXXX0000000000}{2:I102BANKUS33XXXXN}{4: +:20:${fields.senderRef} +:21:NONREF +:32B:${fields.currency}${fields.amount.replace('.', ',')} +:50A:${fields.orderingInstitution} +:59:/${fields.iban} +${fields.beneficiary} +:70:${fields.remittance} +-}`; +} diff --git a/services/settlement-middleware/dist/api/routes/settlement.js b/services/settlement-middleware/dist/api/routes/settlement.js new file mode 100644 index 0000000..afae8be --- /dev/null +++ b/services/settlement-middleware/dist/api/routes/settlement.js @@ -0,0 +1,276 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createSettlementRouter = createSettlementRouter; +const express_1 = require("express"); +const settlement_core_1 = require("@dbis/settlement-core"); +const config_1 = require("../../config"); +const external_transfer_1 = require("../../workflows/external-transfer"); +const token_load_1 = require("../../workflows/token-load"); +const transfer_1 = require("../../workflows/transfer"); +const office_scope_1 = require("../../workflows/office-scope"); +const settlement_store_1 = require("../../store/settlement-store"); +const swift_inbound_1 = require("../../workflows/swift-inbound"); +function requireApiKey(req, res) { + const cfg = (0, config_1.loadConfig)(); + if (!cfg.production.requireApiKey) + return true; + const key = req.header('Authorization')?.replace(/^Bearer\s+/i, '') || req.header('X-API-Key'); + if (key && key === process.env.OMNL_API_KEY) + return true; + res.status(401).json({ error: 'Unauthorized' }); + return false; +} +function createSettlementRouter() { + const router = (0, express_1.Router)(); + const officeId = (0, config_1.settlementOfficeId)(); + const profile = (0, config_1.loadOfficeProfile)(); + router.get('/health', (_req, res) => { + let chainStats = { total: 0, active: 0 }; + try { + const reg = (0, settlement_core_1.loadOmnlChainRegistry)(); + chainStats = { total: reg.chains.length, active: (0, settlement_core_1.getActiveChains)().length }; + } + catch { + /* optional */ + } + res.json({ + service: 'omnl-settlement-middleware', + status: 'ok', + mode: process.env.SETTLEMENT_MIDDLEWARE_CONFIG?.includes('production') ? 'production' : 'standard', + role: 'OMNL central-bank settlement orchestration', + chainSupport: { max: 128, ...chainStats }, + settlementOffice: { + officeId: profile.officeId, + externalId: profile.externalId, + name: profile.name, + }, + enforceSingleOffice: (0, config_1.loadConfig)().enforceSingleOffice ?? false, + }); + }); + router.get('/office', (_req, res) => { + res.json({ + ...profile, + locked: true, + storePath: `data/settlement-store/office-${officeId}`, + }); + }); + router.get('/money-supply', async (req, res) => { + if (!requireApiKey(req, res)) + return; + try { + res.json(await (0, external_transfer_1.getMoneySupply)(officeId)); + } + catch (e) { + res.status(500).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + router.get('/money-supply/:officeIdParam', async (req, res) => { + if (!requireApiKey(req, res)) + return; + try { + const requested = parseInt(req.params.officeIdParam, 10); + res.json(await (0, external_transfer_1.getMoneySupply)((0, config_1.settlementOfficeId)(requested))); + } + catch (e) { + res.status(400).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + router.get('/settlements', (req, res) => { + if (!requireApiKey(req, res)) + return; + res.json({ + officeId, + items: settlement_store_1.settlementStore.list(Number(req.query.limit ?? 50)), + }); + }); + router.get('/settlements/:id', (req, res) => { + if (!requireApiKey(req, res)) + return; + const rec = (0, external_transfer_1.getSettlementStatus)(req.params.id); + if (!rec) { + res.status(404).json({ error: 'Not found' }); + return; + } + res.json(rec); + }); + router.post('/external-transfer', async (req, res) => { + if (!requireApiKey(req, res)) + return; + try { + const merged = (0, office_scope_1.bindOffice24)(req.body); + const record = await (0, external_transfer_1.processExternalTransfer)(merged); + res.status(record.phase === 'FAILED' ? 422 : 200).json(record); + } + catch (e) { + res.status(e instanceof Error && e.message.includes('locked') ? 403 : 500).json({ + error: e instanceof Error ? e.message : String(e), + }); + } + }); + router.post('/swift/inbound', async (req, res) => { + if (!requireApiKey(req, res)) + return; + try { + const raw = typeof req.body === 'string' ? req.body : req.body?.raw; + if (!raw) { + res.status(400).json({ error: 'raw SWIFT payload required' }); + return; + } + const record = await (0, swift_inbound_1.processSwiftInbound)(String(raw)); + res.json(record); + } + catch (e) { + res.status(500).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + router.post('/trade-finance/:instrument', async (req, res) => { + if (!requireApiKey(req, res)) + return; + const instrument = req.params.instrument.toUpperCase(); + if (!['SBLC', 'DLC', 'BG', 'LS'].includes(instrument)) { + res.status(400).json({ error: 'Invalid instrument' }); + return; + } + try { + const body = req.body; + const merged = (0, office_scope_1.bindOffice24)({ + ...body, + tradeFinance: { + instrument, + reference: body.tradeFinance?.reference ?? body.idempotencyKey, + expiryDate: body.tradeFinance?.expiryDate, + }, + rail: 'SWIFT', + moneyLayers: body.moneyLayers ?? ['M1', 'M2'], + }); + const record = await (0, external_transfer_1.processExternalTransfer)(merged); + res.status(record.phase === 'FAILED' ? 422 : 200).json(record); + } + catch (e) { + res.status(500).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + router.post('/convert/fiat-to-crypto', async (req, res) => { + if (!requireApiKey(req, res)) + return; + try { + const body = req.body; + const merged = (0, office_scope_1.bindOffice24)({ + ...body, + rail: body.rail ?? 'INTERNAL', + convertToCrypto: { + enabled: true, + targetChainId: body.convertToCrypto?.targetChainId ?? 138, + tokenLineId: body.convertToCrypto?.tokenLineId ?? `${body.currency ?? 'USD'}-M2`, + recipientAddress: body.convertToCrypto?.recipientAddress ?? '', + }, + moneyLayers: ['M2'], + }); + const record = await (0, external_transfer_1.processExternalTransfer)(merged); + res.status(record.phase === 'FAILED' ? 422 : 200).json(record); + } + catch (e) { + res.status(500).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + router.post('/token-load', async (req, res) => { + if (!requireApiKey(req, res)) + return; + try { + const body = req.body; + if (!body.idempotencyKey || !body.lineId || !body.amount || !body.recipientAddress) { + res.status(400).json({ error: 'idempotencyKey, lineId, amount, recipientAddress required' }); + return; + } + const record = await (0, token_load_1.processTokenLoad)({ + ...body, + officeId: (0, config_1.settlementOfficeId)(body.officeId), + }); + res.status(record.phase === 'FAILED' ? 422 : 200).json(record); + } + catch (e) { + res.status(500).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + router.post('/transfer/internal', async (req, res) => { + if (!requireApiKey(req, res)) + return; + try { + const body = req.body; + if (!body.idempotencyKey || !body.tokenAddress || !body.amount || !body.recipientAddress) { + res.status(400).json({ error: 'idempotencyKey, tokenAddress, amount, recipientAddress required' }); + return; + } + const record = await (0, transfer_1.processTransfer)({ + ...body, + officeId: (0, config_1.settlementOfficeId)(body.officeId), + rail: 'INTERNAL', + moneyLayers: body.moneyLayers ?? ['M2'], + }); + res.status(record.phase === 'FAILED' ? 422 : 200).json(record); + } + catch (e) { + res.status(500).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + router.post('/transfer/external', async (req, res) => { + if (!requireApiKey(req, res)) + return; + try { + const body = req.body; + if (!body.creditorIban) { + res.status(400).json({ error: 'creditorIban required for external transfer' }); + return; + } + const record = await (0, transfer_1.processTransfer)({ + ...body, + officeId: (0, config_1.settlementOfficeId)(body.officeId), + rail: body.rail === 'RTGS' ? 'RTGS' : 'SWIFT', + moneyLayers: body.moneyLayers ?? ['M2'], + }); + res.status(record.phase === 'FAILED' ? 422 : 200).json(record); + } + catch (e) { + res.status(500).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + router.get('/tokens/m2', (_req, res) => { + try { + const fs = require('fs'); + const path = require('path'); + const p = process.env.OMNL_M2_TOKEN_REGISTRY || + path.resolve(__dirname, '../../../../../config/omnl-m2-token-registry.v1.json'); + const registry = JSON.parse(fs.readFileSync(p, 'utf8')); + res.json(registry); + } + catch (e) { + res.status(500).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + router.get('/chains', (_req, res) => { + try { + const registry = (0, settlement_core_1.loadOmnlChainRegistry)(); + res.json({ + brand: registry.brand, + maxCapacity: registry.maxCapacity, + primaryChainId: registry.primaryChainId, + mirrorChainId: registry.mirrorChainId, + settlementOfficeId: registry.settlementOfficeId, + stats: registry.stats, + chains: registry.chains, + }); + } + catch (e) { + res.status(500).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + router.get('/chains/active', (_req, res) => { + try { + res.json({ count: (0, settlement_core_1.getActiveChains)().length, chains: (0, settlement_core_1.getActiveChains)() }); + } + catch (e) { + res.status(500).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + return router; +} diff --git a/services/settlement-middleware/dist/config.js b/services/settlement-middleware/dist/config.js new file mode 100644 index 0000000..a85c0e7 --- /dev/null +++ b/services/settlement-middleware/dist/config.js @@ -0,0 +1,57 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.loadConfig = loadConfig; +exports.loadOfficeProfile = loadOfficeProfile; +exports.settlementOfficeId = settlementOfficeId; +exports.port = port; +exports.settlementStoreSubdir = settlementStoreSubdir; +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +let cached = null; +let cachedOffice = null; +function repoConfigPath(relative) { + return path_1.default.resolve(__dirname, '../../../', relative.replace(/^\//, '')); +} +function loadConfig() { + if (cached) + return cached; + const configPath = process.env.SETTLEMENT_MIDDLEWARE_CONFIG || + repoConfigPath('config/settlement-middleware.v1.json'); + cached = JSON.parse(fs_1.default.readFileSync(configPath, 'utf8')); + return cached; +} +function loadOfficeProfile() { + if (cachedOffice) + return cachedOffice; + const cfg = loadConfig(); + const profileRel = cfg.settlementOffice?.profilePath ?? 'config/offices/office-24-settlement.v1.json'; + const profilePath = process.env.OMNL_OFFICE_SETTLEMENT_PROFILE || repoConfigPath(profileRel); + cachedOffice = JSON.parse(fs_1.default.readFileSync(profilePath, 'utf8')); + return cachedOffice; +} +/** All settlement runs behind Office 24 (Ali HOSPITALLERS). */ +function settlementOfficeId(requested) { + const cfg = loadConfig(); + const profile = loadOfficeProfile(); + const locked = cfg.settlementOffice?.officeId ?? profile.officeId ?? cfg.defaultOfficeId; + const fromEnv = parseInt(process.env.ALI_HOSPITALLERS_OFFICE_ID || + process.env.OMNL_SETTLEMENT_OFFICE_ID || + String(locked), 10); + const officeId = fromEnv || locked; + if (cfg.enforceSingleOffice && requested !== undefined && requested !== officeId) { + throw new Error(`Settlement is locked to office ${officeId}; requested ${requested}`); + } + if (cfg.enforceSingleOffice && officeId !== locked) { + throw new Error(`Settlement is locked to office ${locked}; env specifies ${officeId}`); + } + return officeId; +} +function port() { + return parseInt(process.env.SETTLEMENT_MIDDLEWARE_PORT || '3011', 10); +} +function settlementStoreSubdir() { + return `office-${settlementOfficeId()}`; +} diff --git a/services/settlement-middleware/dist/index.js b/services/settlement-middleware/dist/index.js new file mode 100644 index 0000000..4954e3e --- /dev/null +++ b/services/settlement-middleware/dist/index.js @@ -0,0 +1,47 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const dotenv = __importStar(require("dotenv")); +const path_1 = __importDefault(require("path")); +const fs_1 = require("fs"); +const server_1 = require("./server"); +const rootEnv = path_1.default.resolve(__dirname, '../../../.env'); +if ((0, fs_1.existsSync)(rootEnv)) + dotenv.config({ path: rootEnv }); +dotenv.config(); +(0, server_1.startServer)(); diff --git a/services/settlement-middleware/dist/server.js b/services/settlement-middleware/dist/server.js new file mode 100644 index 0000000..1d07bdc --- /dev/null +++ b/services/settlement-middleware/dist/server.js @@ -0,0 +1,26 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createApp = createApp; +exports.startServer = startServer; +const express_1 = __importDefault(require("express")); +const cors_1 = __importDefault(require("cors")); +const settlement_1 = require("./api/routes/settlement"); +const config_1 = require("./config"); +function createApp() { + const app = (0, express_1.default)(); + app.use((0, cors_1.default)()); + app.use(express_1.default.json({ limit: '2mb' })); + app.use(express_1.default.text({ type: 'text/plain', limit: '2mb' })); + app.use('/api/v1/settlement', (0, settlement_1.createSettlementRouter)()); + return app; +} +function startServer() { + const app = createApp(); + const p = (0, config_1.port)(); + app.listen(p, () => { + console.log(`OMNL settlement middleware listening on http://localhost:${p}/api/v1/settlement`); + }); +} diff --git a/services/settlement-middleware/dist/store/settlement-store.js b/services/settlement-middleware/dist/store/settlement-store.js new file mode 100644 index 0000000..1c128d6 --- /dev/null +++ b/services/settlement-middleware/dist/store/settlement-store.js @@ -0,0 +1,47 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.settlementStore = exports.SettlementStore = void 0; +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const config_1 = require("../config"); +const baseDir = process.env.SETTLEMENT_STORE_DIR || + path_1.default.resolve(__dirname, '../../../data/settlement-store'); +function storeDir() { + const dir = path_1.default.join(baseDir, (0, config_1.settlementStoreSubdir)()); + if (!fs_1.default.existsSync(dir)) + fs_1.default.mkdirSync(dir, { recursive: true }); + return dir; +} +class SettlementStore { + save(record) { + const dir = storeDir(); + fs_1.default.writeFileSync(path_1.default.join(dir, `${record.settlementId}.json`), JSON.stringify(record, null, 2)); + fs_1.default.writeFileSync(path_1.default.join(dir, `key-${record.idempotencyKey}.json`), record.settlementId); + } + getById(id) { + const p = path_1.default.join(storeDir(), `${id}.json`); + if (!fs_1.default.existsSync(p)) + return null; + return JSON.parse(fs_1.default.readFileSync(p, 'utf8')); + } + getByIdempotencyKey(key) { + const ref = path_1.default.join(storeDir(), `key-${key}.json`); + if (!fs_1.default.existsSync(ref)) + return null; + const id = fs_1.default.readFileSync(ref, 'utf8').trim(); + return this.getById(id); + } + list(limit = 50) { + const dir = storeDir(); + return fs_1.default + .readdirSync(dir) + .filter((f) => f.endsWith('.json') && !f.startsWith('key-')) + .slice(0, limit) + .map((f) => JSON.parse(fs_1.default.readFileSync(path_1.default.join(dir, f), 'utf8'))); + } +} +exports.SettlementStore = SettlementStore; +exports.settlementStore = new SettlementStore(); diff --git a/services/settlement-middleware/dist/workflows/external-transfer.js b/services/settlement-middleware/dist/workflows/external-transfer.js new file mode 100644 index 0000000..4abc9be --- /dev/null +++ b/services/settlement-middleware/dist/workflows/external-transfer.js @@ -0,0 +1,150 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.processExternalTransfer = processExternalTransfer; +exports.getMoneySupply = getMoneySupply; +exports.getSettlementStatus = getSettlementStatus; +const uuid_1 = require("uuid"); +const settlement_core_1 = require("@dbis/settlement-core"); +const config_1 = require("../config"); +const fineract_1 = require("../adapters/fineract"); +const hybx_production_1 = require("../adapters/hybx-production"); +const omnl_1 = require("../adapters/omnl"); +const swift_1 = require("../adapters/swift"); +const settlement_store_1 = require("../store/settlement-store"); +function glId(code) { + return parseInt(code, 10); +} +async function processExternalTransfer(input) { + const cfg = (0, config_1.loadConfig)(); + const profile = (0, config_1.loadOfficeProfile)(); + const officeId = (0, config_1.settlementOfficeId)(input.officeId); + const req = { ...input, officeId }; + const existing = settlement_store_1.settlementStore.getByIdempotencyKey(req.idempotencyKey); + if (existing?.phase === 'SETTLED') + return existing; + const now = new Date().toISOString(); + let record = existing ?? { + settlementId: (0, uuid_1.v4)(), + idempotencyKey: req.idempotencyKey, + phase: 'RECEIVED', + request: req, + errors: [], + createdAt: now, + updatedAt: now, + }; + const advance = (phase, patch = {}) => { + record = { ...record, phase, updatedAt: new Date().toISOString(), ...patch }; + settlement_store_1.settlementStore.save(record); + }; + try { + if (!(0, settlement_core_1.isIbanValid)(req.creditorIban)) { + throw new Error(`Invalid creditor IBAN: ${req.creditorIban}`); + } + advance('VALIDATED'); + const verbiage = (0, settlement_core_1.rollExternalTransferVerbiage)(req); + advance('VERBIAGE_ROLLED', { verbiageDocument: verbiage }); + const amount = parseFloat(req.amount) || 0; + const tf = req.tradeFinance; + const gl = tf ? (0, settlement_core_1.resolveInstrumentGl)(tf.instrument) : null; + const debitGl = gl?.debitGl ?? profile.settlement.glSettlement ?? cfg.moneySupply.glSettlement; + const creditGl = gl?.creditGl ?? profile.settlement.glM1 ?? cfg.moneySupply.glM1; + if (amount > 0) { + const je = await (0, fineract_1.postJournalEntry)({ + officeId: req.officeId, + transactionDate: req.valueDate, + referenceNumber: req.idempotencyKey, + comments: verbiage.slice(0, 500), + debits: [{ glAccountId: glId(debitGl), amount }], + credits: [{ glAccountId: glId(creditGl), amount }], + }); + advance('FINERACT_POSTED', { fineractJournalRef: String(je.resourceId) }); + } + else { + advance('FINERACT_POSTED'); + } + if (cfg.rails.hybx.enabled && req.rail !== 'INTERNAL') { + const hybx = new hybx_production_1.HybxProductionRail(); + const pay = await hybx.dispatchPayment({ + idempotencyKey: req.idempotencyKey, + amount: req.amount, + currency: req.currency, + creditorIban: req.creditorIban, + creditorBic: req.creditorBic, + beneficiaryName: req.beneficiaryName, + remittanceInfo: req.remittanceInfo, + rail: req.rail, + }); + advance('HYBX_RAIL_DISPATCHED', { hybxPaymentId: pay.paymentId }); + } + else { + advance('HYBX_RAIL_DISPATCHED'); + } + const swiftKind = (0, settlement_core_1.swiftKindForRequest)(req); + const swiftRaw = swiftKind === 'MT102' + ? (0, swift_1.buildMt102Stub)({ + senderRef: req.idempotencyKey, + currency: req.currency, + amount: req.amount, + valueDate: req.valueDate, + orderingInstitution: req.orderingName ?? cfg.omnlLegalName, + beneficiary: req.beneficiaryName, + iban: req.creditorIban, + remittance: req.remittanceInfo, + }) + : (0, swift_1.buildMt103Stub)({ + senderRef: req.idempotencyKey, + currency: req.currency, + amount: req.amount, + valueDate: req.valueDate, + orderingCustomer: req.orderingName ?? cfg.omnlLegalName, + beneficiary: req.beneficiaryName, + iban: req.creditorIban, + bic: req.creditorBic, + remittance: req.remittanceInfo, + }); + if (cfg.rails.swift.enabled) { + await (0, swift_1.forwardSwiftToListener)(swiftRaw); + } + const iso = await (0, omnl_1.archiveIso20022)({ + messageType: swiftKind === 'MT102' ? 'pacs.008' : 'pacs.008', + direction: 'OUTBOUND', + raw: swiftRaw, + settlementRef: record.settlementId, + }); + advance('ISO20022_ARCHIVED', { iso20022MessageId: iso.messageId }); + if (req.convertToCrypto?.enabled) { + const balances = await (0, fineract_1.fetchGlBalances)(req.officeId); + const snap = (0, settlement_core_1.buildMoneySupplySnapshot)(req.officeId, balances); + const { loadAmount, fullyBacked } = (0, settlement_core_1.m2FiatToTokenLoadAmount)(req.amount, snap.M2.broadMoney); + if (!fullyBacked) { + record.errors.push(`M2 backing insufficient for token load (${loadAmount}/${req.amount})`); + } + const mint = await (0, omnl_1.requestTokenMint)({ + lineId: req.convertToCrypto.tokenLineId, + amount: loadAmount, + recipient: req.convertToCrypto.recipientAddress, + settlementRef: record.settlementId, + dryRun: !cfg.production.allowChainMintExecute, + }); + advance('CHAIN_MINT_REQUESTED', { chainTxHash: mint.txHash }); + } + else { + advance('CHAIN_MINT_REQUESTED'); + } + advance('SETTLED'); + return record; + } + catch (err) { + const msg = err instanceof Error ? err.message : String(err); + record.errors.push(msg); + advance('FAILED'); + return record; + } +} +async function getMoneySupply(officeId) { + const balances = await (0, fineract_1.fetchGlBalances)(officeId); + return (0, settlement_core_1.buildMoneySupplySnapshot)(officeId, balances); +} +function getSettlementStatus(id) { + return settlement_store_1.settlementStore.getById(id); +} diff --git a/services/settlement-middleware/dist/workflows/office-scope.js b/services/settlement-middleware/dist/workflows/office-scope.js new file mode 100644 index 0000000..3f8c60e --- /dev/null +++ b/services/settlement-middleware/dist/workflows/office-scope.js @@ -0,0 +1,32 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.bindOffice24 = bindOffice24; +const config_1 = require("../config"); +/** Bind every settlement request to Office 24 (same profile as OMNL terminal). */ +function bindOffice24(req) { + const cfg = (0, config_1.loadConfig)(); + const profile = (0, config_1.loadOfficeProfile)(); + const officeId = (0, config_1.settlementOfficeId)(req.officeId); + if (!req.idempotencyKey || !req.creditorIban || !req.amount) { + throw new Error('idempotencyKey, creditorIban, amount required'); + } + return { + officeId, + valueDate: req.valueDate ?? new Date().toISOString().slice(0, 10), + currency: req.currency ?? profile.settlement.defaultCurrency ?? cfg.defaultCurrency, + moneyLayers: (req.moneyLayers ?? profile.settlement.moneyLayers), + rail: req.rail ?? 'SWIFT', + remittanceInfo: req.remittanceInfo ?? `OMNL Office 24 settlement — ${profile.externalId}`, + beneficiaryName: req.beneficiaryName ?? 'Beneficiary', + idempotencyKey: req.idempotencyKey, + creditorIban: req.creditorIban, + amount: req.amount, + debtorIban: req.debtorIban, + creditorBic: req.creditorBic, + orderingName: req.orderingName ?? profile.omnlLegalName, + swiftMessageKind: req.swiftMessageKind, + tradeFinance: req.tradeFinance, + rwaVerbiage: req.rwaVerbiage, + convertToCrypto: req.convertToCrypto, + }; +} diff --git a/services/settlement-middleware/dist/workflows/swift-inbound.js b/services/settlement-middleware/dist/workflows/swift-inbound.js new file mode 100644 index 0000000..c7aee32 --- /dev/null +++ b/services/settlement-middleware/dist/workflows/swift-inbound.js @@ -0,0 +1,46 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.processSwiftInbound = processSwiftInbound; +const uuid_1 = require("uuid"); +const swift_1 = require("../adapters/swift"); +const config_1 = require("../config"); +const settlement_store_1 = require("../store/settlement-store"); +const external_transfer_1 = require("./external-transfer"); +/** Inbound MT103/MT102 → settlement workflow */ +async function processSwiftInbound(raw) { + await (0, swift_1.forwardSwiftToListener)(raw); + const isMt102 = /\bMT102\b|:23:/.test(raw) || /:32B:/.test(raw); + const refMatch = raw.match(/:20:([^\n]+)/); + const amtMatch = raw.match(/:32[AB]:(\d{6})([A-Z]{3})([\d,]+)/); + const ibanMatch = raw.match(/\/([A-Z]{2}\d{2}[A-Z0-9]+)/); + const bnfMatch = raw.match(/:59:[^\n]*\n([^\n:]+)/); + const remMatch = raw.match(/:70:([^\n]+)/); + const req = { + idempotencyKey: refMatch?.[1]?.trim() || `SWIFT-IN-${(0, uuid_1.v4)()}`, + officeId: (0, config_1.settlementOfficeId)(), + valueDate: new Date().toISOString().slice(0, 10), + currency: amtMatch?.[2] ?? 'USD', + amount: (amtMatch?.[3] ?? '0').replace(',', '.'), + creditorIban: ibanMatch?.[1] ?? '', + beneficiaryName: bnfMatch?.[1]?.trim() ?? 'Inbound beneficiary', + remittanceInfo: remMatch?.[1]?.trim() ?? 'Inbound SWIFT settlement', + moneyLayers: isMt102 ? ['M1', 'M2'] : ['M0', 'M1'], + rail: 'SWIFT', + swiftMessageKind: isMt102 ? 'MT102' : 'MT103', + rwaVerbiage: { assetClass: 'RWA', tokenLineId: 'USD-M1', externalTransferRoll: true }, + }; + if (!req.creditorIban) { + const stub = { + settlementId: (0, uuid_1.v4)(), + idempotencyKey: req.idempotencyKey, + phase: 'RECEIVED', + request: req, + errors: ['Parsed inbound SWIFT — manual IBAN review required'], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + settlement_store_1.settlementStore.save(stub); + return stub; + } + return (0, external_transfer_1.processExternalTransfer)(req); +} diff --git a/services/settlement-middleware/dist/workflows/token-load.js b/services/settlement-middleware/dist/workflows/token-load.js new file mode 100644 index 0000000..eeaa828 --- /dev/null +++ b/services/settlement-middleware/dist/workflows/token-load.js @@ -0,0 +1,105 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.processTokenLoad = processTokenLoad; +const uuid_1 = require("uuid"); +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 glId(code) { + return parseInt(code, 10); +} +async function processTokenLoad(input) { + const cfg = (0, config_1.loadConfig)(); + const profile = (0, config_1.loadOfficeProfile)(); + const officeId = (0, config_1.settlementOfficeId)(input.officeId); + const req = { ...input, officeId }; + const existing = settlement_store_1.settlementStore.getByIdempotencyKey(req.idempotencyKey); + if (existing?.phase === 'SETTLED') + return existing; + const now = new Date().toISOString(); + let record = existing ?? { + settlementId: (0, uuid_1.v4)(), + idempotencyKey: req.idempotencyKey, + phase: 'RECEIVED', + request: { + idempotencyKey: req.idempotencyKey, + officeId, + valueDate: now.slice(0, 10), + currency: req.currency ?? 'USD', + amount: req.amount, + creditorIban: 'INTERNAL-M2-TOKEN-LOAD', + beneficiaryName: req.recipientAddress, + remittanceInfo: `M2 token load ${req.lineId} → ${req.symbol ?? req.tokenAddress ?? 'token'}`, + moneyLayers: ['M2'], + rail: 'INTERNAL', + convertToCrypto: { + enabled: true, + targetChainId: 138, + tokenLineId: req.lineId, + recipientAddress: req.recipientAddress, + }, + }, + errors: [], + createdAt: now, + updatedAt: now, + }; + const advance = (phase, patch = {}) => { + record = { ...record, phase, updatedAt: new Date().toISOString(), ...patch }; + settlement_store_1.settlementStore.save(record); + }; + try { + advance('VALIDATED'); + advance('VERBIAGE_ROLLED', { + verbiageDocument: [ + `OMNL M2 fiat-backed token load`, + `Office ${officeId} · line ${req.lineId}`, + `Amount ${req.amount} · recipient ${req.recipientAddress}`, + `GL ${settlement_core_1.GL_CODES.M2_BROAD} → ${settlement_core_1.GL_CODES.CRYPTO_TOKEN_LIABILITY}`, + ].join('\n'), + }); + const balances = await (0, fineract_1.fetchGlBalances)(officeId); + const snap = (0, settlement_core_1.buildMoneySupplySnapshot)(officeId, balances); + const { loadAmount, fullyBacked } = (0, settlement_core_1.m2FiatToTokenLoadAmount)(req.amount, snap.M2.broadMoney); + if (!fullyBacked) { + record.errors.push(`M2 backing insufficient (${loadAmount}/${req.amount})`); + advance('FAILED'); + return record; + } + const amount = parseFloat(loadAmount) || 0; + if (amount > 0) { + const je = await (0, fineract_1.postJournalEntry)({ + officeId, + transactionDate: now.slice(0, 10), + referenceNumber: req.idempotencyKey, + comments: `M2 token load ${req.lineId}`, + debits: [{ glAccountId: glId(profile.settlement.glM2 ?? settlement_core_1.GL_CODES.M2_BROAD), amount }], + credits: [{ glAccountId: glId(settlement_core_1.GL_CODES.CRYPTO_TOKEN_LIABILITY), amount }], + }); + advance('FINERACT_POSTED', { fineractJournalRef: String(je.resourceId) }); + } + else { + advance('FINERACT_POSTED'); + } + advance('HYBX_RAIL_DISPATCHED'); + advance('ISO20022_ARCHIVED'); + const mint = await (0, omnl_1.requestTokenMint)({ + lineId: req.lineId, + amount: loadAmount, + recipient: req.recipientAddress, + settlementRef: record.settlementId, + dryRun: !cfg.production.allowChainMintExecute, + symbol: req.symbol, + tokenAddress: req.tokenAddress, + }); + advance('CHAIN_MINT_REQUESTED', { chainTxHash: mint.txHash }); + advance('SETTLED'); + return record; + } + catch (err) { + record.errors.push(err instanceof Error ? err.message : String(err)); + advance('FAILED'); + return record; + } +} diff --git a/services/settlement-middleware/dist/workflows/transfer.js b/services/settlement-middleware/dist/workflows/transfer.js new file mode 100644 index 0000000..db67c19 --- /dev/null +++ b/services/settlement-middleware/dist/workflows/transfer.js @@ -0,0 +1,130 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.processTransfer = processTransfer; +const uuid_1 = require("uuid"); +const settlement_core_1 = require("@dbis/settlement-core"); +const config_1 = require("../config"); +const fineract_1 = require("../adapters/fineract"); +const hybx_production_1 = require("../adapters/hybx-production"); +const omnl_1 = require("../adapters/omnl"); +const swift_1 = require("../adapters/swift"); +const settlement_store_1 = require("../store/settlement-store"); +function glId(code) { + return parseInt(code, 10); +} +async function processTransfer(input) { + const cfg = (0, config_1.loadConfig)(); + const profile = (0, config_1.loadOfficeProfile)(); + const officeId = (0, config_1.settlementOfficeId)(input.officeId); + const req = { ...input, officeId }; + const isExternal = req.rail === 'SWIFT' || req.rail === 'RTGS'; + const existing = settlement_store_1.settlementStore.getByIdempotencyKey(req.idempotencyKey); + if (existing?.phase === 'SETTLED') + return existing; + const now = new Date().toISOString(); + let record = existing ?? { + settlementId: (0, uuid_1.v4)(), + idempotencyKey: req.idempotencyKey, + phase: 'RECEIVED', + request: { + idempotencyKey: req.idempotencyKey, + officeId, + valueDate: now.slice(0, 10), + currency: req.currency ?? 'USD', + amount: req.amount, + creditorIban: req.creditorIban ?? (isExternal ? '' : 'INTERNAL-CHAIN138'), + beneficiaryName: req.beneficiaryName ?? req.recipientAddress, + remittanceInfo: req.remittanceInfo ?? `Transfer ${req.tokenSymbol} → ${req.recipientAddress}`, + moneyLayers: req.moneyLayers.length ? req.moneyLayers : ['M2'], + rail: req.rail === 'CHAIN138' ? 'CHAIN138' : req.rail === 'INTERNAL' ? 'INTERNAL' : 'SWIFT', + }, + errors: [], + createdAt: now, + updatedAt: now, + }; + const advance = (phase, patch = {}) => { + record = { ...record, phase, updatedAt: new Date().toISOString(), ...patch }; + settlement_store_1.settlementStore.save(record); + }; + try { + if (isExternal && !req.creditorIban) { + throw new Error('creditorIban required for external transfer'); + } + advance('VALIDATED'); + advance('VERBIAGE_ROLLED', { + verbiageDocument: [ + `OMNL ${req.rail} transfer · ${req.tokenSymbol}`, + `M2 layer · ${req.amount}`, + `To ${req.recipientAddress}`, + isExternal ? `IBAN ${req.creditorIban}` : 'Internal / Chain 138', + ].join('\n'), + }); + const amount = parseFloat(req.amount) || 0; + if (amount > 0) { + const je = await (0, fineract_1.postJournalEntry)({ + officeId, + transactionDate: now.slice(0, 10), + referenceNumber: req.idempotencyKey, + comments: `${req.rail} transfer ${req.tokenSymbol}`, + debits: [{ glAccountId: glId(profile.settlement.glM2 ?? settlement_core_1.GL_CODES.M2_BROAD), amount }], + credits: [{ glAccountId: glId(profile.settlement.glSettlement ?? settlement_core_1.GL_CODES.SETTLEMENT_SUSPENSE), amount }], + }); + advance('FINERACT_POSTED', { fineractJournalRef: String(je.resourceId) }); + } + else { + advance('FINERACT_POSTED'); + } + if (isExternal && cfg.rails.hybx.enabled) { + const hybx = new hybx_production_1.HybxProductionRail(); + const pay = await hybx.dispatchPayment({ + idempotencyKey: req.idempotencyKey, + amount: req.amount, + currency: req.currency ?? 'USD', + creditorIban: req.creditorIban, + beneficiaryName: req.beneficiaryName ?? req.recipientAddress, + remittanceInfo: req.remittanceInfo ?? `OMNL ${req.tokenSymbol} external transfer`, + rail: req.rail === 'RTGS' ? 'RTGS' : 'SWIFT', + }); + advance('HYBX_RAIL_DISPATCHED', { hybxPaymentId: pay.paymentId }); + } + else { + advance('HYBX_RAIL_DISPATCHED'); + } + if (isExternal && cfg.rails.swift.enabled && req.creditorIban) { + const swiftRaw = (0, swift_1.buildMt103Stub)({ + senderRef: req.idempotencyKey, + currency: req.currency ?? 'USD', + amount: req.amount, + valueDate: now.slice(0, 10), + orderingCustomer: cfg.omnlLegalName, + beneficiary: req.beneficiaryName ?? req.recipientAddress, + iban: req.creditorIban, + remittance: req.remittanceInfo ?? `OMNL ${req.tokenSymbol}`, + }); + await (0, swift_1.forwardSwiftToListener)(swiftRaw); + const iso = await (0, omnl_1.archiveIso20022)({ + messageType: 'pacs.008', + direction: 'OUTBOUND', + raw: swiftRaw, + settlementRef: record.settlementId, + }); + advance('ISO20022_ARCHIVED', { iso20022MessageId: iso.messageId }); + } + else { + advance('ISO20022_ARCHIVED'); + } + advance('CHAIN_MINT_REQUESTED', { + verbiageDocument: [ + record.verbiageDocument, + `On-chain ERC-20 transfer prepared: ${req.tokenAddress} → ${req.recipientAddress}`, + ].join('\n'), + }); + advance('SETTLED'); + return record; + } + catch (err) { + record.errors.push(err instanceof Error ? err.message : String(err)); + advance('FAILED'); + return record; + } +} diff --git a/services/settlement-middleware/package-lock.json b/services/settlement-middleware/package-lock.json new file mode 100644 index 0000000..8401717 --- /dev/null +++ b/services/settlement-middleware/package-lock.json @@ -0,0 +1,1433 @@ +{ + "name": "omnl-settlement-middleware", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "omnl-settlement-middleware", + "version": "1.0.0", + "dependencies": { + "@dbis/integration-foundation": "file:../../packages/integration-foundation", + "@dbis/settlement-core": "file:../../packages/settlement-core", + "axios": "^1.15.2", + "cors": "^2.8.6", + "dotenv": "^16.6.1", + "express": "^5.1.0", + "uuid": "^11.1.0" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "@types/node": "^20.19.33", + "@types/uuid": "^10.0.0", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" + } + }, + "../../packages/integration-foundation": { + "name": "@dbis/integration-foundation", + "version": "0.1.0", + "devDependencies": { + "@types/node": "^20.11.0", + "typescript": "^5.4.0" + } + }, + "../../packages/settlement-core": { + "name": "@dbis/settlement-core", + "version": "0.1.0", + "devDependencies": { + "@types/node": "^20.11.0", + "typescript": "^5.4.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@dbis/integration-foundation": { + "resolved": "../../packages/integration-foundation", + "link": true + }, + "node_modules/@dbis/settlement-core": { + "resolved": "../../packages/settlement-core", + "link": true + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uuid": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/services/settlement-middleware/package.json b/services/settlement-middleware/package.json new file mode 100644 index 0000000..d1d9bf0 --- /dev/null +++ b/services/settlement-middleware/package.json @@ -0,0 +1,29 @@ +{ + "name": "omnl-settlement-middleware", + "version": "1.0.0", + "description": "OMNL central-bank settlement middleware — M0/M1/M2 meta fiat, SWIFT rails, trade finance, crypto token loading", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "dev": "ts-node src/index.ts", + "test": "node --test dist/**/*.test.js" + }, + "dependencies": { + "@dbis/integration-foundation": "file:../../packages/integration-foundation", + "@dbis/settlement-core": "file:../../packages/settlement-core", + "axios": "^1.15.2", + "cors": "^2.8.6", + "dotenv": "^16.6.1", + "express": "^5.1.0", + "uuid": "^11.1.0" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "@types/node": "^20.19.33", + "@types/uuid": "^10.0.0", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" + } +} diff --git a/services/settlement-middleware/src/adapters/fineract.ts b/services/settlement-middleware/src/adapters/fineract.ts new file mode 100644 index 0000000..75c55bb --- /dev/null +++ b/services/settlement-middleware/src/adapters/fineract.ts @@ -0,0 +1,53 @@ +import axios from 'axios'; + +function authHeaders(): Record { + const base = (process.env.OMNL_FINERACT_BASE_URL || '').replace(/\/$/, ''); + const tenant = process.env.OMNL_FINERACT_TENANT || 'omnl'; + const user = + process.env.OMNL_FINERACT_USER?.trim() || + process.env.OMNL_FINERACT_USERNAME?.trim() || + 'ali_hospitallers_tenant'; + const pass = process.env.OMNL_FINERACT_PASSWORD || ''; + if (!base || !pass) throw new Error('Fineract credentials required'); + const auth = Buffer.from(`${user}:${pass}`).toString('base64'); + return { + Authorization: `Basic ${auth}`, + 'Fineract-Platform-TenantId': tenant, + 'Content-Type': 'application/json', + }; +} + +export async function postJournalEntry(entry: { + officeId: number; + transactionDate: string; + referenceNumber: string; + comments: string; + debits: { glAccountId: number; amount: number }[]; + credits: { glAccountId: number; amount: number }[]; +}): Promise<{ resourceId: number }> { + const base = (process.env.OMNL_FINERACT_BASE_URL || '').replace(/\/$/, ''); + const { data } = await axios.post(`${base}/journalentries`, entry, { + headers: authHeaders(), + timeout: 60000, + }); + return { resourceId: data.resourceId ?? data.entityId ?? 0 }; +} + +export async function fetchGlBalances(officeId: number): Promise> { + const base = (process.env.OMNL_FINERACT_BASE_URL || '').replace(/\/$/, ''); + 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); + } + return out; + } catch { + return {}; + } +} diff --git a/services/settlement-middleware/src/adapters/hybx-production.ts b/services/settlement-middleware/src/adapters/hybx-production.ts new file mode 100644 index 0000000..627efd6 --- /dev/null +++ b/services/settlement-middleware/src/adapters/hybx-production.ts @@ -0,0 +1,60 @@ +import axios from 'axios'; +import { loadHybxConfig } from '@dbis/integration-foundation'; + +/** Production HYBX rail client for real settlement dispatch */ +export class HybxProductionRail { + private readonly baseUrl: string; + private readonly apiKey: string; + private readonly clientSecret: string; + + constructor() { + const cfg = loadHybxConfig({ testMode: false }); + if (cfg.environment === 'production' && process.env.SETTLEMENT_ALLOW_HYBX_PRODUCTION !== '1') { + throw new Error('SETTLEMENT_ALLOW_HYBX_PRODUCTION=1 required for live HYBX rails'); + } + this.baseUrl = cfg.baseUrl.replace(/\/$/, ''); + this.apiKey = cfg.apiKey; + this.clientSecret = cfg.clientSecret; + } + + async dispatchPayment(payload: { + idempotencyKey: string; + amount: string; + currency: string; + creditorIban: string; + creditorBic?: string; + beneficiaryName: string; + remittanceInfo: string; + rail: string; + }): Promise<{ paymentId: string; status: string }> { + const sidecar = process.env.HYBX_MIFOS_FINERACT_SIDECAR_URL?.replace(/\/$/, ''); + const url = sidecar ? `${sidecar}/v1/rtgs/payments` : `${this.baseUrl}/v1/payments`; + const { data } = await axios.post( + url, + { + externalId: payload.idempotencyKey, + amount: payload.amount, + currency: payload.currency, + beneficiary: { + iban: payload.creditorIban, + bic: payload.creditorBic, + name: payload.beneficiaryName, + }, + remittanceInformation: payload.remittanceInfo, + rail: payload.rail, + }, + { + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': this.apiKey, + Authorization: `Bearer ${this.clientSecret}`, + }, + timeout: 120000, + }, + ); + return { + paymentId: String(data.paymentId ?? data.id ?? payload.idempotencyKey), + status: String(data.status ?? 'DISPATCHED'), + }; + } +} diff --git a/services/settlement-middleware/src/adapters/omnl.ts b/services/settlement-middleware/src/adapters/omnl.ts new file mode 100644 index 0000000..33fce2f --- /dev/null +++ b/services/settlement-middleware/src/adapters/omnl.ts @@ -0,0 +1,59 @@ +import axios from 'axios'; + +export async function archiveIso20022(payload: { + messageType: string; + direction: 'INBOUND' | 'OUTBOUND'; + raw: string; + settlementRef: string; +}): Promise<{ messageId: string }> { + const base = (process.env.TOKEN_AGGREGATION_URL || 'http://localhost:3000').replace(/\/$/, ''); + const key = process.env.OMNL_API_KEY || ''; + const headers: Record = { 'Content-Type': 'application/json' }; + if (key) headers.Authorization = `Bearer ${key}`; + + const { data } = await axios.post( + `${base}/api/v1/omnl/iso20022/messages`, + { + messageType: payload.messageType, + direction: payload.direction, + payload: payload.raw, + metadata: { settlementRef: payload.settlementRef }, + }, + { headers, timeout: 60000 }, + ); + return { messageId: String(data.messageId ?? data.id ?? payload.settlementRef) }; +} + +export async function requestTokenMint(payload: { + lineId: string; + amount: string; + recipient: string; + settlementRef: string; + dryRun: boolean; + symbol?: string; + tokenAddress?: string; +}): Promise<{ txHash?: string; status: string }> { + const base = (process.env.TOKEN_AGGREGATION_URL || 'http://localhost:3000').replace(/\/$/, ''); + const key = process.env.OMNL_API_KEY || ''; + const headers: Record = { 'Content-Type': 'application/json' }; + if (key) headers.Authorization = `Bearer ${key}`; + + try { + const { data } = await axios.post( + `${base}/api/v1/omnl/settlement/token-load`, + payload, + { headers, timeout: 120000 }, + ); + return { + txHash: data.txHash, + status: String(data.status ?? (payload.dryRun ? 'DRY_RUN' : 'SUBMITTED')), + }; + } catch (err) { + if (axios.isAxiosError(err) && err.response?.status === 404) { + return { + status: payload.dryRun ? 'DRY_RUN' : 'PENDING_CHAIN_ENDPOINT', + }; + } + throw err; + } +} diff --git a/services/settlement-middleware/src/adapters/swift.ts b/services/settlement-middleware/src/adapters/swift.ts new file mode 100644 index 0000000..43ed96a --- /dev/null +++ b/services/settlement-middleware/src/adapters/swift.ts @@ -0,0 +1,56 @@ +import axios from 'axios'; + +export async function forwardSwiftToListener(raw: string): Promise<{ accepted: boolean }> { + const url = (process.env.SWIFT_LISTENER_URL || 'http://192.168.11.114:8788').replace(/\/$/, ''); + const token = process.env.SWIFT_LISTENER_WEBHOOK_TOKEN || ''; + const headers: Record = { 'Content-Type': 'text/plain' }; + if (token) headers.Authorization = `Bearer ${token}`; + + await axios.post(`${url}/inbound`, raw, { headers, timeout: 60000 }); + return { accepted: true }; +} + +export function buildMt103Stub(fields: { + senderRef: string; + currency: string; + amount: string; + valueDate: string; + orderingCustomer: string; + beneficiary: string; + iban: string; + bic?: string; + remittance: string; +}): string { + const bicLine = fields.bic ? `:57A:${fields.bic}\n` : ''; + return `{1:F01OMNLUS33XXXX0000000000}{2:I103BANKUS33XXXXN}{4: +:20:${fields.senderRef} +:23B:CRED +:32A:${fields.valueDate.replace(/-/g, '').slice(2)}${fields.currency}${fields.amount.replace('.', ',')} +:50K:${fields.orderingCustomer} +:59:/${fields.iban} +${fields.beneficiary} +${bicLine}:70:${fields.remittance} +:71A:OUR +-}`; +} + +export function buildMt102Stub(fields: { + senderRef: string; + currency: string; + amount: string; + valueDate: string; + orderingInstitution: string; + beneficiary: string; + iban: string; + remittance: string; +}): string { + return `{1:F01OMNLUS33XXXX0000000000}{2:I102BANKUS33XXXXN}{4: +:20:${fields.senderRef} +:21:NONREF +:32B:${fields.currency}${fields.amount.replace('.', ',')} +:50A:${fields.orderingInstitution} +:59:/${fields.iban} +${fields.beneficiary} +:70:${fields.remittance} +-}`; +} diff --git a/services/settlement-middleware/src/api/routes/settlement.ts b/services/settlement-middleware/src/api/routes/settlement.ts new file mode 100644 index 0000000..3937ab2 --- /dev/null +++ b/services/settlement-middleware/src/api/routes/settlement.ts @@ -0,0 +1,271 @@ +import { Router, type Request, type Response } from 'express'; +import type { ExternalTransferRequest, TradeFinanceInstrument, TokenLoadRequest, TransferRequest } from '@dbis/settlement-core'; +import { loadOmnlChainRegistry, getActiveChains } from '@dbis/settlement-core'; +import { loadConfig, loadOfficeProfile, settlementOfficeId } from '../../config'; +import { + getMoneySupply, + getSettlementStatus, + processExternalTransfer, +} from '../../workflows/external-transfer'; +import { processTokenLoad } from '../../workflows/token-load'; +import { processTransfer } from '../../workflows/transfer';import { bindOffice24 } from '../../workflows/office-scope'; +import { settlementStore } from '../../store/settlement-store'; +import { processSwiftInbound } from '../../workflows/swift-inbound'; + +function requireApiKey(req: Request, res: Response): boolean { + const cfg = loadConfig(); + if (!cfg.production.requireApiKey) return true; + const key = req.header('Authorization')?.replace(/^Bearer\s+/i, '') || req.header('X-API-Key'); + if (key && key === process.env.OMNL_API_KEY) return true; + res.status(401).json({ error: 'Unauthorized' }); + return false; +} + +export function createSettlementRouter(): Router { + const router = Router(); + const officeId = settlementOfficeId(); + const profile = loadOfficeProfile(); + + router.get('/health', (_req, res) => { + let chainStats = { total: 0, active: 0 }; + try { + const reg = loadOmnlChainRegistry(); + chainStats = { total: reg.chains.length, active: getActiveChains().length }; + } catch { + /* optional */ + } + res.json({ + service: 'omnl-settlement-middleware', + status: 'ok', + mode: process.env.SETTLEMENT_MIDDLEWARE_CONFIG?.includes('production') ? 'production' : 'standard', + role: 'OMNL central-bank settlement orchestration', + chainSupport: { max: 128, ...chainStats }, + settlementOffice: { + officeId: profile.officeId, + externalId: profile.externalId, + name: profile.name, + }, + enforceSingleOffice: loadConfig().enforceSingleOffice ?? false, + }); + }); + + router.get('/office', (_req, res) => { + res.json({ + ...profile, + locked: true, + storePath: `data/settlement-store/office-${officeId}`, + }); + }); + + router.get('/money-supply', async (req, res) => { + if (!requireApiKey(req, res)) return; + try { + res.json(await getMoneySupply(officeId)); + } catch (e) { + res.status(500).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + + router.get('/money-supply/:officeIdParam', async (req, res) => { + if (!requireApiKey(req, res)) return; + try { + const requested = parseInt(req.params.officeIdParam, 10); + res.json(await getMoneySupply(settlementOfficeId(requested))); + } catch (e) { + res.status(400).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + + router.get('/settlements', (req, res) => { + if (!requireApiKey(req, res)) return; + res.json({ + officeId, + items: settlementStore.list(Number(req.query.limit ?? 50)), + }); + }); + + router.get('/settlements/:id', (req, res) => { + if (!requireApiKey(req, res)) return; + const rec = getSettlementStatus(req.params.id); + if (!rec) { + res.status(404).json({ error: 'Not found' }); + return; + } + res.json(rec); + }); + + router.post('/external-transfer', async (req, res) => { + if (!requireApiKey(req, res)) return; + try { + const merged = bindOffice24(req.body as ExternalTransferRequest); + const record = await processExternalTransfer(merged); + res.status(record.phase === 'FAILED' ? 422 : 200).json(record); + } catch (e) { + res.status(e instanceof Error && e.message.includes('locked') ? 403 : 500).json({ + error: e instanceof Error ? e.message : String(e), + }); + } + }); + + router.post('/swift/inbound', async (req, res) => { + if (!requireApiKey(req, res)) return; + try { + const raw = typeof req.body === 'string' ? req.body : req.body?.raw; + if (!raw) { + res.status(400).json({ error: 'raw SWIFT payload required' }); + return; + } + const record = await processSwiftInbound(String(raw)); + res.json(record); + } catch (e) { + res.status(500).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + + router.post('/trade-finance/:instrument', async (req, res) => { + if (!requireApiKey(req, res)) return; + const instrument = req.params.instrument.toUpperCase() as TradeFinanceInstrument; + if (!['SBLC', 'DLC', 'BG', 'LS'].includes(instrument)) { + res.status(400).json({ error: 'Invalid instrument' }); + return; + } + try { + const body = req.body as ExternalTransferRequest; + const merged = bindOffice24({ + ...body, + tradeFinance: { + instrument, + reference: body.tradeFinance?.reference ?? body.idempotencyKey, + expiryDate: body.tradeFinance?.expiryDate, + }, + rail: 'SWIFT', + moneyLayers: body.moneyLayers ?? ['M1', 'M2'], + }); + const record = await processExternalTransfer(merged); + res.status(record.phase === 'FAILED' ? 422 : 200).json(record); + } catch (e) { + res.status(500).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + + router.post('/convert/fiat-to-crypto', async (req, res) => { + if (!requireApiKey(req, res)) return; + try { + const body = req.body as ExternalTransferRequest; + const merged = bindOffice24({ + ...body, + rail: body.rail ?? 'INTERNAL', + convertToCrypto: { + enabled: true, + targetChainId: body.convertToCrypto?.targetChainId ?? 138, + tokenLineId: body.convertToCrypto?.tokenLineId ?? `${body.currency ?? 'USD'}-M2`, + recipientAddress: body.convertToCrypto?.recipientAddress ?? '', + }, + moneyLayers: ['M2'], + }); + const record = await processExternalTransfer(merged); + res.status(record.phase === 'FAILED' ? 422 : 200).json(record); + } catch (e) { + res.status(500).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + + router.post('/token-load', async (req, res) => { + if (!requireApiKey(req, res)) return; + try { + const body = req.body as TokenLoadRequest; + if (!body.idempotencyKey || !body.lineId || !body.amount || !body.recipientAddress) { + res.status(400).json({ error: 'idempotencyKey, lineId, amount, recipientAddress required' }); + return; + } + const record = await processTokenLoad({ + ...body, + officeId: settlementOfficeId(body.officeId), + }); + res.status(record.phase === 'FAILED' ? 422 : 200).json(record); + } catch (e) { + res.status(500).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + + router.post('/transfer/internal', async (req, res) => { + if (!requireApiKey(req, res)) return; + try { + const body = req.body as TransferRequest; + if (!body.idempotencyKey || !body.tokenAddress || !body.amount || !body.recipientAddress) { + res.status(400).json({ error: 'idempotencyKey, tokenAddress, amount, recipientAddress required' }); + return; + } + const record = await processTransfer({ + ...body, + officeId: settlementOfficeId(body.officeId), + rail: 'INTERNAL', + moneyLayers: body.moneyLayers ?? ['M2'], + }); + res.status(record.phase === 'FAILED' ? 422 : 200).json(record); + } catch (e) { + res.status(500).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + + router.post('/transfer/external', async (req, res) => { + if (!requireApiKey(req, res)) return; + try { + const body = req.body as TransferRequest; + if (!body.creditorIban) { + res.status(400).json({ error: 'creditorIban required for external transfer' }); + return; + } + const record = await processTransfer({ + ...body, + officeId: settlementOfficeId(body.officeId), + rail: body.rail === 'RTGS' ? 'RTGS' : 'SWIFT', + moneyLayers: body.moneyLayers ?? ['M2'], + }); + res.status(record.phase === 'FAILED' ? 422 : 200).json(record); + } catch (e) { + res.status(500).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + + router.get('/tokens/m2', (_req, res) => { + try { + const fs = require('fs') as typeof import('fs'); + const path = require('path') as typeof import('path'); + const p = + process.env.OMNL_M2_TOKEN_REGISTRY || + path.resolve(__dirname, '../../../../../config/omnl-m2-token-registry.v1.json'); + const registry = JSON.parse(fs.readFileSync(p, 'utf8')); + res.json(registry); + } catch (e) { + res.status(500).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + + router.get('/chains', (_req, res) => { + try { + const registry = loadOmnlChainRegistry(); + res.json({ + brand: registry.brand, + maxCapacity: registry.maxCapacity, + primaryChainId: registry.primaryChainId, + mirrorChainId: registry.mirrorChainId, + settlementOfficeId: registry.settlementOfficeId, + stats: registry.stats, + chains: registry.chains, + }); + } catch (e) { + res.status(500).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + + router.get('/chains/active', (_req, res) => { + try { + res.json({ count: getActiveChains().length, chains: getActiveChains() }); + } catch (e) { + res.status(500).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + + return router; +} diff --git a/services/settlement-middleware/src/config.ts b/services/settlement-middleware/src/config.ts new file mode 100644 index 0000000..c125f4d --- /dev/null +++ b/services/settlement-middleware/src/config.ts @@ -0,0 +1,112 @@ +import fs from 'fs'; +import path from 'path'; + +export type OfficeSettlementProfile = { + version: string; + officeId: number; + externalId: string; + name: string; + fineractTenant: string; + tenantUser: string; + adminUser: string; + operatorUser: string; + settlement: { + defaultCurrency: string; + defaultLineId: string; + moneyLayers: string[]; + glM0: string; + glM1: string; + glM2: string; + glSettlement: string; + glTradeFinanceContingent: string; + }; + rails: MiddlewareConfig['rails']; + omnlLegalName: string; + omnlLei: string; +}; + +export type MiddlewareConfig = { + version: string; + omnlLegalName: string; + defaultOfficeId: number; + enforceSingleOffice?: boolean; + settlementOffice?: { + officeId: number; + externalId: string; + name: string; + profilePath: string; + }; + defaultCurrency: string; + rails: { + swift: { enabled: boolean; listenerUrl: string }; + hybx: { enabled: boolean; sidecarUrl: string }; + chain138: { enabled: boolean; rpcEnv: string; defaultLineId: string }; + }; + moneySupply: { + glM0: string; + glM1: string; + glM2: string; + glSettlement: string; + }; + production: { + requireApiKey: boolean; + allowHybxProduction: boolean; + allowChainMintExecute: boolean; + }; +}; + +let cached: MiddlewareConfig | null = null; +let cachedOffice: OfficeSettlementProfile | null = null; + +function repoConfigPath(relative: string): string { + return path.resolve(__dirname, '../../../', relative.replace(/^\//, '')); +} + +export function loadConfig(): MiddlewareConfig { + if (cached) return cached; + const configPath = + process.env.SETTLEMENT_MIDDLEWARE_CONFIG || + repoConfigPath('config/settlement-middleware.v1.json'); + cached = JSON.parse(fs.readFileSync(configPath, 'utf8')) as MiddlewareConfig; + return cached; +} + +export function loadOfficeProfile(): OfficeSettlementProfile { + if (cachedOffice) return cachedOffice; + const cfg = loadConfig(); + const profileRel = + cfg.settlementOffice?.profilePath ?? 'config/offices/office-24-settlement.v1.json'; + const profilePath = process.env.OMNL_OFFICE_SETTLEMENT_PROFILE || repoConfigPath(profileRel); + cachedOffice = JSON.parse(fs.readFileSync(profilePath, 'utf8')) as OfficeSettlementProfile; + return cachedOffice; +} + +/** All settlement runs behind Office 24 (Ali HOSPITALLERS). */ +export function settlementOfficeId(requested?: number): number { + const cfg = loadConfig(); + const profile = loadOfficeProfile(); + const locked = cfg.settlementOffice?.officeId ?? profile.officeId ?? cfg.defaultOfficeId; + const fromEnv = parseInt( + process.env.ALI_HOSPITALLERS_OFFICE_ID || + process.env.OMNL_SETTLEMENT_OFFICE_ID || + String(locked), + 10, + ); + const officeId = fromEnv || locked; + + if (cfg.enforceSingleOffice && requested !== undefined && requested !== officeId) { + throw new Error(`Settlement is locked to office ${officeId}; requested ${requested}`); + } + if (cfg.enforceSingleOffice && officeId !== locked) { + throw new Error(`Settlement is locked to office ${locked}; env specifies ${officeId}`); + } + return officeId; +} + +export function port(): number { + return parseInt(process.env.SETTLEMENT_MIDDLEWARE_PORT || '3011', 10); +} + +export function settlementStoreSubdir(): string { + return `office-${settlementOfficeId()}`; +} diff --git a/services/settlement-middleware/src/index.ts b/services/settlement-middleware/src/index.ts new file mode 100644 index 0000000..919126c --- /dev/null +++ b/services/settlement-middleware/src/index.ts @@ -0,0 +1,10 @@ +import * as dotenv from 'dotenv'; +import path from 'path'; +import { existsSync } from 'fs'; +import { startServer } from './server'; + +const rootEnv = path.resolve(__dirname, '../../../.env'); +if (existsSync(rootEnv)) dotenv.config({ path: rootEnv }); +dotenv.config(); + +startServer(); diff --git a/services/settlement-middleware/src/server.ts b/services/settlement-middleware/src/server.ts new file mode 100644 index 0000000..0a536ab --- /dev/null +++ b/services/settlement-middleware/src/server.ts @@ -0,0 +1,21 @@ +import express from 'express'; +import cors from 'cors'; +import { createSettlementRouter } from './api/routes/settlement'; +import { port } from './config'; + +export function createApp() { + const app = express(); + app.use(cors()); + app.use(express.json({ limit: '2mb' })); + app.use(express.text({ type: 'text/plain', limit: '2mb' })); + app.use('/api/v1/settlement', createSettlementRouter()); + return app; +} + +export function startServer(): void { + const app = createApp(); + const p = port(); + app.listen(p, () => { + console.log(`OMNL settlement middleware listening on http://localhost:${p}/api/v1/settlement`); + }); +} diff --git a/services/settlement-middleware/src/store/settlement-store.ts b/services/settlement-middleware/src/store/settlement-store.ts new file mode 100644 index 0000000..7adcd22 --- /dev/null +++ b/services/settlement-middleware/src/store/settlement-store.ts @@ -0,0 +1,46 @@ +import fs from 'fs'; +import path from 'path'; +import type { SettlementRecord } from '@dbis/settlement-core'; +import { settlementStoreSubdir } from '../config'; + +const baseDir = + process.env.SETTLEMENT_STORE_DIR || + path.resolve(__dirname, '../../../data/settlement-store'); + +function storeDir(): string { + const dir = path.join(baseDir, settlementStoreSubdir()); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + return dir; +} + +export class SettlementStore { + save(record: SettlementRecord): void { + const dir = storeDir(); + fs.writeFileSync(path.join(dir, `${record.settlementId}.json`), JSON.stringify(record, null, 2)); + fs.writeFileSync(path.join(dir, `key-${record.idempotencyKey}.json`), record.settlementId); + } + + getById(id: string): SettlementRecord | null { + const p = path.join(storeDir(), `${id}.json`); + if (!fs.existsSync(p)) return null; + return JSON.parse(fs.readFileSync(p, 'utf8')) as SettlementRecord; + } + + getByIdempotencyKey(key: string): SettlementRecord | null { + const ref = path.join(storeDir(), `key-${key}.json`); + if (!fs.existsSync(ref)) return null; + const id = fs.readFileSync(ref, 'utf8').trim(); + return this.getById(id); + } + + list(limit = 50): SettlementRecord[] { + const dir = storeDir(); + return fs + .readdirSync(dir) + .filter((f) => f.endsWith('.json') && !f.startsWith('key-')) + .slice(0, limit) + .map((f) => JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8')) as SettlementRecord); + } +} + +export const settlementStore = new SettlementStore(); diff --git a/services/settlement-middleware/src/workflows/external-transfer.ts b/services/settlement-middleware/src/workflows/external-transfer.ts new file mode 100644 index 0000000..2244338 --- /dev/null +++ b/services/settlement-middleware/src/workflows/external-transfer.ts @@ -0,0 +1,171 @@ +import { v4 as uuidv4 } from 'uuid'; +import { + buildMoneySupplySnapshot, + isIbanValid, + m2FiatToTokenLoadAmount, + resolveInstrumentGl, + rollExternalTransferVerbiage, + swiftKindForRequest, + type ExternalTransferRequest, + type SettlementRecord, +} from '@dbis/settlement-core'; +import { loadConfig, loadOfficeProfile, settlementOfficeId } from '../config'; +import { fetchGlBalances, postJournalEntry } from '../adapters/fineract'; +import { HybxProductionRail } from '../adapters/hybx-production'; +import { archiveIso20022, requestTokenMint } from '../adapters/omnl'; +import { buildMt102Stub, buildMt103Stub, forwardSwiftToListener } from '../adapters/swift'; +import { settlementStore } from '../store/settlement-store'; + +function glId(code: string): number { + return parseInt(code, 10); +} + +export async function processExternalTransfer( + input: ExternalTransferRequest, +): Promise { + const cfg = loadConfig(); + const profile = loadOfficeProfile(); + const officeId = settlementOfficeId(input.officeId); + const req: ExternalTransferRequest = { ...input, officeId }; + + const existing = settlementStore.getByIdempotencyKey(req.idempotencyKey); + if (existing?.phase === 'SETTLED') return existing; + + const now = new Date().toISOString(); + let record: SettlementRecord = existing ?? { + settlementId: uuidv4(), + idempotencyKey: req.idempotencyKey, + phase: 'RECEIVED', + request: req, + errors: [], + createdAt: now, + updatedAt: now, + }; + + const advance = (phase: SettlementRecord['phase'], patch: Partial = {}) => { + record = { ...record, phase, updatedAt: new Date().toISOString(), ...patch }; + settlementStore.save(record); + }; + + try { + if (!isIbanValid(req.creditorIban)) { + throw new Error(`Invalid creditor IBAN: ${req.creditorIban}`); + } + advance('VALIDATED'); + + const verbiage = rollExternalTransferVerbiage(req); + advance('VERBIAGE_ROLLED', { verbiageDocument: verbiage }); + + const amount = parseFloat(req.amount) || 0; + const tf = req.tradeFinance; + const gl = tf ? resolveInstrumentGl(tf.instrument) : null; + const debitGl = gl?.debitGl ?? profile.settlement.glSettlement ?? cfg.moneySupply.glSettlement; + const creditGl = gl?.creditGl ?? profile.settlement.glM1 ?? cfg.moneySupply.glM1; + + if (amount > 0) { + const je = await postJournalEntry({ + officeId: req.officeId, + transactionDate: req.valueDate, + referenceNumber: req.idempotencyKey, + comments: verbiage.slice(0, 500), + debits: [{ glAccountId: glId(debitGl), amount }], + credits: [{ glAccountId: glId(creditGl), amount }], + }); + advance('FINERACT_POSTED', { fineractJournalRef: String(je.resourceId) }); + } else { + advance('FINERACT_POSTED'); + } + + if (cfg.rails.hybx.enabled && req.rail !== 'INTERNAL') { + const hybx = new HybxProductionRail(); + const pay = await hybx.dispatchPayment({ + idempotencyKey: req.idempotencyKey, + amount: req.amount, + currency: req.currency, + creditorIban: req.creditorIban, + creditorBic: req.creditorBic, + beneficiaryName: req.beneficiaryName, + remittanceInfo: req.remittanceInfo, + rail: req.rail, + }); + advance('HYBX_RAIL_DISPATCHED', { hybxPaymentId: pay.paymentId }); + } else { + advance('HYBX_RAIL_DISPATCHED'); + } + + const swiftKind = swiftKindForRequest(req); + const swiftRaw = + swiftKind === 'MT102' + ? buildMt102Stub({ + senderRef: req.idempotencyKey, + currency: req.currency, + amount: req.amount, + valueDate: req.valueDate, + orderingInstitution: req.orderingName ?? cfg.omnlLegalName, + beneficiary: req.beneficiaryName, + iban: req.creditorIban, + remittance: req.remittanceInfo, + }) + : buildMt103Stub({ + senderRef: req.idempotencyKey, + currency: req.currency, + amount: req.amount, + valueDate: req.valueDate, + orderingCustomer: req.orderingName ?? cfg.omnlLegalName, + beneficiary: req.beneficiaryName, + iban: req.creditorIban, + bic: req.creditorBic, + remittance: req.remittanceInfo, + }); + + if (cfg.rails.swift.enabled) { + await forwardSwiftToListener(swiftRaw); + } + const iso = await archiveIso20022({ + messageType: swiftKind === 'MT102' ? 'pacs.008' : 'pacs.008', + direction: 'OUTBOUND', + raw: swiftRaw, + settlementRef: record.settlementId, + }); + advance('ISO20022_ARCHIVED', { iso20022MessageId: iso.messageId }); + + if (req.convertToCrypto?.enabled) { + const balances = await fetchGlBalances(req.officeId); + const snap = buildMoneySupplySnapshot(req.officeId, balances); + const { loadAmount, fullyBacked } = m2FiatToTokenLoadAmount( + req.amount, + snap.M2.broadMoney, + ); + if (!fullyBacked) { + record.errors.push(`M2 backing insufficient for token load (${loadAmount}/${req.amount})`); + } + const mint = await requestTokenMint({ + lineId: req.convertToCrypto.tokenLineId, + amount: loadAmount, + recipient: req.convertToCrypto.recipientAddress, + settlementRef: record.settlementId, + dryRun: !cfg.production.allowChainMintExecute, + }); + advance('CHAIN_MINT_REQUESTED', { chainTxHash: mint.txHash }); + } else { + advance('CHAIN_MINT_REQUESTED'); + } + + advance('SETTLED'); + return record; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + record.errors.push(msg); + advance('FAILED'); + return record; + } +} + +export async function getMoneySupply(officeId: number) { + const balances = await fetchGlBalances(officeId); + return buildMoneySupplySnapshot(officeId, balances); +} + +export function getSettlementStatus(id: string): SettlementRecord | null { + return settlementStore.getById(id); +} diff --git a/services/settlement-middleware/src/workflows/office-scope.ts b/services/settlement-middleware/src/workflows/office-scope.ts new file mode 100644 index 0000000..99c602d --- /dev/null +++ b/services/settlement-middleware/src/workflows/office-scope.ts @@ -0,0 +1,33 @@ +import type { ExternalTransferRequest } from '@dbis/settlement-core'; +import { loadConfig, loadOfficeProfile, settlementOfficeId } from '../config'; + +/** Bind every settlement request to Office 24 (same profile as OMNL terminal). */ +export function bindOffice24(req: Partial): ExternalTransferRequest { + const cfg = loadConfig(); + const profile = loadOfficeProfile(); + const officeId = settlementOfficeId(req.officeId); + + if (!req.idempotencyKey || !req.creditorIban || !req.amount) { + throw new Error('idempotencyKey, creditorIban, amount required'); + } + + return { + officeId, + valueDate: req.valueDate ?? new Date().toISOString().slice(0, 10), + currency: req.currency ?? profile.settlement.defaultCurrency ?? cfg.defaultCurrency, + moneyLayers: (req.moneyLayers ?? profile.settlement.moneyLayers) as ExternalTransferRequest['moneyLayers'], + rail: req.rail ?? 'SWIFT', + remittanceInfo: req.remittanceInfo ?? `OMNL Office 24 settlement — ${profile.externalId}`, + beneficiaryName: req.beneficiaryName ?? 'Beneficiary', + idempotencyKey: req.idempotencyKey, + creditorIban: req.creditorIban, + amount: req.amount, + debtorIban: req.debtorIban, + creditorBic: req.creditorBic, + orderingName: req.orderingName ?? profile.omnlLegalName, + swiftMessageKind: req.swiftMessageKind, + tradeFinance: req.tradeFinance, + rwaVerbiage: req.rwaVerbiage, + convertToCrypto: req.convertToCrypto, + }; +} diff --git a/services/settlement-middleware/src/workflows/swift-inbound.ts b/services/settlement-middleware/src/workflows/swift-inbound.ts new file mode 100644 index 0000000..a0f3b63 --- /dev/null +++ b/services/settlement-middleware/src/workflows/swift-inbound.ts @@ -0,0 +1,49 @@ +import { v4 as uuidv4 } from 'uuid'; +import type { ExternalTransferRequest, SettlementRecord } from '@dbis/settlement-core'; +import { forwardSwiftToListener } from '../adapters/swift'; +import { settlementOfficeId } from '../config'; +import { settlementStore } from '../store/settlement-store'; +import { processExternalTransfer } from './external-transfer'; + +/** Inbound MT103/MT102 → settlement workflow */ +export async function processSwiftInbound(raw: string): Promise { + await forwardSwiftToListener(raw); + + const isMt102 = /\bMT102\b|:23:/.test(raw) || /:32B:/.test(raw); + const refMatch = raw.match(/:20:([^\n]+)/); + const amtMatch = raw.match(/:32[AB]:(\d{6})([A-Z]{3})([\d,]+)/); + const ibanMatch = raw.match(/\/([A-Z]{2}\d{2}[A-Z0-9]+)/); + const bnfMatch = raw.match(/:59:[^\n]*\n([^\n:]+)/); + const remMatch = raw.match(/:70:([^\n]+)/); + + const req: ExternalTransferRequest = { + idempotencyKey: refMatch?.[1]?.trim() || `SWIFT-IN-${uuidv4()}`, + officeId: settlementOfficeId(), + valueDate: new Date().toISOString().slice(0, 10), + currency: amtMatch?.[2] ?? 'USD', + amount: (amtMatch?.[3] ?? '0').replace(',', '.'), + creditorIban: ibanMatch?.[1] ?? '', + beneficiaryName: bnfMatch?.[1]?.trim() ?? 'Inbound beneficiary', + remittanceInfo: remMatch?.[1]?.trim() ?? 'Inbound SWIFT settlement', + moneyLayers: isMt102 ? ['M1', 'M2'] : ['M0', 'M1'], + rail: 'SWIFT', + swiftMessageKind: isMt102 ? 'MT102' : 'MT103', + rwaVerbiage: { assetClass: 'RWA', tokenLineId: 'USD-M1', externalTransferRoll: true }, + }; + + if (!req.creditorIban) { + const stub: SettlementRecord = { + settlementId: uuidv4(), + idempotencyKey: req.idempotencyKey, + phase: 'RECEIVED', + request: req, + errors: ['Parsed inbound SWIFT — manual IBAN review required'], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + settlementStore.save(stub); + return stub; + } + + return processExternalTransfer(req); +} diff --git a/services/settlement-middleware/src/workflows/token-load.ts b/services/settlement-middleware/src/workflows/token-load.ts new file mode 100644 index 0000000..152d8c6 --- /dev/null +++ b/services/settlement-middleware/src/workflows/token-load.ts @@ -0,0 +1,115 @@ +import { v4 as uuidv4 } from 'uuid'; +import { + buildMoneySupplySnapshot, + GL_CODES, + m2FiatToTokenLoadAmount, + type TokenLoadRequest, + type SettlementRecord, +} from '@dbis/settlement-core'; +import { loadConfig, loadOfficeProfile, settlementOfficeId } from '../config'; +import { fetchGlBalances, postJournalEntry } from '../adapters/fineract'; +import { requestTokenMint } from '../adapters/omnl'; +import { settlementStore } from '../store/settlement-store'; + +function glId(code: string): number { + return parseInt(code, 10); +} + +export async function processTokenLoad(input: TokenLoadRequest): Promise { + const cfg = loadConfig(); + const profile = loadOfficeProfile(); + const officeId = settlementOfficeId(input.officeId); + const req = { ...input, officeId }; + + const existing = settlementStore.getByIdempotencyKey(req.idempotencyKey); + if (existing?.phase === 'SETTLED') return existing; + + const now = new Date().toISOString(); + let record: SettlementRecord = existing ?? { + settlementId: uuidv4(), + idempotencyKey: req.idempotencyKey, + phase: 'RECEIVED', + request: { + idempotencyKey: req.idempotencyKey, + officeId, + valueDate: now.slice(0, 10), + currency: req.currency ?? 'USD', + amount: req.amount, + creditorIban: 'INTERNAL-M2-TOKEN-LOAD', + beneficiaryName: req.recipientAddress, + remittanceInfo: `M2 token load ${req.lineId} → ${req.symbol ?? req.tokenAddress ?? 'token'}`, + moneyLayers: ['M2'], + rail: 'INTERNAL', + convertToCrypto: { + enabled: true, + targetChainId: 138, + tokenLineId: req.lineId, + recipientAddress: req.recipientAddress, + }, + }, + errors: [], + createdAt: now, + updatedAt: now, + }; + + const advance = (phase: SettlementRecord['phase'], patch: Partial = {}) => { + record = { ...record, phase, updatedAt: new Date().toISOString(), ...patch }; + settlementStore.save(record); + }; + + try { + advance('VALIDATED'); + advance('VERBIAGE_ROLLED', { + verbiageDocument: [ + `OMNL M2 fiat-backed token load`, + `Office ${officeId} · line ${req.lineId}`, + `Amount ${req.amount} · recipient ${req.recipientAddress}`, + `GL ${GL_CODES.M2_BROAD} → ${GL_CODES.CRYPTO_TOKEN_LIABILITY}`, + ].join('\n'), + }); + + const balances = await fetchGlBalances(officeId); + const snap = buildMoneySupplySnapshot(officeId, balances); + const { loadAmount, fullyBacked } = m2FiatToTokenLoadAmount(req.amount, snap.M2.broadMoney); + if (!fullyBacked) { + record.errors.push(`M2 backing insufficient (${loadAmount}/${req.amount})`); + advance('FAILED'); + return record; + } + + const amount = parseFloat(loadAmount) || 0; + if (amount > 0) { + const je = await postJournalEntry({ + officeId, + transactionDate: now.slice(0, 10), + referenceNumber: req.idempotencyKey, + comments: `M2 token load ${req.lineId}`, + debits: [{ glAccountId: glId(profile.settlement.glM2 ?? GL_CODES.M2_BROAD), amount }], + credits: [{ glAccountId: glId(GL_CODES.CRYPTO_TOKEN_LIABILITY), amount }], + }); + advance('FINERACT_POSTED', { fineractJournalRef: String(je.resourceId) }); + } else { + advance('FINERACT_POSTED'); + } + + advance('HYBX_RAIL_DISPATCHED'); + advance('ISO20022_ARCHIVED'); + + const mint = await requestTokenMint({ + lineId: req.lineId, + amount: loadAmount, + recipient: req.recipientAddress, + settlementRef: record.settlementId, + dryRun: !cfg.production.allowChainMintExecute, + symbol: req.symbol, + tokenAddress: req.tokenAddress, + }); + advance('CHAIN_MINT_REQUESTED', { chainTxHash: mint.txHash }); + advance('SETTLED'); + return record; + } catch (err) { + record.errors.push(err instanceof Error ? err.message : String(err)); + advance('FAILED'); + return record; + } +} diff --git a/services/settlement-middleware/src/workflows/transfer.ts b/services/settlement-middleware/src/workflows/transfer.ts new file mode 100644 index 0000000..b4ddcd8 --- /dev/null +++ b/services/settlement-middleware/src/workflows/transfer.ts @@ -0,0 +1,132 @@ +import { v4 as uuidv4 } from 'uuid'; +import { GL_CODES, type TransferRequest, type SettlementRecord } from '@dbis/settlement-core'; +import { loadConfig, loadOfficeProfile, settlementOfficeId } from '../config'; +import { postJournalEntry } from '../adapters/fineract'; +import { HybxProductionRail } from '../adapters/hybx-production'; +import { archiveIso20022 } from '../adapters/omnl'; +import { buildMt103Stub, forwardSwiftToListener } from '../adapters/swift'; +import { settlementStore } from '../store/settlement-store'; + +function glId(code: string): number { + return parseInt(code, 10); +} + +export async function processTransfer(input: TransferRequest): Promise { + const cfg = loadConfig(); + const profile = loadOfficeProfile(); + const officeId = settlementOfficeId(input.officeId); + const req = { ...input, officeId }; + const isExternal = req.rail === 'SWIFT' || req.rail === 'RTGS'; + + const existing = settlementStore.getByIdempotencyKey(req.idempotencyKey); + if (existing?.phase === 'SETTLED') return existing; + + const now = new Date().toISOString(); + let record: SettlementRecord = existing ?? { + settlementId: uuidv4(), + idempotencyKey: req.idempotencyKey, + phase: 'RECEIVED', + request: { + idempotencyKey: req.idempotencyKey, + officeId, + valueDate: now.slice(0, 10), + currency: req.currency ?? 'USD', + amount: req.amount, + creditorIban: req.creditorIban ?? (isExternal ? '' : 'INTERNAL-CHAIN138'), + beneficiaryName: req.beneficiaryName ?? req.recipientAddress, + remittanceInfo: req.remittanceInfo ?? `Transfer ${req.tokenSymbol} → ${req.recipientAddress}`, + moneyLayers: req.moneyLayers.length ? req.moneyLayers : ['M2'], + rail: req.rail === 'CHAIN138' ? 'CHAIN138' : req.rail === 'INTERNAL' ? 'INTERNAL' : 'SWIFT', + }, + errors: [], + createdAt: now, + updatedAt: now, + }; + + const advance = (phase: SettlementRecord['phase'], patch: Partial = {}) => { + record = { ...record, phase, updatedAt: new Date().toISOString(), ...patch }; + settlementStore.save(record); + }; + + try { + if (isExternal && !req.creditorIban) { + throw new Error('creditorIban required for external transfer'); + } + advance('VALIDATED'); + advance('VERBIAGE_ROLLED', { + verbiageDocument: [ + `OMNL ${req.rail} transfer · ${req.tokenSymbol}`, + `M2 layer · ${req.amount}`, + `To ${req.recipientAddress}`, + isExternal ? `IBAN ${req.creditorIban}` : 'Internal / Chain 138', + ].join('\n'), + }); + + const amount = parseFloat(req.amount) || 0; + if (amount > 0) { + const je = await postJournalEntry({ + officeId, + transactionDate: now.slice(0, 10), + referenceNumber: req.idempotencyKey, + comments: `${req.rail} transfer ${req.tokenSymbol}`, + debits: [{ glAccountId: glId(profile.settlement.glM2 ?? GL_CODES.M2_BROAD), amount }], + credits: [{ glAccountId: glId(profile.settlement.glSettlement ?? GL_CODES.SETTLEMENT_SUSPENSE), amount }], + }); + advance('FINERACT_POSTED', { fineractJournalRef: String(je.resourceId) }); + } else { + advance('FINERACT_POSTED'); + } + + if (isExternal && cfg.rails.hybx.enabled) { + const hybx = new HybxProductionRail(); + const pay = await hybx.dispatchPayment({ + idempotencyKey: req.idempotencyKey, + amount: req.amount, + currency: req.currency ?? 'USD', + creditorIban: req.creditorIban!, + beneficiaryName: req.beneficiaryName ?? req.recipientAddress, + remittanceInfo: req.remittanceInfo ?? `OMNL ${req.tokenSymbol} external transfer`, + rail: req.rail === 'RTGS' ? 'RTGS' : 'SWIFT', + }); + advance('HYBX_RAIL_DISPATCHED', { hybxPaymentId: pay.paymentId }); + } else { + advance('HYBX_RAIL_DISPATCHED'); + } + + if (isExternal && cfg.rails.swift.enabled && req.creditorIban) { + const swiftRaw = buildMt103Stub({ + senderRef: req.idempotencyKey, + currency: req.currency ?? 'USD', + amount: req.amount, + valueDate: now.slice(0, 10), + orderingCustomer: cfg.omnlLegalName, + beneficiary: req.beneficiaryName ?? req.recipientAddress, + iban: req.creditorIban, + remittance: req.remittanceInfo ?? `OMNL ${req.tokenSymbol}`, + }); + await forwardSwiftToListener(swiftRaw); + const iso = await archiveIso20022({ + messageType: 'pacs.008', + direction: 'OUTBOUND', + raw: swiftRaw, + settlementRef: record.settlementId, + }); + advance('ISO20022_ARCHIVED', { iso20022MessageId: iso.messageId }); + } else { + advance('ISO20022_ARCHIVED'); + } + + advance('CHAIN_MINT_REQUESTED', { + verbiageDocument: [ + record.verbiageDocument, + `On-chain ERC-20 transfer prepared: ${req.tokenAddress} → ${req.recipientAddress}`, + ].join('\n'), + }); + advance('SETTLED'); + return record; + } catch (err) { + record.errors.push(err instanceof Error ? err.message : String(err)); + advance('FAILED'); + return record; + } +} diff --git a/services/settlement-middleware/tsconfig.json b/services/settlement-middleware/tsconfig.json new file mode 100644 index 0000000..e92074a --- /dev/null +++ b/services/settlement-middleware/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"] +} diff --git a/services/swift-listener/src/parsers/inboundParser.ts b/services/swift-listener/src/parsers/inboundParser.ts index 49461fa..584836f 100644 --- a/services/swift-listener/src/parsers/inboundParser.ts +++ b/services/swift-listener/src/parsers/inboundParser.ts @@ -99,6 +99,24 @@ function parseMt103(fields: Record) { }; } +function parseMt102(fields: Record) { + const amt32b = fields['32B'] ?? fields['32A'] ?? ''; + const parts = amt32b.trim().split(/\s+/); + const currency = parts[0]?.length === 3 ? parts[0] : parts[1] ?? ''; + const amount = parts[0]?.length === 3 ? parts[1] ?? '' : parts[2] ?? parts[0] ?? ''; + return { + senderReference: fields['20'] ?? '', + relatedReference: fields['21'] ?? '', + currency, + amount, + orderingInstitution: fields['50A'] ?? fields['50K'] ?? fields['50F'] ?? '', + beneficiaryCustomer: fields['59'] ?? fields['59A'] ?? '', + accountWithInstitution: fields['57A'] ?? fields['57D'] ?? '', + remittanceInfo: fields['70'] ?? '', + senderToReceiverInfo: fields['72'] ?? '', + }; +} + function parseMt202(fields: Record) { const amt = split32A(fields['32A'] ?? ''); return { @@ -152,6 +170,9 @@ export function parseSwiftFin(raw: string): ParsedSwiftFin { case 'MT103': parsed = parseMt103(fields); break; + case 'MT102': + parsed = parseMt102(fields); + break; case 'MT202': parsed = parseMt202(fields); break; diff --git a/services/swift-listener/src/types.ts b/services/swift-listener/src/types.ts index c691179..d18d20b 100644 --- a/services/swift-listener/src/types.ts +++ b/services/swift-listener/src/types.ts @@ -2,6 +2,7 @@ import type { CanonicalPaymentMessage } from '@dbis/checkpoint-core'; export type SwiftFinMessageType = | 'MT103' + | 'MT102' | 'MT202' | 'MT910' | 'MT940' diff --git a/services/token-aggregation/src/api/routes/omnl.ts b/services/token-aggregation/src/api/routes/omnl.ts index 096f11c..38bfe4c 100644 --- a/services/token-aggregation/src/api/routes/omnl.ts +++ b/services/token-aggregation/src/api/routes/omnl.ts @@ -326,4 +326,51 @@ router.get('/omnl/health', async (req: Request, res: Response) => { } }); +/** + * POST /omnl/settlement/token-load — M2 fiat-backed token mint (Office 24 settlement). + */ +router.post('/omnl/settlement/token-load', (req: Request, res: Response) => { + const { lineId, amount, recipient, settlementRef, dryRun, symbol, tokenAddress } = req.body as { + lineId?: string; + amount?: string; + recipient?: string; + settlementRef?: string; + dryRun?: boolean; + symbol?: string; + tokenAddress?: string; + }; + if (!lineId || !amount || !recipient) { + res.status(400).json({ error: 'lineId, amount, recipient required' }); + return; + } + const execute = + !dryRun && + (process.env.SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE === '1' || + process.env.OMNL_ALLOW_CHAIN_MINT_EXECUTE === '1'); + const loadId = settlementRef ?? `TL-${Date.now()}`; + res.json({ + status: execute ? 'QUEUED' : 'DRY_RUN', + loadId, + lineId, + amount, + recipient, + symbol: symbol ?? null, + tokenAddress: tokenAddress ?? null, + settlementRef: settlementRef ?? null, + moneyLayer: 'M2', + loadFromGl: '2200', + creditGl: '2300', + capabilities: { + swappable: true, + convertible: true, + transferableInternal: true, + transferableExternal: true, + }, + txHash: execute ? undefined : null, + message: execute + ? 'Token load queued for ComplianceCore mint pipeline (M2 → on-chain)' + : 'M2 token load validated — set SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE=1 to mint on-chain', + }); +}); + export default router;