feat: OMNL Bank online production — 128 chains, Central Bank UI, settlement stack
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m22s
CI/CD Pipeline / Security Scanning (push) Successful in 2m30s
CI/CD Pipeline / Lint and Format (push) Failing after 44s
CI/CD Pipeline / Terraform Validation (push) Failing after 27s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 31s
Deploy ChainID 138 / Deploy ChainID 138 (push) Failing after 56s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 29s
Validation / validate-genesis (push) Successful in 34s
Validation / validate-terraform (push) Failing after 39s
Validation / validate-kubernetes (push) Failing after 14s
Validation / validate-smart-contracts (push) Failing after 20s
Validation / validate-security (push) Failing after 1m19s
Validation / validate-documentation (push) Failing after 27s
Verify Deployment / Verify Deployment (push) Failing after 1m13s
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m22s
CI/CD Pipeline / Security Scanning (push) Successful in 2m30s
CI/CD Pipeline / Lint and Format (push) Failing after 44s
CI/CD Pipeline / Terraform Validation (push) Failing after 27s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 31s
Deploy ChainID 138 / Deploy ChainID 138 (push) Failing after 56s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 29s
Validation / validate-genesis (push) Successful in 34s
Validation / validate-terraform (push) Failing after 39s
Validation / validate-kubernetes (push) Failing after 14s
Validation / validate-smart-contracts (push) Failing after 20s
Validation / validate-security (push) Failing after 1m19s
Validation / validate-documentation (push) Failing after 27s
Verify Deployment / Verify Deployment (push) Failing after 1m13s
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
27
config/dbis-exchange.production.v1.json
Normal file
27
config/dbis-exchange.production.v1.json
Normal file
@@ -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"
|
||||
|
||||
},
|
||||
30
config/dbis-exchange.v1.json
Normal file
30
config/dbis-exchange.v1.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
50
config/deployment-omnl.production.env.example
Normal file
50
config/deployment-omnl.production.env.example
Normal file
@@ -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_<id>_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
|
||||
28
config/offices/office-24-settlement.v1.json
Normal file
28
config/offices/office-24-settlement.v1.json
Normal file
@@ -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"
|
||||
}
|
||||
527
config/omnl-m2-token-registry.v1.json
Normal file
527
config/omnl-m2-token-registry.v1.json
Normal file
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
2320
config/omnl-supported-chains.v1.json
Normal file
2320
config/omnl-supported-chains.v1.json
Normal file
File diff suppressed because it is too large
Load Diff
35
config/settlement-middleware.production.v1.json
Normal file
35
config/settlement-middleware.production.v1.json
Normal file
@@ -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" },
|
||||
40
config/settlement-middleware.v1.json
Normal file
40
config/settlement-middleware.v1.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
30
config/trade-finance/instruments.v1.json
Normal file
30
config/trade-finance/instruments.v1.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
14
deploy/nginx/frontend-spa.conf
Normal file
14
deploy/nginx/frontend-spa.conf
Normal file
@@ -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";
|
||||
}
|
||||
}
|
||||
70
deploy/nginx/omnl-bank.conf
Normal file
70
deploy/nginx/omnl-bank.conf
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
99
docker-compose.omnl-production.yml
Normal file
99
docker-compose.omnl-production.yml
Normal file
@@ -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:
|
||||
20
frontend-dapp/Dockerfile.production
Normal file
20
frontend-dapp/Dockerfile.production
Normal file
@@ -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;"]
|
||||
@@ -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() {
|
||||
>
|
||||
<ToastProvider />
|
||||
<AdminProvider>
|
||||
<Layout>
|
||||
<Routes>
|
||||
<Routes>
|
||||
<Route element={<OmnlProductLayout />}>
|
||||
<Route path="/central-bank" element={<CentralBankPage />} />
|
||||
<Route path="/office-24" element={<Office24Page />} />
|
||||
<Route path="/trade" element={<TradePage />} />
|
||||
</Route>
|
||||
<Route element={<Layout />}>
|
||||
<Route path="/" element={<BridgePage />} />
|
||||
<Route path="/swap" element={<SwapPage />} />
|
||||
<Route path="/reserve" element={<ReservePage />} />
|
||||
@@ -57,8 +62,8 @@ function App() {
|
||||
<Route path="/admin" element={<AdminPanel />} />
|
||||
<Route path="/docs" element={<DocsPage />} />
|
||||
<Route path="/wallets" element={<WalletsDemoPage />} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
</Route>
|
||||
</Routes>
|
||||
</AdminProvider>
|
||||
</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
@@ -69,4 +74,3 @@ function App() {
|
||||
}
|
||||
|
||||
export default App
|
||||
|
||||
|
||||
@@ -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({
|
||||
/>
|
||||
)}
|
||||
<div className="relative">
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="text-[20px] font-semibold text-white flex items-center gap-2">
|
||||
Bridge to
|
||||
{destination && (
|
||||
<>
|
||||
<ChainIcon chainId={destination.chainId} name={destination.name} size={24} />
|
||||
<span>{destination.name}</span>
|
||||
</>
|
||||
)}
|
||||
{!destination && <span>Ethereum Mainnet</span>}
|
||||
</h2>
|
||||
<Tooltip content="Refresh all balances and allowances">
|
||||
<button
|
||||
onClick={handleRefreshBalances}
|
||||
disabled={!address}
|
||||
className="p-2 text-teal-400 hover:text-teal-300 hover:bg-teal-500/20 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed border border-white/20"
|
||||
aria-label="Refresh balances"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<p className="text-[#A0A0A0] text-sm mt-1">
|
||||
Wrap ETH, approve tokens, and bridge WETH9 via CCIP
|
||||
<div className="mb-6 flex items-center justify-between gap-3">
|
||||
<p className="ui-muted text-sm">
|
||||
Wrap ETH, approve tokens, and bridge WETH9 via CCIP to{' '}
|
||||
{destination?.name ?? 'Ethereum Mainnet'}
|
||||
</p>
|
||||
<Tooltip content="Refresh all balances and allowances">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRefreshBalances}
|
||||
disabled={!address}
|
||||
className="ui-btn-icon disabled:opacity-50 disabled:cursor-not-allowed shrink-0"
|
||||
aria-label="Refresh balances"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<label className="flex items-center gap-2 text-[13px] font-medium mb-2 text-[#A0A0A0]">
|
||||
<label className="ui-label">
|
||||
<TokenIcon symbol="ETH" size={20} />
|
||||
<span>Amount (ETH)</span>
|
||||
<Tooltip content="Enter the amount of ETH you want to bridge. This will be wrapped to WETH9 first.">
|
||||
@@ -429,10 +418,8 @@ function BridgeButtonsConnected({
|
||||
min="0"
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
className={`w-full min-h-[48px] pl-4 pr-24 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-teal-500 focus:ring-offset-2 focus:ring-offset-[#252830] transition-all text-lg bg-[#1a1d24] text-white placeholder:text-[#A0A0A0] hover:border-white/30 ${
|
||||
amountError
|
||||
? 'border-red-500 focus:border-red-500'
|
||||
: 'border-white/20 focus:border-teal-500'
|
||||
className={`ui-input pl-4 pr-24 text-lg ${
|
||||
amountError ? '!border-red-500 focus:!border-red-500' : ''
|
||||
}`}
|
||||
placeholder="0.0"
|
||||
aria-invalid={!!amountError}
|
||||
@@ -444,7 +431,7 @@ function BridgeButtonsConnected({
|
||||
setAmount(ethBalance.displayValue);
|
||||
setAmountError('');
|
||||
}}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 px-4 py-2 min-h-[36px] text-sm font-medium bg-teal-600 text-white rounded-lg hover:bg-teal-500 transition-colors"
|
||||
className="ui-btn-primary absolute right-2 top-1/2 -translate-y-1/2 !py-2 !min-h-0 text-sm"
|
||||
>
|
||||
MAX
|
||||
</button>
|
||||
@@ -461,7 +448,7 @@ function BridgeButtonsConnected({
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<label htmlFor="bridge-recipient-address" className="block text-[13px] font-medium mb-2 text-[#A0A0A0]">
|
||||
<label htmlFor="bridge-recipient-address" className="ui-label">
|
||||
Recipient Address
|
||||
<Tooltip content="The Ethereum address that will receive the bridged tokens on the destination chain.">
|
||||
<span className="ml-2 text-white/60 cursor-help text-xs">ℹ️</span>
|
||||
@@ -474,10 +461,8 @@ function BridgeButtonsConnected({
|
||||
type="text"
|
||||
value={recipient}
|
||||
onChange={(e) => setRecipient(e.target.value)}
|
||||
className={`w-full min-h-[48px] p-4 border rounded-xl focus:outline-none focus:ring-2 focus:ring-teal-500 focus:ring-offset-2 focus:ring-offset-[#252830] transition-all font-mono text-base bg-[#1a1d24] text-white placeholder:text-[#A0A0A0] hover:border-white/30 ${
|
||||
recipientError
|
||||
? 'border-red-500 focus:border-red-500'
|
||||
: 'border-white/20 focus:border-teal-500'
|
||||
className={`ui-input font-mono pr-36 ${
|
||||
recipientError ? '!border-red-500 focus:!border-red-500' : ''
|
||||
}`}
|
||||
placeholder="0x..."
|
||||
aria-invalid={!!recipientError}
|
||||
@@ -490,7 +475,7 @@ function BridgeButtonsConnected({
|
||||
setRecipient(address);
|
||||
setRecipientError('');
|
||||
}}
|
||||
className="px-4 py-2 text-sm font-medium bg-[#252830] text-white rounded-lg hover:bg-white/10 transition-colors border border-white/20"
|
||||
className="ui-btn-secondary !py-2 text-sm"
|
||||
>
|
||||
Use my address
|
||||
</button>
|
||||
@@ -510,9 +495,9 @@ function BridgeButtonsConnected({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mb-8 p-6 bg-[#1a1d24] rounded-xl border border-white/10">
|
||||
<div className="mb-8 p-6 ui-inset rounded-xl">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-[20px] font-semibold text-[#A0A0A0]">Balances & Fees</h3>
|
||||
<h3 className="text-lg font-semibold ui-muted">Balances & Fees</h3>
|
||||
{address && (
|
||||
<CopyButton text={address} className="text-xs">
|
||||
<span className="text-xs">Copy Address</span>
|
||||
@@ -520,49 +505,49 @@ function BridgeButtonsConnected({
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="flex justify-between items-center p-4 bg-[#252830] rounded-lg border border-white/10 font-mono text-sm">
|
||||
<span className="font-medium text-[#A0A0A0] flex items-center gap-2">
|
||||
<div className="ui-stat-row">
|
||||
<span className="ui-muted font-medium flex items-center gap-2">
|
||||
<TokenIcon symbol="ETH" size={20} />
|
||||
ETH Balance:
|
||||
</span>
|
||||
<span className="text-white font-medium">
|
||||
{ethBalance ? ethBalance.displayValue : <LoadingSkeleton />} <span className="text-[#A0A0A0]">ETH</span>
|
||||
<span className="font-medium">
|
||||
{ethBalance ? ethBalance.displayValue : <LoadingSkeleton />} <span className="ui-muted">ETH</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center p-4 bg-[#252830] rounded-lg border border-white/10 font-mono text-sm">
|
||||
<span className="font-medium text-[#A0A0A0] flex items-center gap-2">
|
||||
<div className="ui-stat-row">
|
||||
<span className="ui-muted font-medium flex items-center gap-2">
|
||||
<TokenIcon symbol="WETH9" size={20} />
|
||||
WETH9 Balance:
|
||||
</span>
|
||||
<span className="text-white font-medium">
|
||||
<span className="font-medium">
|
||||
{address && weth9Balance !== undefined
|
||||
? `${ethers.utils.formatEther(weth9Balance.toString())}`
|
||||
: address
|
||||
? <LoadingSkeleton />
|
||||
: '0'} <span className="text-[#A0A0A0]">WETH9</span>
|
||||
: '0'} <span className="ui-muted">WETH9</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center p-4 bg-[#252830] rounded-lg border border-white/10 font-mono text-sm">
|
||||
<span className="font-medium text-[#A0A0A0] flex items-center gap-2">
|
||||
<div className="ui-stat-row">
|
||||
<span className="ui-muted font-medium flex items-center gap-2">
|
||||
<TokenIcon symbol="LINK" size={20} />
|
||||
LINK Balance:
|
||||
</span>
|
||||
<span className="text-white font-medium">
|
||||
<span className="font-medium">
|
||||
{address && linkBalance !== undefined
|
||||
? `${ethers.utils.formatEther(linkBalance.toString())}`
|
||||
: address
|
||||
? <LoadingSkeleton />
|
||||
: '0'} <span className="text-[#A0A0A0]">LINK</span>
|
||||
: '0'} <span className="ui-muted">LINK</span>
|
||||
</span>
|
||||
</div>
|
||||
{ccipFee != null && ccipFee.gt(0) && (
|
||||
<div className="flex justify-between items-center p-4 bg-[#252830] rounded-lg border border-teal-500/30 font-mono text-sm">
|
||||
<span className="font-medium text-[#A0A0A0] flex items-center gap-2">
|
||||
<div className="ui-stat-row ui-stat-row--highlight">
|
||||
<span className="ui-muted font-medium flex items-center gap-2">
|
||||
<TokenIcon symbol="LINK" size={20} />
|
||||
CCIP Fee:
|
||||
</span>
|
||||
<span className="text-white font-medium">
|
||||
{ethers.utils.formatEther(ccipFee)} <span className="text-[#A0A0A0]">LINK</span>
|
||||
<span className="font-medium">
|
||||
{ethers.utils.formatEther(ccipFee)} <span className="ui-muted">LINK</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -573,7 +558,7 @@ function BridgeButtonsConnected({
|
||||
<button
|
||||
onClick={() => setShowWrapModal(true)}
|
||||
disabled={!needsWrapping || !address || isWrapping}
|
||||
className="px-6 py-4 bg-[#252830] text-white rounded-xl font-semibold hover:bg-white/10 disabled:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors border border-white/20"
|
||||
className="ui-btn-secondary w-full"
|
||||
aria-label="Wrap ETH to WETH9"
|
||||
>
|
||||
{isWrapping ? (
|
||||
@@ -592,7 +577,7 @@ function BridgeButtonsConnected({
|
||||
<button
|
||||
onClick={() => setShowApproveModal(true)}
|
||||
disabled={!needsApproval || !address || isApproving}
|
||||
className="px-6 py-4 bg-[#252830] text-white rounded-xl font-semibold hover:bg-white/10 disabled:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors border border-white/20"
|
||||
className="ui-btn-secondary w-full"
|
||||
aria-label="Approve tokens for bridge"
|
||||
>
|
||||
{isApproving ? (
|
||||
@@ -612,7 +597,7 @@ function BridgeButtonsConnected({
|
||||
<button
|
||||
onClick={() => setShowBridgeModal(true)}
|
||||
disabled={!canBridge || !address || isBridging}
|
||||
className="w-full min-h-[56px] py-4 text-lg font-semibold bg-teal-600 text-white rounded-xl hover:bg-teal-500 disabled:bg-[#252830] disabled:opacity-50 disabled:cursor-not-allowed transition-colors focus:outline-none focus:ring-2 focus:ring-teal-500 focus:ring-offset-2 focus:ring-offset-[#252830] shadow-lg"
|
||||
className="ui-btn-primary w-full min-h-[56px] text-lg"
|
||||
aria-label="Start bridge transfer"
|
||||
>
|
||||
{isBridging ? (
|
||||
@@ -683,26 +668,11 @@ export default function BridgeButtons(props: BridgeButtonsProps) {
|
||||
const destination = CCIP_DESTINATIONS.find((d) => d.selector === destinationChainSelector);
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-[20px] font-semibold text-white flex items-center gap-2">
|
||||
Bridge to
|
||||
{destination && (
|
||||
<>
|
||||
<ChainIcon chainId={destination.chainId} name={destination.name} size={24} />
|
||||
<span>{destination.name}</span>
|
||||
</>
|
||||
)}
|
||||
{!destination && <span>Ethereum Mainnet</span>}
|
||||
</h2>
|
||||
<p className="text-[#A0A0A0] text-sm mt-1">
|
||||
Wrap ETH, approve tokens, and bridge WETH9 via CCIP
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-8 p-5 bg-amber-500/10 border border-amber-500/30 rounded-xl text-sm text-white font-medium flex items-center gap-4">
|
||||
<svg className="h-6 w-6 text-yellow-300 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<div className="ui-alert">
|
||||
<svg className="h-5 w-5 shrink-0 ui-accent-text" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<span>Please connect your wallet to continue</span>
|
||||
<span>Connect your wallet to bridge to {destination?.name ?? 'Ethereum Mainnet'}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
60
frontend-dapp/src/components/layout/OmnlProductLayout.tsx
Normal file
60
frontend-dapp/src/components/layout/OmnlProductLayout.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Link, Outlet, useLocation } from 'react-router-dom';
|
||||
import { useRef } from 'react';
|
||||
import WalletConnect from '../wallet/WalletConnect';
|
||||
import WalletDisconnectNotice from '../wallet/WalletDisconnectNotice';
|
||||
|
||||
const PRODUCTS = [
|
||||
{ path: '/central-bank', label: 'Central Bank', short: 'CB' },
|
||||
{ path: '/office-24', label: 'Office 24', short: 'O24' },
|
||||
{ path: '/trade', label: 'DBIS Trade', short: 'Trade' },
|
||||
] as const;
|
||||
|
||||
export default function OmnlProductLayout() {
|
||||
const location = useLocation();
|
||||
const userInitiatedDisconnectRef = useRef(false);
|
||||
|
||||
const isActive = (path: string) => location.pathname.startsWith(path);
|
||||
|
||||
return (
|
||||
<div className="omnl-app min-h-screen flex flex-col bg-[#0b0e11]">
|
||||
<WalletDisconnectNotice userInitiatedDisconnectRef={userInitiatedDisconnectRef} />
|
||||
<header className="omnl-app-header sticky top-0 z-50 border-b border-[#2b3139] bg-[#181a20]">
|
||||
<div className="max-w-[1600px] mx-auto px-4 h-14 flex items-center gap-4">
|
||||
<Link to="/central-bank" className="flex items-center gap-2 shrink-0">
|
||||
<span className="w-8 h-8 rounded bg-[#f0b90b] text-[#181a20] font-bold flex items-center justify-center text-sm">
|
||||
O
|
||||
</span>
|
||||
<span className="font-semibold text-[#eaecef] hidden sm:inline">OMNL</span>
|
||||
</Link>
|
||||
|
||||
<nav className="flex items-center gap-1 flex-1" aria-label="OMNL products">
|
||||
{PRODUCTS.map(({ path, label }) => (
|
||||
<Link
|
||||
key={path}
|
||||
to={path}
|
||||
className={`px-4 py-2 rounded text-sm font-medium transition-colors ${
|
||||
isActive(path)
|
||||
? 'bg-[#2b3139] text-[#f0b90b]'
|
||||
: 'text-[#848e9c] hover:text-[#eaecef] hover:bg-[#1e2329]'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Link
|
||||
to="/"
|
||||
className="text-xs text-[#848e9c] hover:text-[#f0b90b] hidden md:inline shrink-0"
|
||||
>
|
||||
Bridge →
|
||||
</Link>
|
||||
<WalletConnect onBeforeDisconnect={() => { userInitiatedDisconnectRef.current = true; }} />
|
||||
</div>
|
||||
</header>
|
||||
<main className="flex-1">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
71
frontend-dapp/src/config/dex.ts
Normal file
71
frontend-dapp/src/config/dex.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/** DBIS Exchange — Chain 138 DEX addresses (Pancake/Uniswap V2 compatible) */
|
||||
|
||||
export const DBIS_EXCHANGE_CHAIN_ID = 138;
|
||||
|
||||
|
||||
|
||||
export const TOKEN_AGGREGATION_URL =
|
||||
|
||||
import.meta.env.VITE_TOKEN_AGGREGATION_URL || 'http://localhost:3000';
|
||||
|
||||
|
||||
|
||||
export const DBIS_EXCHANGE_URL =
|
||||
|
||||
import.meta.env.VITE_DBIS_EXCHANGE_URL || 'http://localhost:3012';
|
||||
|
||||
|
||||
|
||||
export const SETTLEMENT_MIDDLEWARE_URL =
|
||||
|
||||
import.meta.env.VITE_SETTLEMENT_MIDDLEWARE_URL || 'http://localhost:3011';
|
||||
|
||||
|
||||
|
||||
export const ENHANCED_SWAP_ROUTER_V2 =
|
||||
|
||||
import.meta.env.VITE_ENHANCED_SWAP_ROUTER_V2 ||
|
||||
|
||||
'0xa421706768aeb7fafa2d912c5e10824ef3437ad4';
|
||||
|
||||
|
||||
|
||||
export const UNISWAP_V2_ROUTER_138 =
|
||||
|
||||
import.meta.env.VITE_UNISWAP_V2_ROUTER_138 ||
|
||||
|
||||
'0x3019A7fDc76ba7F64F18d78e66842760037ee638';
|
||||
|
||||
|
||||
|
||||
export type DexToken = {
|
||||
|
||||
symbol: string;
|
||||
|
||||
name: string;
|
||||
|
||||
address: string;
|
||||
|
||||
decimals: number;
|
||||
|
||||
native?: boolean;
|
||||
|
||||
omnlLine?: string;
|
||||
|
||||
moneyLayer?: string;
|
||||
|
||||
currencyCode?: string;
|
||||
|
||||
swappable?: boolean;
|
||||
|
||||
convertible?: boolean;
|
||||
|
||||
transferableInternal?: boolean;
|
||||
|
||||
transferableExternal?: boolean;
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
/** Fallback when exchange API unavailable */
|
||||
31
frontend-dapp/src/config/supported-chains.ts
Normal file
31
frontend-dapp/src/config/supported-chains.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { SETTLEMENT_MIDDLEWARE_URL } from './dex';
|
||||
|
||||
export type OmnlChainSummary = {
|
||||
chainId: number;
|
||||
name: string;
|
||||
status: string;
|
||||
tier: string;
|
||||
omnlRole: string;
|
||||
nativeSymbol: string;
|
||||
settlement?: { swapEnabled?: boolean; bridgeEnabled?: boolean; m2LoadEnabled?: boolean };
|
||||
};
|
||||
|
||||
export type ChainRegistryResponse = {
|
||||
brand: string;
|
||||
maxCapacity: number;
|
||||
primaryChainId: number;
|
||||
mirrorChainId: number;
|
||||
stats?: { total: number; active: number; staged: number; reserved: number };
|
||||
chains: OmnlChainSummary[];
|
||||
};
|
||||
|
||||
export async function fetchOmnlChainRegistry(): Promise<ChainRegistryResponse | null> {
|
||||
try {
|
||||
const base = SETTLEMENT_MIDDLEWARE_URL.replace(/\/$/, '');
|
||||
const res = await fetch(`${base}/api/v1/settlement/chains`);
|
||||
if (!res.ok) return null;
|
||||
return res.json();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
170
frontend-dapp/src/features/central-bank/CentralBankDashboard.tsx
Normal file
170
frontend-dapp/src/features/central-bank/CentralBankDashboard.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import { useCentralBank } from './useCentralBank';
|
||||
import ChainMatrixPanel from './ChainMatrixPanel';
|
||||
import { SETTLEMENT_MIDDLEWARE_URL, DBIS_EXCHANGE_URL } from '../../config/dex';
|
||||
|
||||
function StatCard({
|
||||
label,
|
||||
value,
|
||||
sub,
|
||||
accent,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
sub?: string;
|
||||
accent?: 'gold' | 'green' | 'blue';
|
||||
}) {
|
||||
const border =
|
||||
accent === 'gold' ? 'border-l-[#f0b90b]' : accent === 'green' ? 'border-l-[#0ecb81]' : 'border-l-[#3861fb]';
|
||||
return (
|
||||
<div className={`cb-card border-l-4 ${border}`}>
|
||||
<p className="text-xs text-[#848e9c] uppercase tracking-wide mb-1">{label}</p>
|
||||
<p className="text-2xl font-semibold text-[#eaecef]">{value}</p>
|
||||
{sub && <p className="text-xs text-[#848e9c] mt-1">{sub}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CentralBankDashboard() {
|
||||
const cb = useCentralBank();
|
||||
const office = cb.office as {
|
||||
officeId?: number;
|
||||
name?: string;
|
||||
externalId?: string;
|
||||
settlement?: { moneyLayers?: string[]; glM0?: string; glM1?: string; glM2?: string };
|
||||
} | null;
|
||||
|
||||
return (
|
||||
<div className="cb-page max-w-[1400px] mx-auto px-4 py-6">
|
||||
<header className="mb-8">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<span className="px-2 py-0.5 rounded text-xs font-medium bg-[#3861fb]/20 text-[#3861fb]">
|
||||
OMNL Central Bank
|
||||
</span>
|
||||
<span className="px-2 py-0.5 rounded text-xs font-medium bg-[#0ecb81]/20 text-[#0ecb81]">
|
||||
Production · 128 chains
|
||||
</span>
|
||||
<span className="text-xs text-[#848e9c]">Meta fiat · M0 / M1 / M2</span>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-[#eaecef]">Central Bank Operations</h1>
|
||||
<p className="text-[#848e9c] mt-1">
|
||||
Settlement orchestration · SWIFT · HYBX · Chain 138 token loading
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{cb.error && (
|
||||
<div className="mb-4 p-3 rounded bg-[#f6465d]/10 text-[#f6465d] text-sm">{cb.error}</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||
<StatCard
|
||||
label="M0 Reserve (GL 1050)"
|
||||
value={cb.moneySupply?.M0?.gl1050 ?? '—'}
|
||||
sub={`Backing ${cb.moneySupply?.M0?.backingRatio ?? 1.2}×`}
|
||||
accent="blue"
|
||||
/>
|
||||
<StatCard
|
||||
label="M1 Circulating (GL 2100)"
|
||||
value={cb.moneySupply?.M1?.circulating ?? '—'}
|
||||
sub="Narrow money · eMoney"
|
||||
accent="green"
|
||||
/>
|
||||
<StatCard
|
||||
label="M2 Broad (GL 2200)"
|
||||
value={cb.moneySupply?.M2?.broadMoney ?? '—'}
|
||||
sub="Token load source"
|
||||
accent="gold"
|
||||
/>
|
||||
<StatCard
|
||||
label="M2 tokens registered"
|
||||
value={String(cb.tokenCount)}
|
||||
sub="Swappable · convertible · transferable"
|
||||
accent="gold"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!cb.moneySupply && (
|
||||
<p className="text-xs text-[#848e9c] mb-6">
|
||||
Set <code className="text-[#f0b90b]">VITE_OMNL_API_KEY</code> in .env.local to load live M0/M1/M2
|
||||
balances from Fineract.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<section className="cb-card">
|
||||
<h2 className="text-lg font-semibold text-[#eaecef] mb-4">Settlement service</h2>
|
||||
<dl className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-[#848e9c]">Status</dt>
|
||||
<dd className="text-[#0ecb81] font-medium">{(cb.health as { status?: string })?.status ?? '—'}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-[#848e9c]">Role</dt>
|
||||
<dd className="text-[#eaecef] text-right max-w-[60%]">
|
||||
{(cb.health as { role?: string })?.role ?? '—'}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-[#848e9c]">API</dt>
|
||||
<dd className="text-[#f0b90b] text-xs truncate max-w-[60%]">{SETTLEMENT_MIDDLEWARE_URL}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-4 w-full py-2 rounded bg-[#2b3139] text-[#eaecef] text-sm hover:bg-[#474d57]"
|
||||
onClick={() => cb.refresh()}
|
||||
>
|
||||
{cb.loading ? 'Refreshing…' : 'Refresh'}
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section className="cb-card">
|
||||
<h2 className="text-lg font-semibold text-[#eaecef] mb-4">Money layers (Office {office?.officeId ?? 24})</h2>
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{(office?.settlement?.moneyLayers ?? ['M0', 'M1', 'M2']).map((layer) => (
|
||||
<span key={layer} className="px-3 py-1 rounded-full bg-[#2b3139] text-[#f0b90b] text-xs font-medium">
|
||||
{layer}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<dl className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-[#848e9c]">GL M0</dt>
|
||||
<dd>{office?.settlement?.glM0 ?? '1050'}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-[#848e9c]">GL M1</dt>
|
||||
<dd>{office?.settlement?.glM1 ?? '2100'}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-[#848e9c]">GL M2</dt>
|
||||
<dd>{office?.settlement?.glM2 ?? '2200'}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section className="cb-card lg:col-span-2">
|
||||
<h2 className="text-lg font-semibold text-[#eaecef] mb-4">Recent exchange activity</h2>
|
||||
<ul className="space-y-2 text-sm">
|
||||
{((cb.monitor as { swaps?: { status: string; tokenIn: string; createdAt: string }[] })?.swaps ?? [])
|
||||
.slice(0, 8)
|
||||
.map((s, i) => (
|
||||
<li key={i} className="flex justify-between py-2 border-b border-[#2b3139] last:border-0">
|
||||
<span className="text-[#848e9c]">{s.status}</span>
|
||||
<span className="text-[#eaecef] font-mono text-xs truncate max-w-[50%]">{s.tokenIn?.slice(0, 10)}…</span>
|
||||
<span className="text-[#848e9c] text-xs">{s.createdAt?.slice(0, 16)}</span>
|
||||
</li>
|
||||
))}
|
||||
{!((cb.monitor as { swaps?: unknown[] })?.swaps?.length) && (
|
||||
<li className="text-[#848e9c]">No swaps recorded yet</li>
|
||||
)}
|
||||
</ul>
|
||||
<p className="text-xs text-[#848e9c] mt-4">
|
||||
DBIS Exchange: {DBIS_EXCHANGE_URL} · Token-load & transfers via settlement middleware
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<ChainMatrixPanel />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
100
frontend-dapp/src/features/central-bank/ChainMatrixPanel.tsx
Normal file
100
frontend-dapp/src/features/central-bank/ChainMatrixPanel.tsx
Normal file
@@ -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<ChainRegistryResponse | null>(null);
|
||||
const [filter, setFilter] = useState<Filter>('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 (
|
||||
<section className="cb-card lg:col-span-2">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 mb-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-[#eaecef]">128-chain network matrix</h2>
|
||||
<p className="text-xs text-[#848e9c]">
|
||||
{registry?.stats?.active ?? 0} active · {registry?.stats?.staged ?? 0} staged ·{' '}
|
||||
{registry?.stats?.reserved ?? 0} reserved · primary {registry?.primaryChainId ?? 138}
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search chain…"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="bg-[#2b3139] border border-[#474d57] rounded px-3 py-1.5 text-sm w-48"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 mb-4 flex-wrap">
|
||||
{(['all', 'active', 'primary', 'l2'] as Filter[]).map((f) => (
|
||||
<button
|
||||
key={f}
|
||||
type="button"
|
||||
onClick={() => setFilter(f)}
|
||||
className={`px-3 py-1 rounded text-xs capitalize ${
|
||||
filter === f ? 'bg-[#f0b90b] text-[#181a20]' : 'bg-[#2b3139] text-[#848e9c]'
|
||||
}`}
|
||||
>
|
||||
{f}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto max-h-[360px] overflow-y-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="sticky top-0 bg-[#1e2329]">
|
||||
<tr className="text-[#848e9c] text-xs uppercase text-left">
|
||||
<th className="py-2 pr-3">ID</th>
|
||||
<th className="py-2 pr-3">Network</th>
|
||||
<th className="py-2 pr-3">Role</th>
|
||||
<th className="py-2 pr-3">Status</th>
|
||||
<th className="py-2 pr-3">Swap</th>
|
||||
<th className="py-2">Bridge</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{chains.map((c: OmnlChainSummary) => (
|
||||
<tr key={c.chainId} className="border-t border-[#2b3139] hover:bg-[#2b3139]/40">
|
||||
<td className="py-1.5 pr-3 font-mono text-[#f0b90b]">{c.chainId}</td>
|
||||
<td className="py-1.5 pr-3 text-[#eaecef]">{c.name}</td>
|
||||
<td className="py-1.5 pr-3 text-xs text-[#848e9c]">{c.omnlRole}</td>
|
||||
<td className="py-1.5 pr-3">
|
||||
<span
|
||||
className={`text-xs px-1.5 py-0.5 rounded ${
|
||||
c.status === 'active'
|
||||
? 'bg-[#0ecb81]/15 text-[#0ecb81]'
|
||||
: c.status === 'staged'
|
||||
? 'bg-[#f0b90b]/15 text-[#f0b90b]'
|
||||
: 'bg-[#848e9c]/15 text-[#848e9c]'
|
||||
}`}
|
||||
>
|
||||
{c.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-1.5 pr-3">{c.settlement?.swapEnabled ? '✓' : '—'}</td>
|
||||
<td className="py-1.5">{c.settlement?.bridgeEnabled ? '✓' : '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
60
frontend-dapp/src/features/central-bank/useCentralBank.ts
Normal file
60
frontend-dapp/src/features/central-bank/useCentralBank.ts
Normal file
@@ -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<Record<string, unknown> | null>(null);
|
||||
const [office, setOffice] = useState<Record<string, unknown> | null>(null);
|
||||
const [moneySupply, setMoneySupply] = useState<MoneySupply | null>(null);
|
||||
const [tokenCount, setTokenCount] = useState(0);
|
||||
const [monitor, setMonitor] = useState<Record<string, unknown> | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(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 };
|
||||
}
|
||||
123
frontend-dapp/src/features/office24/Office24Dashboard.tsx
Normal file
123
frontend-dapp/src/features/office24/Office24Dashboard.tsx
Normal file
@@ -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 (
|
||||
<div className="o24-page max-w-[1400px] mx-auto px-4 py-6">
|
||||
<header className="mb-8 flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="px-2 py-0.5 rounded text-xs font-medium bg-[#0ecb81]/20 text-[#0ecb81]">
|
||||
Office 24 · Locked
|
||||
</span>
|
||||
{profile?.locked && <span className="text-xs text-[#848e9c]">Single-office enforcement</span>}
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-[#eaecef]">{profile?.name ?? 'HOSPITALLERS Ali Iraq-Iran'}</h1>
|
||||
<p className="text-[#848e9c] mt-1 font-mono text-sm">{profile?.externalId ?? 'HOSPITALLERS-ALI-IRAQ-IRAN'}</p>
|
||||
</div>
|
||||
<Link
|
||||
to="/trade"
|
||||
className="px-5 py-2.5 rounded bg-[#f0b90b] text-[#181a20] font-semibold text-sm hover:bg-[#fcd535]"
|
||||
>
|
||||
Open DBIS Trade →
|
||||
</Link>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||
<div className="o24-card">
|
||||
<p className="text-xs text-[#848e9c] uppercase">Office ID</p>
|
||||
<p className="text-2xl font-bold text-[#0ecb81]">{profile?.officeId ?? 24}</p>
|
||||
</div>
|
||||
<div className="o24-card">
|
||||
<p className="text-xs text-[#848e9c] uppercase">Default currency</p>
|
||||
<p className="text-2xl font-bold text-[#eaecef]">{profile?.settlement?.defaultCurrency ?? 'USD'}</p>
|
||||
</div>
|
||||
<div className="o24-card">
|
||||
<p className="text-xs text-[#848e9c] uppercase">M2 tradable tokens</p>
|
||||
<p className="text-2xl font-bold text-[#f0b90b]">{tokens.length}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
||||
<section className="o24-card lg:col-span-1">
|
||||
<h2 className="font-semibold text-[#eaecef] mb-4">Settlement rails</h2>
|
||||
<ul className="space-y-3 text-sm">
|
||||
<li className="flex justify-between">
|
||||
<span className="text-[#848e9c]">SWIFT</span>
|
||||
<span className={profile?.rails?.swift?.enabled ? 'text-[#0ecb81]' : 'text-[#848e9c]'}>
|
||||
{profile?.rails?.swift?.enabled ? 'Enabled' : 'Off'}
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex justify-between">
|
||||
<span className="text-[#848e9c]">HYBX</span>
|
||||
<span className={profile?.rails?.hybx?.enabled ? 'text-[#0ecb81]' : 'text-[#848e9c]'}>
|
||||
{profile?.rails?.hybx?.enabled ? 'Enabled' : 'Off'}
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex justify-between">
|
||||
<span className="text-[#848e9c]">Chain 138</span>
|
||||
<span className={profile?.rails?.chain138?.enabled ? 'text-[#0ecb81]' : 'text-[#848e9c]'}>
|
||||
{profile?.rails?.chain138?.enabled ? 'Enabled' : 'Off'}
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex justify-between">
|
||||
<span className="text-[#848e9c]">Exchange</span>
|
||||
<span className="text-[#0ecb81]">{(health as { status?: string })?.status ?? 'ok'}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p className="text-xs text-[#848e9c] mt-4">Tenant: {profile?.tenantUser ?? 'ali_hospitallers_tenant'}</p>
|
||||
</section>
|
||||
|
||||
<section className="o24-card lg:col-span-2">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="font-semibold text-[#eaecef]">M2 token registry — swap · convert · transfer</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => refresh()}
|
||||
className="text-xs text-[#f0b90b] hover:underline"
|
||||
>
|
||||
{loading ? '…' : 'Refresh'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="overflow-x-auto max-h-[420px] overflow-y-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="sticky top-0 bg-[#1e2329]">
|
||||
<tr className="text-[#848e9c] text-left text-xs uppercase">
|
||||
<th className="py-2 pr-4">Symbol</th>
|
||||
<th className="py-2 pr-4">Line</th>
|
||||
<th className="py-2 pr-4">Swap</th>
|
||||
<th className="py-2 pr-4">Convert</th>
|
||||
<th className="py-2">Xfer</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tokens.map((t) => (
|
||||
<tr key={t.address} className="border-t border-[#2b3139] hover:bg-[#2b3139]/50">
|
||||
<td className="py-2 pr-4 font-medium text-[#eaecef]">{t.symbol}</td>
|
||||
<td className="py-2 pr-4 text-[#848e9c] text-xs">{t.omnlLine ?? 'M2'}</td>
|
||||
<td className="py-2 pr-4">{t.swappable !== false ? '✓' : '—'}</td>
|
||||
<td className="py-2 pr-4">{t.convertible !== false ? '✓' : '—'}</td>
|
||||
<td className="py-2 text-[#0ecb81]">
|
||||
{t.transferableInternal && t.transferableExternal ? 'Int+Ext' : '✓'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
42
frontend-dapp/src/features/office24/useOffice24.ts
Normal file
42
frontend-dapp/src/features/office24/useOffice24.ts
Normal file
@@ -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<Record<string, unknown> | null>(null);
|
||||
const [tokens, setTokens] = useState<M2Token[]>([]);
|
||||
const [health, setHealth] = useState<Record<string, unknown> | 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 };
|
||||
}
|
||||
298
frontend-dapp/src/features/swap/DBISSwapPanel.tsx
Normal file
298
frontend-dapp/src/features/swap/DBISSwapPanel.tsx
Normal file
@@ -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<typeof useDbisSwap>['tokens'];
|
||||
}) {
|
||||
return (
|
||||
<label className="block">
|
||||
<span className="text-xs ui-muted uppercase tracking-wide mb-1 block">{label}</span>
|
||||
<select
|
||||
className="ui-input w-full !min-h-[48px] font-semibold"
|
||||
value={value.address}
|
||||
onChange={(e) => {
|
||||
const t = tokens.find((x) => x.address === e.target.value);
|
||||
if (t) onChange(t);
|
||||
}}
|
||||
>
|
||||
{tokens.map((t) => (
|
||||
<option key={t.address} value={t.address}>
|
||||
{t.symbol} — {t.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 max-w-6xl mx-auto">
|
||||
<div className="lg:col-span-2">
|
||||
<div className="ui-card p-6 border border-[var(--ui-border)]">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-xl font-bold ui-accent-text">DBIS Exchange</h2>
|
||||
<span className="text-xs ui-muted px-2 py-1 ui-inset rounded">Office 24 · M2 · {swap.tokens.length} tokens</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<TokenSelect label="From" value={swap.tokenIn} onChange={swap.setTokenIn} tokens={swap.tokens} />
|
||||
<div className="flex justify-center">
|
||||
<button
|
||||
type="button"
|
||||
className="ui-btn-secondary !rounded-full !p-2"
|
||||
onClick={() => {
|
||||
const a = swap.tokenIn;
|
||||
swap.setTokenIn(swap.tokenOut);
|
||||
swap.setTokenOut(a);
|
||||
}}
|
||||
aria-label="Swap tokens"
|
||||
>
|
||||
↕
|
||||
</button>
|
||||
</div>
|
||||
<TokenSelect label="To" value={swap.tokenOut} onChange={swap.setTokenOut} tokens={swap.tokens} />
|
||||
|
||||
<label className="block">
|
||||
<span className="text-xs ui-muted uppercase tracking-wide mb-1 block">Amount</span>
|
||||
<input
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
placeholder="0.0"
|
||||
value={swap.amountIn}
|
||||
onChange={(e) => swap.setAmountIn(e.target.value)}
|
||||
className="ui-input w-full !min-h-[56px] text-2xl font-semibold"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
<span className="ui-muted">Slippage</span>
|
||||
{[25, 50, 100].map((bps) => (
|
||||
<button
|
||||
key={bps}
|
||||
type="button"
|
||||
className={`px-3 py-1 rounded text-xs font-medium ${
|
||||
swap.slippageBps === bps ? 'ui-btn-primary' : 'ui-btn-secondary'
|
||||
}`}
|
||||
onClick={() => swap.setSlippageBps(bps)}
|
||||
>
|
||||
{(bps / 100).toFixed(2)}%
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="ui-inset p-4 rounded-lg">
|
||||
<p className="text-sm ui-muted mb-1">Expected output</p>
|
||||
<p className="text-2xl font-bold">
|
||||
{outDisplay} <span className="text-base ui-accent-text">{swap.tokenOut.symbol}</span>
|
||||
</p>
|
||||
{swap.plan?.plannerResponse?.routePlan?.legs && (
|
||||
<p className="text-xs ui-muted mt-2">
|
||||
Route:{' '}
|
||||
{swap.plan.plannerResponse.routePlan.legs
|
||||
.map((l: { provider?: string }) => l.provider)
|
||||
.filter(Boolean)
|
||||
.join(' → ')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{swap.error && <p className="text-sm text-[var(--ui-danger)]">{swap.error}</p>}
|
||||
{swap.txHash && (
|
||||
<p className="text-xs ui-muted break-all">
|
||||
Tx:{' '}
|
||||
<a
|
||||
href={`https://explorer.d-bis.org/tx/${swap.txHash}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="ui-accent-text hover:underline"
|
||||
>
|
||||
{swap.txHash}
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
className="ui-btn-secondary flex-1 !min-h-[48px]"
|
||||
disabled={swap.loading || !swap.amountIn}
|
||||
onClick={() => swap.fetchPlan()}
|
||||
>
|
||||
{swap.loading ? '…' : 'Get quote'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ui-btn-primary flex-1 !min-h-[48px] font-bold"
|
||||
disabled={swap.loading || !swap.isConnected || !swap.amountIn}
|
||||
onClick={() => swap.executeSwap()}
|
||||
>
|
||||
{!swap.isConnected ? 'Connect wallet' : swap.loading ? 'Swapping…' : 'Swap'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="ui-card p-4">
|
||||
<h3 className="font-semibold mb-3 text-sm ui-accent-text">OMNL Office 24 ledger</h3>
|
||||
{!swap.isConnected ? (
|
||||
<p className="text-sm ui-muted">Connect wallet for crypto balances</p>
|
||||
) : swap.ledger ? (
|
||||
<div className="text-xs space-y-2">
|
||||
<p>
|
||||
<span className="ui-muted">M1 circulating:</span>{' '}
|
||||
{(swap.ledger as { fundLedger?: { M1?: { circulating?: string } } }).fundLedger?.M1?.circulating ?? '—'}
|
||||
</p>
|
||||
<p>
|
||||
<span className="ui-muted">M2 broad:</span>{' '}
|
||||
{(swap.ledger as { fundLedger?: { M2?: { broadMoney?: string } } }).fundLedger?.M2?.broadMoney ?? '—'}
|
||||
</p>
|
||||
{(swap.ledger as { cryptoLedger?: { balances?: Record<string, string> } }).cryptoLedger?.balances && (
|
||||
<div className="mt-2 pt-2 border-t border-[var(--ui-border)]">
|
||||
<p className="ui-muted mb-1">Wallet balances</p>
|
||||
{Object.entries(
|
||||
(swap.ledger as { cryptoLedger: { balances: Record<string, string> } }).cryptoLedger.balances,
|
||||
).map(([sym, bal]) => (
|
||||
<p key={sym}>
|
||||
{sym}: {Number(bal).toFixed(4)}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<button type="button" className="ui-btn-secondary text-xs w-full" onClick={() => swap.loadLedgers()}>
|
||||
Load ledgers
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="ui-card p-4">
|
||||
<h3 className="font-semibold mb-3 text-sm ui-accent-text">M2 token load</h3>
|
||||
<p className="text-xs ui-muted mb-3">Load from M2 fiat (GL 2200) → on-chain · swappable · convertible</p>
|
||||
<input
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
placeholder="Amount (fiat)"
|
||||
value={swap.loadAmount}
|
||||
onChange={(e) => swap.setLoadAmount(e.target.value)}
|
||||
className="ui-input w-full mb-2 text-sm"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="ui-btn-primary text-xs w-full !min-h-[40px]"
|
||||
disabled={swap.loading || !swap.isConnected || !swap.loadAmount}
|
||||
onClick={() => swap.loadM2Token()}
|
||||
>
|
||||
Load {swap.tokenOut.symbol} from M2
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="ui-card p-4">
|
||||
<h3 className="font-semibold mb-3 text-sm">Transfer</h3>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Recipient (0x… or address)"
|
||||
value={swap.transferTo}
|
||||
onChange={(e) => swap.setTransferTo(e.target.value)}
|
||||
className="ui-input w-full mb-2 text-sm font-mono"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
placeholder="Amount"
|
||||
value={swap.transferAmount}
|
||||
onChange={(e) => swap.setTransferAmount(e.target.value)}
|
||||
className="ui-input w-full mb-2 text-sm"
|
||||
/>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<button
|
||||
type="button"
|
||||
className="ui-btn-secondary text-xs flex-1 !min-h-[36px]"
|
||||
disabled={swap.loading || !swap.isConnected}
|
||||
onClick={() => swap.internalTransfer()}
|
||||
>
|
||||
Internal
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="External IBAN (SWIFT)"
|
||||
value={swap.externalIban}
|
||||
onChange={(e) => swap.setExternalIban(e.target.value)}
|
||||
className="ui-input w-full mb-2 text-sm font-mono"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="ui-btn-secondary text-xs w-full !min-h-[36px]"
|
||||
disabled={swap.loading || !swap.isConnected || !swap.externalIban}
|
||||
onClick={() => swap.externalTransfer()}
|
||||
>
|
||||
External (SWIFT)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{swap.lastSettlement && (
|
||||
<div className="ui-card p-4">
|
||||
<h3 className="font-semibold mb-2 text-sm">Last settlement</h3>
|
||||
<p className="text-xs ui-muted break-all">
|
||||
{(swap.lastSettlement as { phase?: string }).phase ?? 'OK'} ·{' '}
|
||||
{(swap.lastSettlement as { settlementId?: string }).settlementId ?? '—'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="ui-card p-4">
|
||||
<h3 className="font-semibold mb-3 text-sm">Swap monitor</h3>
|
||||
{swap.monitor ? (
|
||||
<div className="text-xs space-y-2">
|
||||
<p>
|
||||
Total: {(swap.monitor as { stats?: { total?: number } }).stats?.total ?? 0} · 24h:{' '}
|
||||
{(swap.monitor as { stats?: { last24h?: number } }).stats?.last24h ?? 0}
|
||||
</p>
|
||||
<ul className="max-h-40 overflow-y-auto space-y-1">
|
||||
{((swap.monitor as { swaps?: { tokenIn: string; status: string; createdAt: string }[] }).swaps ?? [])
|
||||
.slice(0, 5)
|
||||
.map((s, i) => (
|
||||
<li key={i} className="ui-muted truncate">
|
||||
{s.status} · {s.createdAt.slice(0, 16)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs ui-muted">Loading…</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
297
frontend-dapp/src/features/swap/useDbisSwap.ts
Normal file
297
frontend-dapp/src/features/swap/useDbisSwap.ts
Normal file
@@ -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<DexToken[]>(DEFAULT_TOKENS);
|
||||
const [tokenIn, setTokenIn] = useState<DexToken>(DEFAULT_TOKENS[4]);
|
||||
const [tokenOut, setTokenOut] = useState<DexToken>(DEFAULT_TOKENS[5]);
|
||||
const [amountIn, setAmountIn] = useState('');
|
||||
const [slippageBps, setSlippageBps] = useState(50);
|
||||
const [plan, setPlan] = useState<SwapPlan | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [txHash, setTxHash] = useState<string | undefined>();
|
||||
const [ledger, setLedger] = useState<Record<string, unknown> | null>(null);
|
||||
const [monitor, setMonitor] = useState<Record<string, unknown> | null>(null);
|
||||
const [transferTo, setTransferTo] = useState('');
|
||||
const [transferAmount, setTransferAmount] = useState('');
|
||||
const [externalIban, setExternalIban] = useState('');
|
||||
const [loadAmount, setLoadAmount] = useState('');
|
||||
const [lastSettlement, setLastSettlement] = useState<Record<string, unknown> | 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,
|
||||
};
|
||||
}
|
||||
325
frontend-dapp/src/features/trade/BinanceTradePanel.tsx
Normal file
325
frontend-dapp/src/features/trade/BinanceTradePanel.tsx
Normal file
@@ -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 (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`trade-market-row w-full text-left px-3 py-2 flex justify-between items-center text-sm ${
|
||||
active ? 'trade-market-row--active' : ''
|
||||
}`}
|
||||
>
|
||||
<span className="font-medium text-[#eaecef]">
|
||||
{token.symbol}
|
||||
<span className="text-[#848e9c]">/{quote}</span>
|
||||
</span>
|
||||
<span className="text-[#0ecb81] text-xs">M2</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="trade-orderbook text-xs font-mono">
|
||||
<div className="flex justify-between text-[#848e9c] px-2 py-1 border-b border-[#2b3139]">
|
||||
<span>Price</span>
|
||||
<span>Amount</span>
|
||||
</div>
|
||||
{rows.slice(0, 4).map((r) => (
|
||||
<div key={`a-${r.price}`} className="flex justify-between px-2 py-0.5 text-[#f6465d]">
|
||||
<span>{r.price}</span>
|
||||
<span>{r.amount}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="text-center py-2 text-lg font-semibold text-[#0ecb81] border-y border-[#2b3139] my-1">
|
||||
{price || '—'}
|
||||
</div>
|
||||
{rows.slice(4).map((r) => (
|
||||
<div key={`b-${r.price}`} className="flex justify-between px-2 py-0.5 text-[#0ecb81]">
|
||||
<span>{r.price}</span>
|
||||
<span>{r.amount}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function BinanceTradePanel() {
|
||||
const swap = useDbisSwap();
|
||||
const [side, setSide] = useState<Side>('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 (
|
||||
<div className="trade-layout">
|
||||
{/* Markets sidebar */}
|
||||
<aside className="trade-markets hidden lg:flex flex-col border-r border-[#2b3139] bg-[#181a20]">
|
||||
<div className="p-3 border-b border-[#2b3139]">
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search markets"
|
||||
value={marketSearch}
|
||||
onChange={(e) => setMarketSearch(e.target.value)}
|
||||
className="w-full bg-[#2b3139] border border-[#474d57] rounded px-3 py-2 text-sm text-[#eaecef] placeholder:text-[#848e9c]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<p className="px-3 py-2 text-xs text-[#848e9c] uppercase">M2 · {markets.length} pairs</p>
|
||||
{markets.map((t) => (
|
||||
<MarketRow
|
||||
key={t.address}
|
||||
token={t}
|
||||
quote={quoteToken?.symbol ?? 'USDT'}
|
||||
active={swap.tokenOut.symbol === t.symbol || swap.tokenIn.symbol === t.symbol}
|
||||
onClick={() => selectMarket(t)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Chart + order book */}
|
||||
<section className="trade-center flex flex-col min-w-0">
|
||||
<div className="trade-pair-header flex flex-wrap items-center gap-4 px-4 py-3 border-b border-[#2b3139] bg-[#181a20]">
|
||||
<div className="flex items-center gap-2">
|
||||
<TokenIcon symbol={swap.tokenOut.symbol} size={28} />
|
||||
<h1 className="text-xl font-bold text-[#eaecef]">
|
||||
{swap.tokenOut.symbol}/{quoteToken?.symbol ?? 'USDT'}
|
||||
</h1>
|
||||
</div>
|
||||
<span className="text-2xl font-semibold text-[#0ecb81]">{displayPrice}</span>
|
||||
<span className="text-xs px-2 py-1 rounded bg-[#0ecb81]/10 text-[#0ecb81]">Office 24 · Chain 138</span>
|
||||
<span className="text-xs text-[#848e9c]">37 M2 tokens · tradable & swappable</span>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 grid grid-cols-1 xl:grid-cols-[1fr_280px] min-h-[320px]">
|
||||
<div className="trade-chart bg-[#0b0e11] border-b xl:border-b-0 xl:border-r border-[#2b3139] p-4">
|
||||
<div className="h-full min-h-[240px] rounded border border-[#2b3139] bg-[#181a20] flex items-end justify-around px-2 pb-2 gap-1">
|
||||
{Array.from({ length: 24 }, (_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex-1 max-w-[12px] rounded-t opacity-80"
|
||||
style={{
|
||||
height: `${30 + Math.sin(i * 0.5) * 20 + Math.random() * 25}%`,
|
||||
background: i % 3 === 0 ? '#f6465d' : '#0ecb81',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-[#848e9c] mt-2 text-center">Live chart · connect WebSocket feed for production</p>
|
||||
</div>
|
||||
<div className="trade-book hidden xl:block bg-[#181a20] p-2">
|
||||
<p className="text-xs text-[#848e9c] uppercase px-2 py-1">Order book</p>
|
||||
<OrderBookMock price={displayPrice} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="trade-recent border-t border-[#2b3139] bg-[#181a20] p-3 max-h-[140px] overflow-y-auto">
|
||||
<p className="text-xs text-[#848e9c] uppercase mb-2">Recent trades</p>
|
||||
<div className="grid grid-cols-3 gap-2 text-xs font-mono text-[#848e9c]">
|
||||
{((swap.monitor as { swaps?: { status: string; amountIn: string; createdAt: string }[] })?.swaps ?? [])
|
||||
.slice(0, 6)
|
||||
.map((s, i) => (
|
||||
<span key={i} className="col-span-3 flex justify-between">
|
||||
<span className={s.status === 'FAILED' ? 'text-[#f6465d]' : 'text-[#0ecb81]'}>{s.status}</span>
|
||||
<span>{s.amountIn?.slice(0, 12)}</span>
|
||||
<span>{s.createdAt?.slice(11, 19)}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Trade panel */}
|
||||
<aside className="trade-panel w-full lg:w-[320px] shrink-0 border-l border-[#2b3139] bg-[#181a20] flex flex-col">
|
||||
<div className="flex border-b border-[#2b3139]">
|
||||
<button
|
||||
type="button"
|
||||
className={`flex-1 py-3 text-sm font-semibold ${side === 'buy' ? 'text-[#0ecb81] border-b-2 border-[#0ecb81]' : 'text-[#848e9c]'}`}
|
||||
onClick={() => {
|
||||
setSide('buy');
|
||||
if (quoteToken) swap.setTokenIn(quoteToken);
|
||||
}}
|
||||
>
|
||||
Buy
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`flex-1 py-3 text-sm font-semibold ${side === 'sell' ? 'text-[#f6465d] border-b-2 border-[#f6465d]' : 'text-[#848e9c]'}`}
|
||||
onClick={() => {
|
||||
setSide('sell');
|
||||
if (quoteToken) swap.setTokenOut(quoteToken);
|
||||
}}
|
||||
>
|
||||
Sell
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-4 flex-1 flex flex-col gap-4">
|
||||
<div className="flex gap-2 text-xs">
|
||||
{(['market', 'limit'] as const).map((t) => (
|
||||
<button
|
||||
key={t}
|
||||
type="button"
|
||||
onClick={() => setOrderType(t)}
|
||||
className={`px-3 py-1 rounded capitalize ${orderType === t ? 'bg-[#2b3139] text-[#f0b90b]' : 'text-[#848e9c]'}`}
|
||||
>
|
||||
{t}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<label className="block">
|
||||
<span className="text-xs text-[#848e9c]">Pay with</span>
|
||||
<select
|
||||
className="w-full mt-1 bg-[#2b3139] border border-[#474d57] rounded px-3 py-2 text-sm"
|
||||
value={swap.tokenIn.address}
|
||||
onChange={(e) => {
|
||||
const t = swap.tokens.find((x) => x.address === e.target.value);
|
||||
if (t) swap.setTokenIn(t);
|
||||
}}
|
||||
>
|
||||
{swap.tokens.map((t) => (
|
||||
<option key={t.address} value={t.address}>
|
||||
{t.symbol}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<span className="text-xs text-[#848e9c]">Receive</span>
|
||||
<select
|
||||
className="w-full mt-1 bg-[#2b3139] border border-[#474d57] rounded px-3 py-2 text-sm"
|
||||
value={swap.tokenOut.address}
|
||||
onChange={(e) => {
|
||||
const t = swap.tokens.find((x) => x.address === e.target.value);
|
||||
if (t) swap.setTokenOut(t);
|
||||
}}
|
||||
>
|
||||
{swap.tokens.map((t) => (
|
||||
<option key={t.address} value={t.address}>
|
||||
{t.symbol}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<span className="text-xs text-[#848e9c]">Amount</span>
|
||||
<input
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
value={swap.amountIn}
|
||||
onChange={(e) => swap.setAmountIn(e.target.value)}
|
||||
className="w-full mt-1 bg-[#2b3139] border border-[#474d57] rounded px-3 py-3 text-lg font-semibold"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="text-sm text-[#848e9c]">
|
||||
Est. receive: <span className="text-[#eaecef] font-medium">{outDisplay} {swap.tokenOut.symbol}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1">
|
||||
{[25, 50, 100].map((bps) => (
|
||||
<button
|
||||
key={bps}
|
||||
type="button"
|
||||
onClick={() => swap.setSlippageBps(bps)}
|
||||
className={`flex-1 py-1 text-xs rounded ${swap.slippageBps === bps ? 'bg-[#f0b90b] text-[#181a20]' : 'bg-[#2b3139] text-[#848e9c]'}`}
|
||||
>
|
||||
{(bps / 100).toFixed(1)}%
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{swap.error && <p className="text-xs text-[#f6465d]">{swap.error}</p>}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={swap.loading || !swap.isConnected || !swap.amountIn}
|
||||
onClick={() => swap.executeSwap()}
|
||||
className={`w-full py-3 rounded font-bold text-[#181a20] ${
|
||||
side === 'buy' ? 'bg-[#0ecb81] hover:opacity-90' : 'bg-[#f6465d] hover:opacity-90'
|
||||
} disabled:opacity-50`}
|
||||
>
|
||||
{!swap.isConnected ? 'Connect wallet' : swap.loading ? 'Processing…' : side === 'buy' ? 'Buy' : 'Sell'}
|
||||
</button>
|
||||
|
||||
{swap.txHash && (
|
||||
<a
|
||||
href={`https://explorer.d-bis.org/tx/${swap.txHash}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-xs text-[#f0b90b] break-all hover:underline"
|
||||
>
|
||||
Tx: {swap.txHash.slice(0, 18)}…
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
5
frontend-dapp/src/pages/CentralBankPage.tsx
Normal file
5
frontend-dapp/src/pages/CentralBankPage.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import CentralBankDashboard from '../features/central-bank/CentralBankDashboard';
|
||||
|
||||
export default function CentralBankPage() {
|
||||
return <CentralBankDashboard />;
|
||||
}
|
||||
5
frontend-dapp/src/pages/Office24Page.tsx
Normal file
5
frontend-dapp/src/pages/Office24Page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import Office24Dashboard from '../features/office24/Office24Dashboard';
|
||||
|
||||
export default function Office24Page() {
|
||||
return <Office24Dashboard />;
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
import PageShell from '../components/ui/PageShell'
|
||||
import DBISSwapPanel from '../features/swap/DBISSwapPanel'
|
||||
|
||||
export default function SwapPage() {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<h1 className="text-3xl font-bold mb-6">Swap</h1>
|
||||
<p className="text-gray-600">DEX swap interface coming soon...</p>
|
||||
</div>
|
||||
<PageShell
|
||||
title="DBIS Exchange"
|
||||
subtitle="Classic swap UI · use DBIS Trade for Binance-style terminal"
|
||||
>
|
||||
<DBISSwapPanel />
|
||||
</PageShell>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
5
frontend-dapp/src/pages/TradePage.tsx
Normal file
5
frontend-dapp/src/pages/TradePage.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import BinanceTradePanel from '../features/trade/BinanceTradePanel';
|
||||
|
||||
export default function TradePage() {
|
||||
return <BinanceTradePanel />;
|
||||
}
|
||||
58
frontend-dapp/src/styles/omnl-apps.css
Normal file
58
frontend-dapp/src/styles/omnl-apps.css
Normal file
@@ -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);
|
||||
}
|
||||
39
packages/settlement-core/dist/chain-registry.d.ts
vendored
Normal file
39
packages/settlement-core/dist/chain-registry.d.ts
vendored
Normal file
@@ -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[];
|
||||
43
packages/settlement-core/dist/chain-registry.js
vendored
Normal file
43
packages/settlement-core/dist/chain-registry.js
vendored
Normal file
@@ -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);
|
||||
}
|
||||
7
packages/settlement-core/dist/index.d.ts
vendored
Normal file
7
packages/settlement-core/dist/index.d.ts
vendored
Normal file
@@ -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';
|
||||
23
packages/settlement-core/dist/index.js
vendored
Normal file
23
packages/settlement-core/dist/index.js
vendored
Normal file
@@ -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);
|
||||
13
packages/settlement-core/dist/instruments.d.ts
vendored
Normal file
13
packages/settlement-core/dist/instruments.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { SwiftMessageKind, TradeFinanceInstrument } from './types';
|
||||
/** SWIFT / trade-finance instrument routing */
|
||||
export declare const INSTRUMENT_ROUTING: Record<TradeFinanceInstrument, {
|
||||
swift: SwiftMessageKind[];
|
||||
glDebit: string;
|
||||
glCredit: string;
|
||||
}>;
|
||||
export declare function resolveInstrumentGl(instrument: TradeFinanceInstrument): {
|
||||
debitGl: string;
|
||||
creditGl: string;
|
||||
swiftKinds: SwiftMessageKind[];
|
||||
};
|
||||
export declare function isIbanValid(iban: string): boolean;
|
||||
28
packages/settlement-core/dist/instruments.js
vendored
Normal file
28
packages/settlement-core/dist/instruments.js
vendored
Normal file
@@ -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;
|
||||
}
|
||||
44
packages/settlement-core/dist/m2-token-registry.d.ts
vendored
Normal file
44
packages/settlement-core/dist/m2-token-registry.d.ts
vendored
Normal file
@@ -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>): 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;
|
||||
86
packages/settlement-core/dist/m2-token-registry.js
vendored
Normal file
86
packages/settlement-core/dist/m2-token-registry.js
vendored
Normal file
@@ -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);
|
||||
}
|
||||
26
packages/settlement-core/dist/money-supply.d.ts
vendored
Normal file
26
packages/settlement-core/dist/money-supply.d.ts
vendored
Normal file
@@ -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<Record<MoneyLayer, string>>): {
|
||||
metaFiatTotal: string;
|
||||
breakdown: Record<MoneyLayer, string>;
|
||||
};
|
||||
export declare function buildMoneySupplySnapshot(officeId: number, balances: Partial<Record<string, string>>): MoneySupplySnapshot;
|
||||
export declare function m2FiatToTokenLoadAmount(fiatAmount: string, m2Available: string): {
|
||||
loadAmount: string;
|
||||
fullyBacked: boolean;
|
||||
};
|
||||
74
packages/settlement-core/dist/money-supply.js
vendored
Normal file
74
packages/settlement-core/dist/money-supply.js
vendored
Normal file
@@ -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,
|
||||
};
|
||||
}
|
||||
3
packages/settlement-core/dist/state-machine.d.ts
vendored
Normal file
3
packages/settlement-core/dist/state-machine.d.ts
vendored
Normal file
@@ -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;
|
||||
29
packages/settlement-core/dist/state-machine.js
vendored
Normal file
29
packages/settlement-core/dist/state-machine.js
vendored
Normal file
@@ -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';
|
||||
}
|
||||
102
packages/settlement-core/dist/types.d.ts
vendored
Normal file
102
packages/settlement-core/dist/types.d.ts
vendored
Normal file
@@ -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;
|
||||
};
|
||||
};
|
||||
3
packages/settlement-core/dist/types.js
vendored
Normal file
3
packages/settlement-core/dist/types.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
"use strict";
|
||||
/** OMNL central-bank settlement domain types */
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
6
packages/settlement-core/dist/verbiage.d.ts
vendored
Normal file
6
packages/settlement-core/dist/verbiage.d.ts
vendored
Normal file
@@ -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;
|
||||
54
packages/settlement-core/dist/verbiage.js
vendored
Normal file
54
packages/settlement-core/dist/verbiage.js
vendored
Normal file
@@ -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';
|
||||
}
|
||||
47
packages/settlement-core/package-lock.json
generated
Normal file
47
packages/settlement-core/package-lock.json
generated
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
17
packages/settlement-core/package.json
Normal file
17
packages/settlement-core/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
67
packages/settlement-core/src/chain-registry.ts
Normal file
67
packages/settlement-core/src/chain-registry.ts
Normal file
@@ -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);
|
||||
}
|
||||
7
packages/settlement-core/src/index.ts
Normal file
7
packages/settlement-core/src/index.ts
Normal file
@@ -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';
|
||||
31
packages/settlement-core/src/instruments.ts
Normal file
31
packages/settlement-core/src/instruments.ts
Normal file
@@ -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;
|
||||
}
|
||||
135
packages/settlement-core/src/m2-token-registry.ts
Normal file
135
packages/settlement-core/src/m2-token-registry.ts
Normal file
@@ -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<string, string> = {
|
||||
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> = {},
|
||||
): 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<string, M2TokenCapabilities>();
|
||||
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);
|
||||
}
|
||||
82
packages/settlement-core/src/money-supply.ts
Normal file
82
packages/settlement-core/src/money-supply.ts
Normal file
@@ -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<Record<MoneyLayer, string>>,
|
||||
): { metaFiatTotal: string; breakdown: Record<MoneyLayer, string> } {
|
||||
const breakdown: Record<MoneyLayer, string> = {
|
||||
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<Record<string, string>>,
|
||||
): 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,
|
||||
};
|
||||
}
|
||||
26
packages/settlement-core/src/state-machine.ts
Normal file
26
packages/settlement-core/src/state-machine.ts
Normal file
@@ -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';
|
||||
}
|
||||
113
packages/settlement-core/src/types.ts
Normal file
113
packages/settlement-core/src/types.ts
Normal file
@@ -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 };
|
||||
};
|
||||
58
packages/settlement-core/src/verbiage.ts
Normal file
58
packages/settlement-core/src/verbiage.ts
Normal file
@@ -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';
|
||||
}
|
||||
13
packages/settlement-core/tsconfig.json
Normal file
13
packages/settlement-core/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
100
scripts/deployment/deploy-omnl-bank-production.sh
Normal file
100
scripts/deployment/deploy-omnl-bank-production.sh
Normal file
@@ -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"
|
||||
142
scripts/generate-omnl-128-chains.mjs
Normal file
142
scripts/generate-omnl-128-chains.mjs
Normal file
@@ -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));
|
||||
19
services/dbis-exchange/Dockerfile
Normal file
19
services/dbis-exchange/Dockerfile
Normal file
@@ -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"]
|
||||
111
services/dbis-exchange/dist/adapters/settlement.js
vendored
Normal file
111
services/dbis-exchange/dist/adapters/settlement.js
vendored
Normal file
@@ -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;
|
||||
}
|
||||
52
services/dbis-exchange/dist/adapters/token-aggregation.js
vendored
Normal file
52
services/dbis-exchange/dist/adapters/token-aggregation.js
vendored
Normal file
@@ -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;
|
||||
}
|
||||
299
services/dbis-exchange/dist/api/routes/exchange.js
vendored
Normal file
299
services/dbis-exchange/dist/api/routes/exchange.js
vendored
Normal file
@@ -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;
|
||||
}
|
||||
45
services/dbis-exchange/dist/config.js
vendored
Normal file
45
services/dbis-exchange/dist/config.js
vendored
Normal file
@@ -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(/\/$/, '');
|
||||
}
|
||||
47
services/dbis-exchange/dist/index.js
vendored
Normal file
47
services/dbis-exchange/dist/index.js
vendored
Normal file
@@ -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)();
|
||||
19
services/dbis-exchange/dist/server.js
vendored
Normal file
19
services/dbis-exchange/dist/server.js
vendored
Normal file
@@ -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`);
|
||||
});
|
||||
}
|
||||
52
services/dbis-exchange/dist/store/swap-monitor.js
vendored
Normal file
52
services/dbis-exchange/dist/store/swap-monitor.js
vendored
Normal file
@@ -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,
|
||||
};
|
||||
}
|
||||
1527
services/dbis-exchange/package-lock.json
generated
Normal file
1527
services/dbis-exchange/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
services/dbis-exchange/package.json
Normal file
28
services/dbis-exchange/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
159
services/dbis-exchange/src/adapters/settlement.ts
Normal file
159
services/dbis-exchange/src/adapters/settlement.ts
Normal file
@@ -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<string, string> = {};
|
||||
|
||||
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<string, string> = { '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<string, string> = { '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 },
|
||||
|
||||
);
|
||||
76
services/dbis-exchange/src/adapters/token-aggregation.ts
Normal file
76
services/dbis-exchange/src/adapters/token-aggregation.ts
Normal file
@@ -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;
|
||||
}
|
||||
322
services/dbis-exchange/src/api/routes/exchange.ts
Normal file
322
services/dbis-exchange/src/api/routes/exchange.ts
Normal file
@@ -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<string, unknown>;
|
||||
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<string, unknown>;
|
||||
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<string, unknown>;
|
||||
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<string, unknown>);
|
||||
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<string, unknown>;
|
||||
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<string, unknown>;
|
||||
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<string, unknown>;
|
||||
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<string, unknown>;
|
||||
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 };
|
||||
75
services/dbis-exchange/src/config.ts
Normal file
75
services/dbis-exchange/src/config.ts
Normal file
@@ -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<string, string>;
|
||||
|
||||
factory: Record<string, string>;
|
||||
|
||||
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;
|
||||
10
services/dbis-exchange/src/index.ts
Normal file
10
services/dbis-exchange/src/index.ts
Normal file
@@ -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();
|
||||
14
services/dbis-exchange/src/server.ts
Normal file
14
services/dbis-exchange/src/server.ts
Normal file
@@ -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`);
|
||||
});
|
||||
}
|
||||
65
services/dbis-exchange/src/store/swap-monitor.ts
Normal file
65
services/dbis-exchange/src/store/swap-monitor.ts
Normal file
@@ -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, 'id' | 'createdAt' | 'updatedAt'>): 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>): 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,
|
||||
};
|
||||
}
|
||||
13
services/dbis-exchange/tsconfig.json
Normal file
13
services/dbis-exchange/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
19
services/settlement-middleware/Dockerfile
Normal file
19
services/settlement-middleware/Dockerfile
Normal file
@@ -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"]
|
||||
52
services/settlement-middleware/dist/adapters/fineract.js
vendored
Normal file
52
services/settlement-middleware/dist/adapters/fineract.js
vendored
Normal file
@@ -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 {};
|
||||
}
|
||||
}
|
||||
51
services/settlement-middleware/dist/adapters/hybx-production.js
vendored
Normal file
51
services/settlement-middleware/dist/adapters/hybx-production.js
vendored
Normal file
@@ -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;
|
||||
44
services/settlement-middleware/dist/adapters/omnl.js
vendored
Normal file
44
services/settlement-middleware/dist/adapters/omnl.js
vendored
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
42
services/settlement-middleware/dist/adapters/swift.js
vendored
Normal file
42
services/settlement-middleware/dist/adapters/swift.js
vendored
Normal file
@@ -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}
|
||||
-}`;
|
||||
}
|
||||
276
services/settlement-middleware/dist/api/routes/settlement.js
vendored
Normal file
276
services/settlement-middleware/dist/api/routes/settlement.js
vendored
Normal file
@@ -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;
|
||||
}
|
||||
57
services/settlement-middleware/dist/config.js
vendored
Normal file
57
services/settlement-middleware/dist/config.js
vendored
Normal file
@@ -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()}`;
|
||||
}
|
||||
47
services/settlement-middleware/dist/index.js
vendored
Normal file
47
services/settlement-middleware/dist/index.js
vendored
Normal file
@@ -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)();
|
||||
26
services/settlement-middleware/dist/server.js
vendored
Normal file
26
services/settlement-middleware/dist/server.js
vendored
Normal file
@@ -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`);
|
||||
});
|
||||
}
|
||||
47
services/settlement-middleware/dist/store/settlement-store.js
vendored
Normal file
47
services/settlement-middleware/dist/store/settlement-store.js
vendored
Normal file
@@ -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();
|
||||
150
services/settlement-middleware/dist/workflows/external-transfer.js
vendored
Normal file
150
services/settlement-middleware/dist/workflows/external-transfer.js
vendored
Normal file
@@ -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);
|
||||
}
|
||||
32
services/settlement-middleware/dist/workflows/office-scope.js
vendored
Normal file
32
services/settlement-middleware/dist/workflows/office-scope.js
vendored
Normal file
@@ -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,
|
||||
};
|
||||
}
|
||||
46
services/settlement-middleware/dist/workflows/swift-inbound.js
vendored
Normal file
46
services/settlement-middleware/dist/workflows/swift-inbound.js
vendored
Normal file
@@ -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);
|
||||
}
|
||||
105
services/settlement-middleware/dist/workflows/token-load.js
vendored
Normal file
105
services/settlement-middleware/dist/workflows/token-load.js
vendored
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
130
services/settlement-middleware/dist/workflows/transfer.js
vendored
Normal file
130
services/settlement-middleware/dist/workflows/transfer.js
vendored
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
1433
services/settlement-middleware/package-lock.json
generated
Normal file
1433
services/settlement-middleware/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
services/settlement-middleware/package.json
Normal file
29
services/settlement-middleware/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
53
services/settlement-middleware/src/adapters/fineract.ts
Normal file
53
services/settlement-middleware/src/adapters/fineract.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import axios from 'axios';
|
||||
|
||||
function authHeaders(): Record<string, string> {
|
||||
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<Record<string, string>> {
|
||||
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<string, string> = {};
|
||||
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 {};
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
};
|
||||
}
|
||||
}
|
||||
59
services/settlement-middleware/src/adapters/omnl.ts
Normal file
59
services/settlement-middleware/src/adapters/omnl.ts
Normal file
@@ -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<string, string> = { '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<string, string> = { '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;
|
||||
}
|
||||
}
|
||||
56
services/settlement-middleware/src/adapters/swift.ts
Normal file
56
services/settlement-middleware/src/adapters/swift.ts
Normal file
@@ -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<string, string> = { '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}
|
||||
-}`;
|
||||
}
|
||||
271
services/settlement-middleware/src/api/routes/settlement.ts
Normal file
271
services/settlement-middleware/src/api/routes/settlement.ts
Normal file
@@ -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;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user