diff --git a/config/deployment-omnl.production.env.example b/config/deployment-omnl.production.env.example index 72822e6..31058cb 100644 --- a/config/deployment-omnl.production.env.example +++ b/config/deployment-omnl.production.env.example @@ -50,6 +50,18 @@ CHAIN_138_RPC_URL=http://192.168.11.221:8545 # === Mirror chain 651940 === CHAIN_651940_RPC_URL=https://mainnet-rpc.alltra.global +# === Z Chain (consumer wallet, chainId 900002) === +CHAIN_900002_RPC_URL=http://127.0.0.1:8546 +OMNL_COMPLIANCE_CORE_900002= +VITE_RPC_URL_900002=http://127.0.0.1:8546 +VITE_Z_CHAIN_EXPLORER_URL=https://explorer.z.omdnl.org + +# === Z Bot (Azure OpenAI — optional; rule-based fallback when unset) === +AZURE_OPENAI_ENDPOINT= +AZURE_OPENAI_API_KEY= +AZURE_OPENAI_DEPLOYMENT=gpt-4o-mini +AZURE_OPENAI_API_VERSION=2024-02-15-preview + # === Major L1/L2 RPC (set per chain — CHAIN__RPC_URL) === CHAIN_1_RPC_URL=https://eth.llamarpc.com CHAIN_56_RPC_URL=https://bsc-dataseed.binance.org diff --git a/config/ecosystem-links.v1.json b/config/ecosystem-links.v1.json index 63699bf..08fe960 100644 --- a/config/ecosystem-links.v1.json +++ b/config/ecosystem-links.v1.json @@ -28,6 +28,9 @@ "reserve": "/reserve", "history": "/history", "wallets": "/wallets", + "zEcosystem": "/z", + "zWallet": "/z-wallet", + "zBot": "/z-wallet#z-bot", "admin": "/admin", "docs": "/docs" }, @@ -50,6 +53,9 @@ { "id": "reserve", "label": "Reserve", "path": "/reserve", "group": "app" }, { "id": "history", "label": "History", "path": "/history", "group": "app" }, { "id": "wallets", "label": "Wallets", "path": "/wallets", "group": "app" }, + { "id": "zEcosystem", "label": "Z Ecosystem", "path": "/z", "group": "app" }, + { "id": "zWallet", "label": "International Z Wallet", "path": "/z-wallet", "group": "app" }, + { "id": "zBot", "label": "Z Bot", "path": "/z-wallet#z-bot", "group": "app" }, { "id": "admin", "label": "Admin", "path": "/admin", "group": "app" }, { "id": "docs", "label": "Developers", "path": "/docs", "group": "app" }, { "id": "omnlCompliance", "label": "OMNL Compliance", "path": "/omnl/compliance", "group": "console" }, diff --git a/config/genesis-z-chain-900002.json b/config/genesis-z-chain-900002.json new file mode 100644 index 0000000..57481b5 --- /dev/null +++ b/config/genesis-z-chain-900002.json @@ -0,0 +1,37 @@ +{ + "config": { + "chainId": 900002, + "homesteadBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "berlinBlock": 0, + "londonBlock": 0, + "arrowGlacierBlock": 0, + "grayGlacierBlock": 0, + "mergeNetSplitBlock": 0, + "shanghaiTime": 0, + "cancunTime": 0, + "ibft2": { + "blockperiodseconds": 2, + "epochlength": 30000, + "requesttimeoutseconds": 10 + } + }, + "nonce": "0x0", + "timestamp": "0x0", + "gasLimit": "0x1c9c380", + "difficulty": "0x1", + "mixHash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365", + "coinbase": "0x0000000000000000000000000000000000000000", + "alloc": { + "0xfe3b557e8fb62b89f4916b721be55ceb828dbd73": { + "balance": "0x33b2e3c9fd0803ce8000000", + "comment": "Besu dev key #1 — funded for local deploy" + } + } +} diff --git a/config/hybx-omnl-cross-chain-lines.json b/config/hybx-omnl-cross-chain-lines.json index d1d1098..12967aa 100644 --- a/config/hybx-omnl-cross-chain-lines.json +++ b/config/hybx-omnl-cross-chain-lines.json @@ -1,24 +1,24 @@ { "version": "1.0.0", "updated": "2026-06-20", - "description": "OMNL office settlement lines \u2014 treasury EVM wallets + canonical M1 token anchors per chain.", + "description": "OMNL office settlement lines — treasury EVM wallets + canonical M1 token anchors per chain.", "lines": [ { "lineId": "0xa16da49f1c038e9d798245a68f33c539b595df5ed39f4c566345c381ac5fd8ad", "officeId": 1, "externalId": "1", - "entityName": "OMNL Head Office (DBIS) \u2013 Central Bank", + "entityName": "OMNL Head Office (DBIS) – Central Bank", "walletId": "HYBX-OFFICE-001", "treasuryWalletMainnet": "0x9baa0efd06a496f401b2fb8508192fec675b6986", "treasuryWalletChain138": "0xe41a19d5e62d83c086efe71ccecae397d32d2336", "chains": { - "138": { - "tokenM0": "0x0000000000000000000000000000000000000000", - "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" - }, "1": { "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "138": { + "tokenM0": "0x0000000000000000000000000000000000000000", + "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" } }, "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", @@ -33,13 +33,13 @@ "treasuryWalletMainnet": "0x8a72f33cb9f1110dfa2b0a950b887b814614e7ce", "treasuryWalletChain138": "0xcda90617d6b86698ef54ff1e6bdccbe5df8a54f1", "chains": { - "138": { - "tokenM0": "0x0000000000000000000000000000000000000000", - "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" - }, "1": { "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "138": { + "tokenM0": "0x0000000000000000000000000000000000000000", + "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" } }, "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", @@ -54,13 +54,13 @@ "treasuryWalletMainnet": "0x31d23172792db7d38c19a0d9136b47e5b9cb61bb", "treasuryWalletChain138": "0x6fba59d90a891588d14bc2ecd63371ebc1974759", "chains": { - "138": { - "tokenM0": "0x0000000000000000000000000000000000000000", - "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" - }, "1": { "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "138": { + "tokenM0": "0x0000000000000000000000000000000000000000", + "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" } }, "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", @@ -75,13 +75,13 @@ "treasuryWalletMainnet": "0x476eac28c5a369556590026a33bf59f7798e2121", "treasuryWalletChain138": "0x42c4091ab2080a029b3371b473cc22d27ed7170a", "chains": { - "138": { - "tokenM0": "0x0000000000000000000000000000000000000000", - "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" - }, "1": { "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "138": { + "tokenM0": "0x0000000000000000000000000000000000000000", + "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" } }, "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", @@ -96,13 +96,13 @@ "treasuryWalletMainnet": "0x3908d64ec91ad908ed0740654905b85f40c96892", "treasuryWalletChain138": "0xc536e3a0bb2ada09077f6fef49710ca2c5c7f404", "chains": { - "138": { - "tokenM0": "0x0000000000000000000000000000000000000000", - "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" - }, "1": { "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "138": { + "tokenM0": "0x0000000000000000000000000000000000000000", + "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" } }, "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", @@ -117,13 +117,13 @@ "treasuryWalletMainnet": "0x75aa3ef512ff29a0475954c0a478b6b8a0e7485b", "treasuryWalletChain138": "0xd1f1d00a3706fc5d680ac8a5bd49dcce17408ea2", "chains": { - "138": { - "tokenM0": "0x0000000000000000000000000000000000000000", - "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" - }, "1": { "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "138": { + "tokenM0": "0x0000000000000000000000000000000000000000", + "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" } }, "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", @@ -138,13 +138,13 @@ "treasuryWalletMainnet": "0x2f0d8f2b449eddbb0e1d57df5b8bab7bc1853fe3", "treasuryWalletChain138": "0xd437cbf6bf13f2c3b43abd0e5468e66edfe01299", "chains": { - "138": { - "tokenM0": "0x0000000000000000000000000000000000000000", - "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" - }, "1": { "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "138": { + "tokenM0": "0x0000000000000000000000000000000000000000", + "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" } }, "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", @@ -159,13 +159,13 @@ "treasuryWalletMainnet": "0xf75948728a2379b485dc4695233c102f0a261089", "treasuryWalletChain138": "0x7287fe01c4ab5f70e3d24dd6bc24ae925552bd40", "chains": { - "138": { - "tokenM0": "0x0000000000000000000000000000000000000000", - "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" - }, "1": { "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "138": { + "tokenM0": "0x0000000000000000000000000000000000000000", + "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" } }, "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", @@ -180,13 +180,13 @@ "treasuryWalletMainnet": "0xeaffdc8cff3096712beb4cdc0b0e076d8f3f68c6", "treasuryWalletChain138": "0xa7ebf614c6e47db20266be996db135d048fa20f7", "chains": { - "138": { - "tokenM0": "0x0000000000000000000000000000000000000000", - "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" - }, "1": { "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "138": { + "tokenM0": "0x0000000000000000000000000000000000000000", + "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" } }, "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", @@ -201,13 +201,13 @@ "treasuryWalletMainnet": "0x73f9180c4d219e3cca43b1c02895c685c4153414", "treasuryWalletChain138": "0xe5736303f0b60f9dc14ef45b71547c228f261e6e", "chains": { - "138": { - "tokenM0": "0x0000000000000000000000000000000000000000", - "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" - }, "1": { "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "138": { + "tokenM0": "0x0000000000000000000000000000000000000000", + "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" } }, "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", @@ -222,13 +222,13 @@ "treasuryWalletMainnet": "0x16cecc9f7d048c2a8829eea46b3c30fb134523fc", "treasuryWalletChain138": "0xac894ac9727fae7ff1c82103d74c5eafe5429d1a", "chains": { - "138": { - "tokenM0": "0x0000000000000000000000000000000000000000", - "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" - }, "1": { "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "138": { + "tokenM0": "0x0000000000000000000000000000000000000000", + "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" } }, "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", @@ -243,13 +243,13 @@ "treasuryWalletMainnet": "0x7090a3acdda8514d6b07aff27f91ee8430d0a61c", "treasuryWalletChain138": "0xdd0a1f686b87349bdc5aa909078688bc065a19f2", "chains": { - "138": { - "tokenM0": "0x0000000000000000000000000000000000000000", - "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" - }, "1": { "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "138": { + "tokenM0": "0x0000000000000000000000000000000000000000", + "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" } }, "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", @@ -264,13 +264,13 @@ "treasuryWalletMainnet": "0x5921918623c5fa3b3798467972497c6809d95790", "treasuryWalletChain138": "0xcf0acb4afb1e07dc7fb6a6cbbf40d302641f9eaa", "chains": { - "138": { - "tokenM0": "0x0000000000000000000000000000000000000000", - "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" - }, "1": { "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "138": { + "tokenM0": "0x0000000000000000000000000000000000000000", + "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" } }, "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", @@ -285,13 +285,13 @@ "treasuryWalletMainnet": "0xa312cc28d812832d03c72cb822b2b460ade174d1", "treasuryWalletChain138": "0x8112c72ecfc0493b2cc7622218bc015df97d8ceb", "chains": { - "138": { - "tokenM0": "0x0000000000000000000000000000000000000000", - "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" - }, "1": { "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "138": { + "tokenM0": "0x0000000000000000000000000000000000000000", + "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" } }, "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", @@ -306,13 +306,13 @@ "treasuryWalletMainnet": "0x09c8bbb678649ef543b0ca6e2d0248ed374be70e", "treasuryWalletChain138": "0x291069ab31457997037ac59296f4fff2b55a033c", "chains": { - "138": { - "tokenM0": "0x0000000000000000000000000000000000000000", - "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" - }, "1": { "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "138": { + "tokenM0": "0x0000000000000000000000000000000000000000", + "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" } }, "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", @@ -327,13 +327,13 @@ "treasuryWalletMainnet": "0xaca2ab7b574d19cee64b004baa46994abf9bf0f2", "treasuryWalletChain138": "0x0ad3a2a39dbf0c7b1646c2c57902f5756b95e93f", "chains": { - "138": { - "tokenM0": "0x0000000000000000000000000000000000000000", - "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" - }, "1": { "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "138": { + "tokenM0": "0x0000000000000000000000000000000000000000", + "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" } }, "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", @@ -348,13 +348,13 @@ "treasuryWalletMainnet": "0x45a70949a11fc813c82d44f80aec9fc07ff819e4", "treasuryWalletChain138": "0x3a3ae3e4355ac2a7cbeb1b12d2900eb37763f0b9", "chains": { - "138": { - "tokenM0": "0x0000000000000000000000000000000000000000", - "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" - }, "1": { "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "138": { + "tokenM0": "0x0000000000000000000000000000000000000000", + "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" } }, "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", @@ -369,13 +369,13 @@ "treasuryWalletMainnet": "0x6c2e2c8b8b5ecb82246ceabbcbc862dc2b77634a", "treasuryWalletChain138": "0x3d3475d9e9bc261372b99dd4f2b0df5dc40623dd", "chains": { - "138": { - "tokenM0": "0x0000000000000000000000000000000000000000", - "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" - }, "1": { "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "138": { + "tokenM0": "0x0000000000000000000000000000000000000000", + "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" } }, "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", @@ -390,13 +390,13 @@ "treasuryWalletMainnet": "0xea6eab4953f7f54f7c1001375e57d573bec66161", "treasuryWalletChain138": "0x106e4a810aff37a1bc62d30d414ec575e83e7539", "chains": { - "138": { - "tokenM0": "0x0000000000000000000000000000000000000000", - "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" - }, "1": { "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "138": { + "tokenM0": "0x0000000000000000000000000000000000000000", + "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" } }, "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", @@ -411,13 +411,13 @@ "treasuryWalletMainnet": "0x834a95c1f4216e90677334339074fda872b97847", "treasuryWalletChain138": "0xfd371f2457a6a58f11c80cc1c4ca2305f4674db3", "chains": { - "138": { - "tokenM0": "0x0000000000000000000000000000000000000000", - "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" - }, "1": { "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "138": { + "tokenM0": "0x0000000000000000000000000000000000000000", + "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" } }, "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", @@ -432,13 +432,13 @@ "treasuryWalletMainnet": "0x2e4d9d09801dc09e90b3752db42c1a8e6d033520", "treasuryWalletChain138": "0xa08621a479bc263e563b2eadb7490d1c07039ba7", "chains": { - "138": { - "tokenM0": "0x0000000000000000000000000000000000000000", - "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" - }, "1": { "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "138": { + "tokenM0": "0x0000000000000000000000000000000000000000", + "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" } }, "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", @@ -453,13 +453,13 @@ "treasuryWalletMainnet": "0x7af34ec0e3d65f147ba6134471bff7bb25f785d0", "treasuryWalletChain138": "0xa31bf188d4235aa509f6ead064587ee99e6029c6", "chains": { - "138": { - "tokenM0": "0x0000000000000000000000000000000000000000", - "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" - }, "1": { "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "138": { + "tokenM0": "0x0000000000000000000000000000000000000000", + "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" } }, "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", @@ -474,13 +474,13 @@ "treasuryWalletMainnet": "0x8c219794fdb74cf8ded871fab6135e40c4168da9", "treasuryWalletChain138": "0x204fa6d8c72a21116de75b37abf0d1cd642ddcfa", "chains": { - "138": { - "tokenM0": "0x0000000000000000000000000000000000000000", - "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" - }, "1": { "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "138": { + "tokenM0": "0x0000000000000000000000000000000000000000", + "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" } }, "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", @@ -495,13 +495,13 @@ "treasuryWalletMainnet": "0x87dab11386841366682d6c5fd3cd90587552f8cd", "treasuryWalletChain138": "0xc93522ae4ab7482572cc29edd68fbb6d5ec6a585", "chains": { - "138": { - "tokenM0": "0x0000000000000000000000000000000000000000", - "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" - }, "1": { "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "138": { + "tokenM0": "0x0000000000000000000000000000000000000000", + "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" } }, "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", @@ -516,13 +516,13 @@ "treasuryWalletMainnet": "0xfc4D24Bda60240D6dfa1a702eE83c65002cf5218", "treasuryWalletChain138": "0x934adfFFbe4D079b7dbDcE183805dBcBeC7818cb", "chains": { - "138": { - "tokenM0": "0x0000000000000000000000000000000000000000", - "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" - }, "1": { "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "138": { + "tokenM0": "0x0000000000000000000000000000000000000000", + "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" } }, "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", @@ -538,6 +538,10 @@ "treasuryWalletChain138": "0xc93522ae4ab7482572cc29edd68fbb6d5ec6a585", "treasuryWalletShivaOneX": "0x0000000000000000000000000000000000000000", "chains": { + "1": { + "tokenM0": "0x0000000000000000000000000000000000000000", + "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, "138": { "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b" @@ -546,9 +550,9 @@ "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0x0000000000000000000000000000000000000000" }, - "1": { + "900002": { "tokenM0": "0x0000000000000000000000000000000000000000", - "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + "tokenM1": "0x5FbDB2315678afecb367f032d93F642f64180aa3" } }, "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", diff --git a/config/omnl-production-domains.v1.json b/config/omnl-production-domains.v1.json index 9dc5de1..699c7c3 100644 --- a/config/omnl-production-domains.v1.json +++ b/config/omnl-production-domains.v1.json @@ -42,7 +42,8 @@ { "host": "digital.omdnl.org", "product": "digital-swap", - "defaultRoute": "/swap" + "defaultRoute": "/swap", + "routes": ["/z", "/z-wallet", "/swap", "/hub"] }, { "host": "office24.omdnl.org", diff --git a/config/omnl-supported-chains.v1.json b/config/omnl-supported-chains.v1.json index c05ed1c..7ecaac4 100644 --- a/config/omnl-supported-chains.v1.json +++ b/config/omnl-supported-chains.v1.json @@ -1,4 +1,4 @@ -{ +{.\scripts\deployment\deploy-z-ecosystem.ps1 "$schema": "OMNL Bank supported chains — 128-chain production registry", "version": "1.0.0", "brand": "OMNL Central Bank", @@ -1244,20 +1244,23 @@ }, { "chainId": 900002, - "name": "OMNL Expansion Slot 2", + "name": "Z Chain", "type": "evm", - "nativeSymbol": "ETH", - "tier": "reserved", - "omnlRole": "expansion", - "status": "reserved", + "nativeSymbol": "Z", + "tier": "primary", + "omnlRole": "z-chain", + "status": "active", "rpcEnv": "CHAIN_900002_RPC_URL", + "explorerUrl": "https://explorer.z.omdnl.org", + "rpcDefault": "http://127.0.0.1:8546", "settlement": { - "m2LoadEnabled": false, - "swapEnabled": false, - "bridgeEnabled": false, - "officeId": 24 + "m2LoadEnabled": true, + "swapEnabled": true, + "bridgeEnabled": true, + "officeId": 29 }, - "complianceEnv": "OMNL_COMPLIANCE_CORE_900002" + "complianceEnv": "OMNL_COMPLIANCE_CORE_900002", + "integrationRef": "config/z-chain-integration.v1.json" }, { "chainId": 900003, diff --git a/config/z-chain-deployed.v1.json b/config/z-chain-deployed.v1.json new file mode 100644 index 0000000..c6a03e6 --- /dev/null +++ b/config/z-chain-deployed.v1.json @@ -0,0 +1,17 @@ +{ + "$schema": "Z Chain on-chain deployment addresses (chainId 900002)", + "version": "1.0.0", + "updated": "2026-07-06", + "chainId": 900002, + "network": "z-chain", + "status": "active", + "contracts": { + "cUSDC": "0x5FbDB2315678afecb367f032d93F642f64180aa3", + "accountWalletRegistry": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" + }, + "deployScript": "script/z-chain/DeployZChainEmoney.s.sol", + "notes": [ + "Run scripts/deployment/deploy-z-chain-contracts.sh after bootstrap-z-chain-local", + "Update addresses here and in hybx-omnl-cross-chain-lines.json chains.900002" + ] +} diff --git a/config/z-chain-integration.v1.json b/config/z-chain-integration.v1.json new file mode 100644 index 0000000..4e6768d --- /dev/null +++ b/config/z-chain-integration.v1.json @@ -0,0 +1,105 @@ +{ + "$schema": "International Z Wallet + Z Chain — OMNL integration", + "version": "1.0.0", + "updated": "2026-07-05", + "brand": { + "wallet": "International Z Wallet", + "bank": "Z Online Bank", + "blockchain": "Z Chain", + "bot": "Z Bot" + }, + "chain": { + "chainId": 900002, + "name": "Z Chain", + "network": "z-chain", + "type": "evm", + "nativeSymbol": "Z", + "rpcEnv": "CHAIN_900002_RPC_URL", + "rpcDefault": "http://127.0.0.1:8546", + "explorerUrl": "https://explorer.z.omdnl.org", + "omnlRegistrySlot": 900002 + }, + "fineract": { + "officeId": 29, + "externalId": "ZBANK-OMNL", + "entityName": "zBank", + "hybxWalletId": "HYBX-OFFICE-029", + "m1SpendableGl": "2100" + }, + "bankCorridor": { + "orderingName": "International Z Wallet", + "creditorBic": "ZCHNOMNL33", + "debtorIban": "Z298ZCHN000000000000000029", + "swiftMessageKind": "MT103", + "remittanceTemplate": "International Z Wallet → Z Online Bank · Z Chain settlement" + }, + "internationalCorridors": [ + { + "id": "IQ-USD", + "label": "Iraq (USD)", + "country": "IQ", + "currency": "USD", + "rails": [ + "SWIFT", + "HYBX", + "CHAIN_MINT" + ] + }, + { + "id": "IR-USD", + "label": "Iran (USD)", + "country": "IR", + "currency": "USD", + "rails": [ + "HYBX", + "CHAIN_MINT" + ] + }, + { + "id": "EU-EUR", + "label": "Europe (EUR)", + "country": "EU", + "currency": "EUR", + "rails": [ + "SWIFT", + "HYBX" + ] + }, + { + "id": "GLOBAL-USD", + "label": "Global (USD)", + "country": "GLOBAL", + "currency": "USD", + "rails": [ + "SWIFT", + "HYBX", + "CHAIN_MINT" + ] + } + ], + "wallet": { + "thirdwebMetadataName": "International Z Wallet", + "supportedAuth": [ + "email", + "google", + "apple", + "passkey" + ], + "defaultTokenLineId": "USD-M2", + "defaultTokenSymbol": "cUSDC" + }, + "settlement": { + "zBridgeOfficeId": 24, + "targetChainId": 900002, + "rails": [ + "HYBX", + "SWIFT", + "CHAIN_MINT" + ] + }, + "contracts": { + "cUSDC": "0x5FbDB2315678afecb367f032d93F642f64180aa3", + "accountWalletRegistry": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + "deployRef": "config/z-chain-deployed.v1.json" + } +} diff --git a/config/z-chain-network-config.json b/config/z-chain-network-config.json new file mode 100644 index 0000000..3e909e1 --- /dev/null +++ b/config/z-chain-network-config.json @@ -0,0 +1,29 @@ +{ + "genesis": { + "config": { + "chainId": 900002, + "ibft2": { + "blockperiodseconds": 2, + "epochlength": 30000, + "requesttimeoutseconds": 10 + } + }, + "nonce": "0x0", + "timestamp": "0x0", + "gasLimit": "0x1c9c380", + "difficulty": "0x1", + "mixHash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365", + "coinbase": "0x0000000000000000000000000000000000000000", + "alloc": { + "0xfe3b557e8fb62b89f4916b721be55ceb828dbd73": { + "balance": "0x33b2e3c9fd0803ce8000000" + } + } + }, + "blockchain": { + "nodes": { + "generate": true, + "count": 1 + } + } +} diff --git a/config/z-chain-production.v1.json b/config/z-chain-production.v1.json new file mode 100644 index 0000000..9a9c8c5 --- /dev/null +++ b/config/z-chain-production.v1.json @@ -0,0 +1,34 @@ +{ + "$schema": "Z Chain production endpoints — Besu IBFT2 on LXC or Azure", + "version": "1.0.0", + "updated": "2026-07-06", + "chainId": 900002, + "network": "z-chain", + "genesis": "config/genesis-z-chain-900002.json", + "networkConfig": "config/z-chain-network-config.json", + "endpoints": { + "rpcPublic": "https://rpc.z.omdnl.org", + "rpcLan": "http://192.168.11.212:8546", + "explorer": "https://explorer.z.omdnl.org", + "walletPortal": "https://digital.omdnl.org/z-wallet", + "ecosystemHub": "https://digital.omdnl.org/z" + }, + "env": { + "rpc": "CHAIN_900002_RPC_URL", + "compliance": "OMNL_COMPLIANCE_CORE_900002", + "viteRpc": "VITE_RPC_URL_900002", + "viteExplorer": "VITE_Z_CHAIN_EXPLORER_URL" + }, + "besu": { + "containerName": "z-chain-besu-prod", + "rpcPort": 8546, + "dataPath": "/opt/besu-z-chain", + "genesisPath": "/opt/besu-z-chain/config/genesis.json" + }, + "deployScripts": { + "bootstrapLocal": "scripts/deployment/bootstrap-z-chain-local.sh", + "bootstrapProduction": "scripts/deployment/bootstrap-z-chain-production.sh", + "deployContracts": "scripts/deployment/deploy-z-chain-contracts.sh", + "syncAddresses": "scripts/deployment/sync-z-chain-deployed-addresses.mjs" + } +} diff --git a/config/z-ecosystem.local.env.example b/config/z-ecosystem.local.env.example new file mode 100644 index 0000000..dec4f91 --- /dev/null +++ b/config/z-ecosystem.local.env.example @@ -0,0 +1,15 @@ +# Z Ecosystem local overrides — copy to z-ecosystem.local.env (gitignored) +# Loaded by scripts/deployment/deploy-z-ecosystem.ps1 + +# Z Chain (local Hardhat/Besu) +CHAIN_900002_RPC_URL=http://127.0.0.1:8546 + +# Z Bot — Azure OpenAI (optional; rule-based fallback when unset) +AZURE_OPENAI_ENDPOINT=https://.openai.azure.com +AZURE_OPENAI_API_KEY= +AZURE_OPENAI_DEPLOYMENT=gpt-4o-mini +AZURE_OPENAI_API_VERSION=2024-02-15-preview + +# Settlement middleware (international transfers) +SETTLEMENT_MIDDLEWARE_URL=http://127.0.0.1:3011 +SETTLEMENT_MIDDLEWARE_CONFIG=config/settlement-middleware.production.v1.json diff --git a/config/z-ecosystem.v1.json b/config/z-ecosystem.v1.json new file mode 100644 index 0000000..bed3352 --- /dev/null +++ b/config/z-ecosystem.v1.json @@ -0,0 +1,41 @@ +{ + "$schema": "Z consumer ecosystem hub links", + "version": "1.0.0", + "updated": "2026-07-05", + "brand": "Z Ecosystem", + "routes": { + "hub": "/z", + "wallet": "/z-wallet", + "bot": "/z-wallet#z-bot", + "onlineBank": "/central-bank", + "settlement": "/central-bank#z-full-settlement" + }, + "apps": [ + { + "id": "zWallet", + "label": "International Z Wallet", + "path": "/z-wallet", + "description": "Thirdweb in-app wallet on Z Chain with international corridors" + }, + { + "id": "zBot", + "label": "Z Bot", + "path": "/z-wallet#z-bot", + "description": "In-app assistant for balance, transfers, and corridors" + }, + { + "id": "zChain", + "label": "Z Chain", + "path": "/z", + "chainId": 900002, + "description": "Consumer EVM chain (OMNL expansion slot 900002)" + }, + { + "id": "zOnlineBank", + "label": "Z Online Bank", + "path": "/central-bank", + "description": "M0–M4 banking and settlement rails" + } + ], + "integrationRef": "config/z-chain-integration.v1.json" +} diff --git a/contracts/z-chain/AccountWalletRegistryZ.sol b/contracts/z-chain/AccountWalletRegistryZ.sol new file mode 100644 index 0000000..1ade916 --- /dev/null +++ b/contracts/z-chain/AccountWalletRegistryZ.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/access/AccessControl.sol"; + +interface IAccountWalletRegistryZ { + struct WalletLink { + bytes32 walletRefId; + string provider; + bool active; + } + + function linkAccountToWallet(bytes32 accountRefId, bytes32 walletRefId, string calldata provider) external; + function getWalletsForAccount(bytes32 accountRefId) external view returns (WalletLink[] memory); +} + +/** + * @title AccountWalletRegistryZ + * @notice Minimal registry for Z Chain deploy scope + */ +contract AccountWalletRegistryZ is IAccountWalletRegistryZ, AccessControl { + bytes32 public constant ACCOUNT_MANAGER_ROLE = keccak256("ACCOUNT_MANAGER_ROLE"); + + mapping(bytes32 => WalletLink[]) private _accountToWallets; + + constructor(address admin) { + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(ACCOUNT_MANAGER_ROLE, admin); + } + + function linkAccountToWallet(bytes32 accountRefId, bytes32 walletRefId, string calldata provider) external { + require(hasRole(ACCOUNT_MANAGER_ROLE, msg.sender), "not manager"); + _accountToWallets[accountRefId].push(WalletLink(walletRefId, provider, true)); + } + + function getWalletsForAccount(bytes32 accountRefId) external view returns (WalletLink[] memory) { + return _accountToWallets[accountRefId]; + } +} diff --git a/contracts/z-chain/ZChainUSDC.sol b/contracts/z-chain/ZChainUSDC.sol new file mode 100644 index 0000000..039d695 --- /dev/null +++ b/contracts/z-chain/ZChainUSDC.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +/** + * @title ZChainUSDC + * @notice Minimal M2 stand-in for Z Chain local/dev deploys (cUSDC) + */ +contract ZChainUSDC is ERC20 { + constructor(address admin) ERC20("Z Chain USD", "cUSDC") { + _mint(admin, 1_000_000_000 * 10 ** 6); + } + + function decimals() public pure override returns (uint8) { + return 6; + } +} diff --git a/docs/04-configuration/Z_ECOSYSTEM.md b/docs/04-configuration/Z_ECOSYSTEM.md new file mode 100644 index 0000000..a614d5c --- /dev/null +++ b/docs/04-configuration/Z_ECOSYSTEM.md @@ -0,0 +1,149 @@ +# Z Ecosystem — Z Chain, International Z Wallet, Z Bot + +Consumer-facing Z products on OMNL expansion slot **900002**. + +## Components + +| Product | Route | Config | +|---------|-------|--------| +| Z Ecosystem hub | `/z` | `config/z-ecosystem.v1.json` | +| International Z Wallet | `/z-wallet` | `config/z-chain-integration.v1.json` | +| Z Bot (in-app chat) | `/z-wallet#z-bot` | `POST /api/v1/z-bot/chat` | +| Z Online Bank | `/central-bank` | existing z-bank settlement | + +## Local Z Chain bootstrap + +```bash +# Start Besu (chainId 900002) on port 8546 +bash scripts/deployment/bootstrap-z-chain-local.sh + +# Env +export CHAIN_900002_RPC_URL=http://127.0.0.1:8546 +export VITE_RPC_URL_900002=http://127.0.0.1:8546 + +# Deploy contracts (optional) +export PRIVATE_KEY=0x... +bash scripts/deployment/deploy-z-chain-contracts.sh +``` + +Windows: + +```powershell +.\scripts\deployment\bootstrap-z-chain-local.ps1 +``` + +Verify: `cast chain-id --rpc-url http://127.0.0.1:8546` → `900002` (hex `0xdbba2`) + +### One-command complete setup (Windows) + +```powershell +npm install --legacy-peer-deps +.\scripts\deployment\complete-z-chain.ps1 +``` + +Uses Hardhat node when Docker is unavailable. Keeps node running in background (`logs/z-chain-node.pid`). + + +## Frontend env + +```env +VITE_ENABLE_Z_WALLET=true +VITE_RPC_URL_900002=http://127.0.0.1:8546 +VITE_Z_CHAIN_EXPLORER_URL=https://explorer.z.omdnl.org +VITE_THIRDWEB_CLIENT_ID=... +``` + +## API + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/z-wallet/corridors` | International corridor catalog | +| GET | `/api/v1/z-wallet/status/:ref` | Settlement status proxy | +| GET | `/api/v1/z-bot/health` | Z Bot + Azure OpenAI config status | +| POST | `/api/v1/z-bot/chat` | Z Bot assistant | + +Z Bot body: + +```json +{ + "sessionId": "zbot-...", + "message": "What is my balance?", + "walletAddress": "0x...", + "chainId": 900002 +} +``` + +## Z Bot LLM (optional) + +Set Azure OpenAI for live replies; otherwise rule-based fallback: + +```env +AZURE_OPENAI_ENDPOINT=https://.openai.azure.com +AZURE_OPENAI_API_KEY=... +AZURE_OPENAI_DEPLOYMENT=gpt-4o-mini +AZURE_OPENAI_API_VERSION=2024-02-15-preview +``` + +## International corridors + +Defined in `config/z-chain-integration.v1.json`: + +- Iraq (USD) — SWIFT, HYBX, CHAIN_MINT +- Iran (USD) — HYBX, CHAIN_MINT +- Europe (EUR) — SWIFT, HYBX +- Global (USD) — SWIFT, HYBX, CHAIN_MINT + +Transfers submit to `POST /z-bank/settle-m0-m4` (or `/external-transfer` fallback) with `targetChainId: 900002`. + +## Registry + +`config/omnl-supported-chains.v1.json` slot **900002** — `name: Z Chain`, `status: active`, `omnlRole: z-chain`. + +## Production Besu (LXC / VM) + +On the Z Chain host (see `config/z-chain-production.v1.json`): + +```bash +sudo bash scripts/deployment/bootstrap-z-chain-production.sh +``` + +Wire nginx/Cloudflare: + +- `https://rpc.z.omdnl.org` → `127.0.0.1:8546` +- `https://explorer.z.omdnl.org` → Blockscout (optional, follow Chain 138 explorer pattern) +- Portal routes: `https://digital.omdnl.org/z` and `/z-wallet` + +Set on banking portal LXC: + +```env +CHAIN_900002_RPC_URL=https://rpc.z.omdnl.org +VITE_RPC_URL_900002=https://rpc.z.omdnl.org +VITE_Z_CHAIN_EXPLORER_URL=https://explorer.z.omdnl.org +``` + +## Local dev without Docker + +If Docker Desktop is not running, use Anvil: + +```powershell +.\scripts\deployment\bootstrap-z-chain-anvil.ps1 +``` + +Default Anvil account #0 key: `0xac0974bec39a17e36ba4a590bcafb95bd05417ff81f81088a92f31fcfcf35588` + +```bash +export PRIVATE_KEY=0xac0974bec39a17e36ba4a590bcafb95bd05417ff81f81088a92f31fcfcf35588 +bash scripts/deployment/deploy-z-chain-contracts.sh +node scripts/deployment/sync-z-chain-deployed-addresses.mjs +``` + +## Contract addresses + +After deploy, sync all configs: + +```bash +node scripts/deployment/sync-z-chain-deployed-addresses.mjs +``` + +Updates `z-chain-deployed.v1.json`, `z-chain-integration.v1.json`, and `hybx-omnl-cross-chain-lines.json`. + diff --git a/foundry.toml b/foundry.toml index 332ea8c..7d53e57 100644 --- a/foundry.toml +++ b/foundry.toml @@ -68,6 +68,12 @@ via_ir = true # Generic deploy profile for post-Dencun EVMs. Do not use for Chain 138. evm_version = "cancun" +[profile.zchain] +optimizer = true +optimizer_runs = 100 +via_ir = true +evm_version = "cancun" + [profile.chain138] optimizer = true optimizer_runs = 100 @@ -151,6 +157,7 @@ cbor_metadata = false # Prevents default localhost:8545 when ETH_RPC_URL not set [rpc_endpoints] chain138 = "http://192.168.11.211:8545" +zchain = "http://127.0.0.1:8546" avalanche = "https://avalanche-c-chain.publicnode.com" arbitrum = "https://arbitrum-one.publicnode.com" cronos = "https://evm.cronos.org" diff --git a/frontend-dapp/.env.example b/frontend-dapp/.env.example index 0e4cc28..82711c6 100644 --- a/frontend-dapp/.env.example +++ b/frontend-dapp/.env.example @@ -8,6 +8,12 @@ VITE_WALLETCONNECT_PROJECT_ID=your_reown_project_id_from_cloud_reown_com VITE_THIRDWEB_CLIENT_ID=your_thirdweb_client_id_here VITE_RPC_URL_138=https://rpc-http-pub.d-bis.org +# Z Chain (chainId 900002) — International Z Wallet +# VITE_ENABLE_Z_WALLET=true +# VITE_RPC_URL_900002=http://127.0.0.1:8546 +# VITE_Z_CHAIN_EXPLORER_URL=https://explorer.z.omdnl.org +# VITE_Z_CHAIN_ID=900002 + # Defi Oracle Meta Testnet (chain ID 2138) — off unless VITE_ENABLE_CHAIN2138 is true or 1 # VITE_ENABLE_CHAIN2138=true # VITE_RPC_URL_2138=https://rpc.public-2138.defi-oracle.io diff --git a/frontend-dapp/.env.local.example b/frontend-dapp/.env.local.example new file mode 100644 index 0000000..5dc9faa --- /dev/null +++ b/frontend-dapp/.env.local.example @@ -0,0 +1,5 @@ +# Z Chain local dev — generated by complete-z-chain.ps1 +VITE_ENABLE_Z_WALLET=true +VITE_RPC_URL_900002=http://127.0.0.1:8546 +VITE_Z_CHAIN_EXPLORER_URL=https://explorer.z.omdnl.org +CHAIN_900002_RPC_URL=http://127.0.0.1:8546 diff --git a/frontend-dapp/scripts/portal-serve.mjs b/frontend-dapp/scripts/portal-serve.mjs new file mode 100644 index 0000000..c94a3fc --- /dev/null +++ b/frontend-dapp/scripts/portal-serve.mjs @@ -0,0 +1,145 @@ +/** + * Production-style portal server: static dist + API proxies (matches vite.config.ts). + */ +import http from 'http'; +import https from 'https'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const DIST = path.join(__dirname, '..', 'dist'); +const PORT = Number(process.env.PORT || process.env.OMNL_BANK_FRONTEND_PORT || 3002); +const API_TARGET = process.env.TOKEN_AGGREGATION_URL || 'http://127.0.0.1:3000'; +const SETTLEMENT_TARGET = process.env.SETTLEMENT_URL || process.env.SETTLEMENT_MIDDLEWARE_URL || 'http://127.0.0.1:3011'; +const EXCHANGE_TARGET = process.env.EXCHANGE_URL || 'https://exchange.omdnl.org'; + +const MIME = { + '.html': 'text/html; charset=utf-8', + '.js': 'application/javascript; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.svg': 'image/svg+xml', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.gif': 'image/gif', + '.webp': 'image/webp', + '.ico': 'image/x-icon', + '.woff': 'font/woff', + '.woff2': 'font/woff2', + '.ttf': 'font/ttf', + '.map': 'application/json', +}; + +function shouldServeOmnlStatic(url) { + return ( + /^\/omnl\/(compliance|dashboard|terminal)\/?(\?.*)?$/.test(url) || + url.startsWith('/omnl/static/') + ); +} + +function proxyRequest(req, res, targetBase, rewrite) { + const incoming = new URL(req.url, 'http://localhost'); + const target = new URL(targetBase); + const pathname = rewrite ? rewrite(incoming.pathname) : incoming.pathname; + const options = { + protocol: target.protocol, + hostname: target.hostname, + port: target.port || (target.protocol === 'https:' ? 443 : 80), + path: `${pathname}${incoming.search}`, + method: req.method, + headers: { ...req.headers, host: target.host }, + }; + + const lib = target.protocol === 'https:' ? https : http; + const proxyReq = lib.request(options, (proxyRes) => { + res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers); + proxyRes.pipe(res); + }); + proxyReq.on('error', (err) => { + if (!res.headersSent) { + res.writeHead(502, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'proxy_error', message: err.message })); + } + }); + req.pipe(proxyReq); +} + +function sendFile(res, filePath, cacheControl) { + const ext = path.extname(filePath).toLowerCase(); + const type = MIME[ext] || 'application/octet-stream'; + const headers = { 'Content-Type': type }; + if (cacheControl) headers['Cache-Control'] = cacheControl; + res.writeHead(200, headers); + fs.createReadStream(filePath).pipe(res); +} + +function resolveStatic(urlPath) { + const decoded = decodeURIComponent(urlPath.split('?')[0]); + if (decoded.endsWith('/')) { + const indexPath = path.join(DIST, decoded, 'index.html'); + if (fs.existsSync(indexPath)) return indexPath; + } + const direct = path.join(DIST, decoded); + if (fs.existsSync(direct) && fs.statSync(direct).isFile()) return direct; + const withIndex = path.join(DIST, decoded, 'index.html'); + if (fs.existsSync(withIndex)) return withIndex; + return null; +} + +const server = http.createServer((req, res) => { + const url = req.url || '/'; + + if (url.startsWith('/api')) { + return proxyRequest(req, res, API_TARGET); + } + if (url.startsWith('/reserve')) { + return proxyRequest(req, res, API_TARGET); + } + if (url.startsWith('/settlement')) { + return proxyRequest(req, res, SETTLEMENT_TARGET, (pathname) => + pathname.replace(/^\/settlement/, '/api/v1/settlement'), + ); + } + if (url.startsWith('/exchange')) { + return proxyRequest(req, res, EXCHANGE_TARGET); + } + if (url.startsWith('/omnl/')) { + if (shouldServeOmnlStatic(url)) { + const file = resolveStatic(url); + if (file) { + const cache = url.startsWith('/omnl/static/') ? 'public, max-age=31536000, immutable' : 'no-cache'; + return sendFile(res, file, cache); + } + } + return proxyRequest(req, res, API_TARGET); + } + + const file = resolveStatic(url === '/' ? '/index.html' : url); + if (file) { + const cache = + url.startsWith('/assets/') ? 'public, max-age=31536000, immutable' : 'no-cache, no-store, must-revalidate'; + return sendFile(res, file, cache); + } + + const indexHtml = path.join(DIST, 'index.html'); + if (fs.existsSync(indexHtml)) { + return sendFile(res, indexHtml, 'no-cache, no-store, must-revalidate'); + } + + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('Not found'); +}); + +if (!fs.existsSync(DIST)) { + console.error(`Missing build output: ${DIST}\nRun: npm run build`); + process.exit(1); +} + +server.listen(PORT, () => { + console.log(`portal-serve listening on http://localhost:${PORT}`); + console.log(` dist: ${DIST}`); + console.log(` api: ${API_TARGET}`); + console.log(` settlement: ${SETTLEMENT_TARGET}`); +}); diff --git a/frontend-dapp/src/App.tsx b/frontend-dapp/src/App.tsx index 65fc3a2..8e82002 100644 --- a/frontend-dapp/src/App.tsx +++ b/frontend-dapp/src/App.tsx @@ -16,6 +16,8 @@ import AdminPanel from './pages/AdminPanel' import DocsPage from './pages/DocsPage' import HubPage from './pages/HubPage' import WalletsDemoPage from './pages/WalletsDemoPage' +import ZEcosystemHubPage from './pages/ZEcosystemHubPage' +import ZWalletPage from './pages/ZWalletPage' import Layout from './components/layout/Layout' import OmnlProductLayout from './components/layout/OmnlProductLayout' import ToastProvider from './components/ui/ToastProvider' @@ -51,6 +53,7 @@ function App() { }> } /> + } /> } /> } /> } /> @@ -69,6 +72,7 @@ function App() { } /> } /> } /> + } /> } /> diff --git a/frontend-dapp/src/config/chainIcons.ts b/frontend-dapp/src/config/chainIcons.ts index 8240181..676297a 100644 --- a/frontend-dapp/src/config/chainIcons.ts +++ b/frontend-dapp/src/config/chainIcons.ts @@ -14,6 +14,7 @@ export const CHAIN_ICON_URLS: Record = { 137: `${TW}/polygon/info/logo.png`, 138: ETH_DIAMOND, // Chain 138 (DBIS) - from chain-138.json 900001: ETH_DIAMOND, // Shiva Blockchain (OneX) + 900002: ETH_DIAMOND, // Z Chain 42161: `${TW}/arbitrum/info/logo.png`, 43114: `${TW}/avalanchec/info/logo.png`, 8453: `${TW}/base/info/logo.png`, diff --git a/frontend-dapp/src/config/networks.ts b/frontend-dapp/src/config/networks.ts index 9d136c7..6627b64 100644 --- a/frontend-dapp/src/config/networks.ts +++ b/frontend-dapp/src/config/networks.ts @@ -18,8 +18,14 @@ import { shivaOnexExplorerUrl, shivaOnexRpcUrl, } from './onexShiva' +import { + Z_CHAIN_ID, + zChainExplorerUrl, + zChainRpcUrl, +} from './zChain' const rpcUrlShivaOneX = shivaOnexRpcUrl +const rpcUrlZChain = zChainRpcUrl const rpcUrl138 = import.meta.env.VITE_RPC_URL_138 || 'https://rpc-http-pub.d-bis.org' const rpcUrl651940 = import.meta.env.VITE_RPC_URL_651940 || 'https://mainnet-rpc.alltra.global' const rpcUrl42793 = import.meta.env.VITE_RPC_URL_42793 || 'https://node.mainnet.etherlink.com' @@ -105,6 +111,30 @@ export const shivaOneXChain = defineChain({ export const shivaOneXChainId = shivaOneXChain.id +/** Chain 900002 - Z Chain (International Z Wallet) */ +export const zChain = defineChain({ + id: Z_CHAIN_ID, + name: 'Z Chain', + network: 'z-chain', + nativeCurrency: { + decimals: 18, + name: 'Z', + symbol: 'Z', + }, + rpcUrls: { + default: { http: [rpcUrlZChain] }, + public: { http: [rpcUrlZChain] }, + }, + blockExplorers: { + default: { + name: 'Z Chain Explorer', + url: zChainExplorerUrl, + }, + }, +}) + +export const zChainId = zChain.id + /** Chain 651940 - ALL Mainnet */ export const allMainnet = defineChain({ id: 651940, @@ -197,6 +227,7 @@ export const gnosis = defineChain({ const customChainsWithout2138 = [ chain138, shivaOneXChain, + zChain, allMainnet, etherlink, bsc, @@ -208,6 +239,7 @@ const customChainsWithout2138 = [ const customChainsWith2138 = [ chain138, shivaOneXChain, + zChain, chain2138Testnet, allMainnet, etherlink, @@ -224,8 +256,8 @@ export const customChains = chain2138TestnetEnabled /** Source chains the frontend can intentionally target for wallet UX. */ export const frontendSourceChains = chain2138TestnetEnabled - ? ([chain138, shivaOneXChain, chain2138Testnet] as const) - : ([chain138, shivaOneXChain] as const) + ? ([chain138, shivaOneXChain, zChain, chain2138Testnet] as const) + : ([chain138, shivaOneXChain, zChain] as const) /** Shared source-chain IDs for connect/switch prompts. */ export const frontendSourceChainIds: number[] = frontendSourceChains.map((chain) => chain.id) @@ -251,6 +283,7 @@ export const allNetworks = [...customChains, ...standardChains] as const export const supportedChainIds = [ chain138.id, shivaOneXChain.id, + zChain.id, ...(chain2138TestnetEnabled ? [chain2138Testnet.id] : []), allMainnet.id, etherlink.id, @@ -270,6 +303,7 @@ export const chainRpcUrls: Record = { [mainnet.id]: import.meta.env.VITE_MAINNET_RPC_URL || 'https://ethereum.publicnode.com', [chain138.id]: rpcUrl138, [shivaOneXChain.id]: rpcUrlShivaOneX, + [zChain.id]: rpcUrlZChain, ...(chain2138TestnetEnabled ? { [chain2138Testnet.id]: rpcUrl2138 } : {}), [allMainnet.id]: rpcUrl651940, [etherlink.id]: rpcUrl42793, diff --git a/frontend-dapp/src/config/zChain.ts b/frontend-dapp/src/config/zChain.ts new file mode 100644 index 0000000..6bb7c69 --- /dev/null +++ b/frontend-dapp/src/config/zChain.ts @@ -0,0 +1,21 @@ +import zChainConfig from '../../../config/z-chain-integration.v1.json'; + +export type ZChainConfig = typeof zChainConfig; + +export const zChain = zChainConfig; + +export const Z_CHAIN_ID = Number(import.meta.env.VITE_Z_CHAIN_ID || zChain.chain.chainId); + +export const zChainRpcUrl = + import.meta.env.VITE_RPC_URL_900002 || + import.meta.env.VITE_Z_CHAIN_RPC_URL || + zChain.chain.rpcDefault; + +export const zChainExplorerUrl = + import.meta.env.VITE_Z_CHAIN_EXPLORER_URL || zChain.chain.explorerUrl; + +export const zWalletEnabled = + import.meta.env.VITE_ENABLE_Z_WALLET !== 'false' && import.meta.env.VITE_ENABLE_Z_WALLET !== '0'; + +export const Z_WALLET_OFFICE_ID = zChain.fineract.officeId; +export const Z_HYBX_WALLET_ID = zChain.fineract.hybxWalletId; diff --git a/frontend-dapp/src/features/z-bot/ZBotPanel.tsx b/frontend-dapp/src/features/z-bot/ZBotPanel.tsx new file mode 100644 index 0000000..7103af0 --- /dev/null +++ b/frontend-dapp/src/features/z-bot/ZBotPanel.tsx @@ -0,0 +1,75 @@ +import { useEffect, useRef, useState } from 'react'; +import { useZBotChat } from './useZBotChat'; +import { zChain } from '../../config/zChain'; +import './z-bot.css'; + +type Props = { + walletAddress?: string; + chainId?: number; + defaultOpen?: boolean; +}; + +export default function ZBotPanel({ walletAddress, chainId, defaultOpen = false }: Props) { + const [open, setOpen] = useState(defaultOpen); + const [input, setInput] = useState(''); + const { messages, sendMessage, loading } = useZBotChat(walletAddress, chainId); + const chatRef = useRef(null); + + useEffect(() => { + if (chatRef.current) { + chatRef.current.scrollTop = chatRef.current.scrollHeight; + } + }, [messages, open]); + + const handleSend = () => { + const text = input; + setInput(''); + void sendMessage(text); + }; + + return ( + <> + + + {open && ( +
+
+ {zChain.brand.bot} + +
+
+ {messages.map((m, i) => ( +
+
{m.content}
+
+ ))} + {loading &&
Thinking…
} +
+
+ setInput(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleSend()} + /> + +
+
+ )} + + ); +} diff --git a/frontend-dapp/src/features/z-bot/useZBotChat.ts b/frontend-dapp/src/features/z-bot/useZBotChat.ts new file mode 100644 index 0000000..16405d5 --- /dev/null +++ b/frontend-dapp/src/features/z-bot/useZBotChat.ts @@ -0,0 +1,71 @@ +import { useCallback, useState } from 'react'; +import { TOKEN_AGGREGATION_URL } from '../../config/dex'; +import { omnlApiHeaders } from '../../config/dex'; + +export type ZBotMessage = { + role: 'user' | 'assistant'; + content: string; + timestamp: Date; +}; + +function sessionId(): string { + const key = 'z-bot-session'; + let id = sessionStorage.getItem(key); + if (!id) { + id = `zbot-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; + sessionStorage.setItem(key, id); + } + return id; +} + +export function useZBotChat(walletAddress?: string, chainId?: number) { + const [messages, setMessages] = useState([ + { + role: 'assistant', + content: 'Hi — I am Z Bot. Ask about your balance, international corridors, or transfer status.', + timestamp: new Date(), + }, + ]); + const [loading, setLoading] = useState(false); + + const sendMessage = useCallback( + async (text: string) => { + if (!text.trim()) return; + const userMsg: ZBotMessage = { role: 'user', content: text.trim(), timestamp: new Date() }; + setMessages((prev) => [...prev, userMsg]); + setLoading(true); + try { + const res = await fetch(`${TOKEN_AGGREGATION_URL}/z-bot/chat`, { + method: 'POST', + headers: omnlApiHeaders(), + body: JSON.stringify({ + sessionId: sessionId(), + message: text.trim(), + walletAddress, + chainId, + }), + }); + const data = (await res.json()) as { reply?: string; error?: string }; + const reply = data.reply ?? data.error ?? 'Sorry, I could not process that request.'; + setMessages((prev) => [ + ...prev, + { role: 'assistant', content: reply, timestamp: new Date() }, + ]); + } catch (e) { + setMessages((prev) => [ + ...prev, + { + role: 'assistant', + content: e instanceof Error ? e.message : 'Network error', + timestamp: new Date(), + }, + ]); + } finally { + setLoading(false); + } + }, + [walletAddress, chainId], + ); + + return { messages, sendMessage, loading }; +} diff --git a/frontend-dapp/src/features/z-bot/z-bot.css b/frontend-dapp/src/features/z-bot/z-bot.css new file mode 100644 index 0000000..a7ec8ef --- /dev/null +++ b/frontend-dapp/src/features/z-bot/z-bot.css @@ -0,0 +1,111 @@ +.z-bot-fab { + position: fixed; + right: 1.25rem; + bottom: 1.25rem; + z-index: 50; + padding: 0.65rem 1rem; + border-radius: 9999px; + background: linear-gradient(135deg, #7c3aed, #0d9488); + color: #fff; + font-weight: 600; + font-size: 0.875rem; + border: none; + cursor: pointer; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35); +} + +.z-bot-panel { + position: fixed; + right: 1.25rem; + bottom: 4.5rem; + z-index: 50; + width: min(360px, calc(100vw - 2rem)); + height: 420px; + display: flex; + flex-direction: column; + background: #1a1d24; + border: 1px solid rgba(124, 58, 237, 0.35); + border-radius: 1rem; + overflow: hidden; + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.45); +} + +.z-bot-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 1rem; + background: #252830; + font-weight: 600; + color: #e8f4ff; +} + +.z-bot-close { + background: none; + border: none; + color: #a0a0a0; + font-size: 1.25rem; + cursor: pointer; + line-height: 1; +} + +.z-bot-chat { + flex: 1; + overflow-y: auto; + padding: 0.75rem; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.z-bot-message { + max-width: 90%; + padding: 0.5rem 0.75rem; + border-radius: 0.75rem; + font-size: 0.875rem; + line-height: 1.4; +} + +.z-bot-message--user { + align-self: flex-end; + background: #0d9488; + color: #fff; +} + +.z-bot-message--assistant { + align-self: flex-start; + background: #2d3140; + color: #e8f4ff; +} + +.z-bot-input-row { + display: flex; + gap: 0.5rem; + padding: 0.75rem; + border-top: 1px solid rgba(255, 255, 255, 0.08); +} + +.z-bot-input { + flex: 1; + border-radius: 0.5rem; + border: 1px solid rgba(255, 255, 255, 0.1); + background: #252830; + color: #fff; + padding: 0.5rem 0.75rem; + font-size: 0.875rem; +} + +.z-bot-send { + border-radius: 0.5rem; + border: none; + background: #7c3aed; + color: #fff; + padding: 0.5rem 0.85rem; + font-weight: 500; + cursor: pointer; +} + +.z-bot-send:disabled { + opacity: 0.5; + cursor: not-allowed; +} diff --git a/frontend-dapp/src/features/z-wallet/InternationalTransferPanel.tsx b/frontend-dapp/src/features/z-wallet/InternationalTransferPanel.tsx new file mode 100644 index 0000000..4a2048c --- /dev/null +++ b/frontend-dapp/src/features/z-wallet/InternationalTransferPanel.tsx @@ -0,0 +1,82 @@ +import TransferStatusCard from './TransferStatusCard'; +import { useZWalletInternationalTransfer } from './useZWalletInternationalTransfer'; +import { useState } from 'react'; + +type Props = { + recipientAddress?: string; +}; + +export default function InternationalTransferPanel({ recipientAddress }: Props) { + const { submitTransfer, loading, lastResult, error, corridors } = useZWalletInternationalTransfer(); + const [amount, setAmount] = useState(''); + const [creditorIban, setCreditorIban] = useState(''); + const [corridorId, setCorridorId] = useState(corridors[0]?.id ?? 'GLOBAL-USD'); + const selected = corridors.find((c) => c.id === corridorId); + + return ( +
+
+

International transfer

+

Send abroad via Z Bank rails (SWIFT, HYBX, CHAIN_MINT).

+
+ + + + + + + + + + {error &&

{error}

} + {lastResult?.settlementId && } +
+ ); +} diff --git a/frontend-dapp/src/features/z-wallet/TransferStatusCard.tsx b/frontend-dapp/src/features/z-wallet/TransferStatusCard.tsx new file mode 100644 index 0000000..d1d378c --- /dev/null +++ b/frontend-dapp/src/features/z-wallet/TransferStatusCard.tsx @@ -0,0 +1,31 @@ +import { useZWalletTransferStatus } from './useZWalletTransferStatus'; + +type Props = { + settlementRef: string; +}; + +export default function TransferStatusCard({ settlementRef }: Props) { + const { status, loading } = useZWalletTransferStatus(settlementRef); + + return ( +
+

+ Reference: {settlementRef} +

+ {loading && !status &&

Loading status…

} + {status && ( + <> +

+ Phase: {status.phase ?? status.status ?? 'pending'} +

+ {status.chainTxHash && ( +

+ Tx: {status.chainTxHash} +

+ )} + {status.error &&

{status.error}

} + + )} +
+ ); +} diff --git a/frontend-dapp/src/features/z-wallet/useZWalletInternationalTransfer.ts b/frontend-dapp/src/features/z-wallet/useZWalletInternationalTransfer.ts new file mode 100644 index 0000000..ef13ddf --- /dev/null +++ b/frontend-dapp/src/features/z-wallet/useZWalletInternationalTransfer.ts @@ -0,0 +1,90 @@ +import { useCallback, useState } from 'react'; +import { omnlApiHeaders, settlementApi } from '../../config/dex'; +import { zChain, Z_CHAIN_ID } from '../../config/zChain'; +import type { SettlementResult } from '../online-bank/useZBankSettlement'; + +function idem(prefix: string) { + return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; +} + +async function postJson(url: string, body: unknown): Promise<{ ok: boolean; status: number; data: T }> { + const res = await fetch(url, { + method: 'POST', + headers: omnlApiHeaders(), + body: JSON.stringify(body), + }); + const data = (await res.json().catch(() => ({ error: res.statusText || `HTTP ${res.status}` }))) as T; + return { ok: res.ok, status: res.status, data }; +} + +export function useZWalletInternationalTransfer() { + const [loading, setLoading] = useState(false); + const [lastResult, setLastResult] = useState(null); + const [error, setError] = useState(null); + + const submitTransfer = useCallback( + async (params: { + amount: string; + creditorIban: string; + corridorId: string; + currency: string; + recipientAddress?: string; + }) => { + setLoading(true); + setError(null); + try { + const corridor = zChain.internationalCorridors.find((c) => c.id === params.corridorId); + const rail = corridor?.rails.includes('HYBX') ? 'HYBX' : 'SWIFT'; + const payload = { + idempotencyKey: idem('Z-WALLET-INTL'), + officeId: zChain.settlement.zBridgeOfficeId, + currency: params.currency, + amount: params.amount, + creditorIban: params.creditorIban, + beneficiaryName: zChain.brand.bank, + orderingName: zChain.bankCorridor.orderingName, + creditorBic: zChain.bankCorridor.creditorBic, + debtorIban: zChain.bankCorridor.debtorIban, + remittanceInfo: `${zChain.bankCorridor.remittanceTemplate} · ${params.corridorId}`, + moneyLayers: ['M1', 'M2', 'M4'] as const, + rail, + swiftMessageKind: zChain.bankCorridor.swiftMessageKind as 'MT103', + metadata: { + sourceOfficeId: zChain.fineract.officeId, + sourceHybxWallet: zChain.fineract.hybxWalletId, + targetChainId: Z_CHAIN_ID, + integration: 'z-chain', + corridorId: params.corridorId, + }, + convertToCrypto: params.recipientAddress + ? { + enabled: true, + targetChainId: Z_CHAIN_ID, + tokenLineId: zChain.wallet.defaultTokenLineId, + recipientAddress: params.recipientAddress, + } + : undefined, + }; + + let r = await postJson(settlementApi('/z-bank/settle-m0-m4'), payload); + if (r.status === 404 || r.status === 405) { + r = await postJson(settlementApi('/external-transfer'), payload); + } + setLastResult(r.data); + if (!r.ok) { + setError((r.data as { error?: string }).error ?? `Transfer failed (${r.status})`); + } + return r; + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + setError(msg); + return { ok: false, status: 0, data: { error: msg } as SettlementResult }; + } finally { + setLoading(false); + } + }, + [], + ); + + return { submitTransfer, loading, lastResult, error, corridors: zChain.internationalCorridors }; +} diff --git a/frontend-dapp/src/features/z-wallet/useZWalletTransferStatus.ts b/frontend-dapp/src/features/z-wallet/useZWalletTransferStatus.ts new file mode 100644 index 0000000..a93b867 --- /dev/null +++ b/frontend-dapp/src/features/z-wallet/useZWalletTransferStatus.ts @@ -0,0 +1,42 @@ +import { useCallback, useEffect, useState } from 'react'; +import { TOKEN_AGGREGATION_URL } from '../../config/dex'; +import { omnlApiHeaders } from '../../config/dex'; + +export type TransferStatus = { + settlementId?: string; + phase?: string; + status?: string; + error?: string; + chainTxHash?: string; +}; + +export function useZWalletTransferStatus(settlementRef: string | undefined) { + const [status, setStatus] = useState(null); + const [loading, setLoading] = useState(false); + + const refresh = useCallback(async () => { + if (!settlementRef) return; + setLoading(true); + try { + const res = await fetch(`${TOKEN_AGGREGATION_URL}/z-wallet/status/${encodeURIComponent(settlementRef)}`, { + headers: omnlApiHeaders(false), + }); + if (res.ok) { + setStatus((await res.json()) as TransferStatus); + } + } catch { + /* ignore */ + } finally { + setLoading(false); + } + }, [settlementRef]); + + useEffect(() => { + if (!settlementRef) return; + void refresh(); + const t = window.setInterval(() => void refresh(), 8000); + return () => window.clearInterval(t); + }, [settlementRef, refresh]); + + return { status, loading, refresh }; +} diff --git a/frontend-dapp/src/pages/HubPage.tsx b/frontend-dapp/src/pages/HubPage.tsx index 4261a09..e53eb2d 100644 --- a/frontend-dapp/src/pages/HubPage.tsx +++ b/frontend-dapp/src/pages/HubPage.tsx @@ -27,6 +27,12 @@ const OMNL_OPS: { path: string; label: string; icon: NasaIconName }[] = [ { path: '/omnl/terminal', label: 'Settlement terminal', icon: 'rail' }, ]; +const Z_ECOSYSTEM: { path: string; label: string; icon: NasaIconName }[] = [ + { path: '/z', label: 'Z Ecosystem hub', icon: 'globe' }, + { path: '/z-wallet', label: 'International Z Wallet', icon: 'globe' }, + { path: '/z-wallet#z-bot', label: 'Z Bot assistant', icon: 'rail' }, +]; + function LinkGrid({ title, items, @@ -71,6 +77,7 @@ export default function HubPage() { +

GUOSMM official gazette

diff --git a/frontend-dapp/src/pages/ZEcosystemHubPage.tsx b/frontend-dapp/src/pages/ZEcosystemHubPage.tsx new file mode 100644 index 0000000..7b3e5cd --- /dev/null +++ b/frontend-dapp/src/pages/ZEcosystemHubPage.tsx @@ -0,0 +1,49 @@ +import { Link } from 'react-router-dom'; +import NasaIcon, { type NasaIconName } from '../components/icons/NasaIcon'; +import zEcosystem from '../../../config/z-ecosystem.v1.json'; +import { zChain } from '../config/zChain'; + +const Z_APPS: { path: string; label: string; icon: NasaIconName; description: string }[] = + zEcosystem.apps.map((app) => ({ + path: app.path, + label: app.label, + icon: (app.id === 'zWallet' ? 'globe' : app.id === 'zBot' ? 'rail' : app.id === 'zOnlineBank' ? 'central-bank' : 'network') as NasaIconName, + description: app.description, + })); + +export default function ZEcosystemHubPage() { + return ( +
+
+

Z Ecosystem · chain {zChain.chain.chainId}

+

+ + Z Ecosystem +

+

+ {zChain.brand.blockchain} consumer entry — international wallet, Z Bot assistant, and Z Online Bank settlement. +

+
+ +
+

Apps

+
    + {Z_APPS.map(({ path, label, icon, description }) => ( +
  • + + + + {label} + + {description} + +
  • + ))} +
+
+
+ ); +} diff --git a/frontend-dapp/src/pages/ZWalletPage.tsx b/frontend-dapp/src/pages/ZWalletPage.tsx new file mode 100644 index 0000000..135aee7 --- /dev/null +++ b/frontend-dapp/src/pages/ZWalletPage.tsx @@ -0,0 +1,126 @@ +/** + * International Z Wallet — Thirdweb v5 on Z Chain (900002) + */ +import { createThirdwebClient, defineChain } from 'thirdweb'; +import { ThirdwebProvider, ConnectButton, useActiveAccount, useWalletBalance } from 'thirdweb/react'; +import { inAppWallet } from 'thirdweb/wallets'; +import { chainRpcUrls, zChain as zChainDef, zChainId } from '../config/networks'; +import { zChain, zWalletEnabled } from '../config/zChain'; +import InternationalTransferPanel from '../features/z-wallet/InternationalTransferPanel'; +import ZBotPanel from '../features/z-bot/ZBotPanel'; +import { Link } from 'react-router-dom'; + +const clientId = import.meta.env.VITE_THIRDWEB_CLIENT_ID || '542981292d51ec610388ba8985f027d7'; +const client = createThirdwebClient({ clientId }); + +const zThirdwebChain = defineChain({ + id: zChainDef.id, + name: zChainDef.name, + rpc: chainRpcUrls[zChainId] || zChain.chain.rpcDefault, + nativeCurrency: { + name: zChainDef.nativeCurrency.name, + symbol: zChainDef.nativeCurrency.symbol, + decimals: zChainDef.nativeCurrency.decimals, + }, +}); + +const wallets = [ + inAppWallet({ + auth: { + options: zChain.wallet.supportedAuth as ('email' | 'google' | 'apple' | 'passkey')[], + }, + metadata: { + name: zChain.wallet.thirdwebMetadataName, + image: { + src: '/favicon.ico', + width: 32, + height: 32, + }, + }, + }), +]; + +function ZWalletContent() { + const account = useActiveAccount(); + const { data: balance, isLoading: balanceLoading } = useWalletBalance({ + client, + chain: zThirdwebChain, + address: account?.address, + }); + + if (!zWalletEnabled) { + return ( +
+ International Z Wallet is disabled. Set VITE_ENABLE_Z_WALLET=true to enable. +
+ ); + } + + return ( +
+
+

+ + Z Ecosystem + {' '} + · chain {zChainId} +

+

{zChain.brand.wallet}

+

+ {zChain.brand.blockchain} — email, Google, Apple, passkey, or external wallet. International corridors via{' '} + {zChain.brand.bank}. +

+
+ +
+ + {account && ( +
+

+ Address: {account.address} +

+ {balanceLoading ? ( +

Loading balance…

+ ) : balance ? ( +

+ Balance:{' '} + + {balance.displayValue} {balance.symbol} + +

+ ) : null} +

+ Office {zChain.fineract.officeId} · {zChain.fineract.hybxWalletId} +

+
+ )} +
+ + + + +
+ ); +} + +export default function ZWalletPage() { + return ( + + + + ); +} diff --git a/hardhat.config.js b/hardhat.config.js index 5d1c250..2247242 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -37,7 +37,7 @@ module.exports = { }, networks: { hardhat: { - chainId: 31337, + chainId: process.env.Z_CHAIN_LOCAL === "1" ? 900002 : 31337, }, mainnet: { url: process.env.ETHEREUM_MAINNET_RPC || "https://eth.llamarpc.com", @@ -94,6 +94,13 @@ module.exports = { accounts: sharedAccounts, chainId: 43114, }, + zchain: { + url: process.env.CHAIN_900002_RPC_URL || "http://127.0.0.1:8546", + accounts: sharedAccounts.length + ? sharedAccounts + : ["0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"], + chainId: 900002, + }, }, // Single Etherscan API key = V2 for all chains (see docs/deployment/ETHERSCAN_V2_VERIFY_OTHER_CHAINS.md) etherscan: { diff --git a/hardhat.zchain.config.js b/hardhat.zchain.config.js new file mode 100644 index 0000000..ab5ff41 --- /dev/null +++ b/hardhat.zchain.config.js @@ -0,0 +1,39 @@ +require("@nomicfoundation/hardhat-toolbox"); +require("dotenv").config(); + +function resolveAccounts() { + const rawPrivateKey = (process.env.PRIVATE_KEY || "").trim(); + if (!rawPrivateKey) return []; + const normalizedPrivateKey = rawPrivateKey.startsWith("0x") ? rawPrivateKey : `0x${rawPrivateKey}`; + if (/^0x[0-9a-fA-F]{64}$/.test(normalizedPrivateKey)) return [normalizedPrivateKey]; + return []; +} + +const sharedAccounts = resolveAccounts(); +const devAccount = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; + +/** Minimal Hardhat config — Z Chain deploy only (avoids full monorepo compile). */ +module.exports = { + solidity: { + compilers: [ + { version: "0.8.20", settings: { optimizer: { enabled: true, runs: 200 } } }, + { version: "0.8.19", settings: { optimizer: { enabled: true, runs: 200 } } }, + ], + }, + networks: { + hardhat: { + chainId: process.env.Z_CHAIN_LOCAL === "1" ? 900002 : 31337, + }, + zchain: { + url: process.env.CHAIN_900002_RPC_URL || "http://127.0.0.1:8546", + accounts: sharedAccounts.length ? sharedAccounts : [devAccount], + chainId: 900002, + }, + }, + paths: { + sources: "./contracts/z-chain", + tests: "./test/z-chain", + cache: "./cache/zchain", + artifacts: "./artifacts/zchain", + }, +}; diff --git a/package.json b/package.json index 2fdd4cc..503e076 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,12 @@ "deploy:logger:gnosis": "hardhat run scripts/ccip-deployment/deploy-ccip-logger-multichain.js --network gnosis", "deploy:logger:cronos": "hardhat run scripts/ccip-deployment/deploy-ccip-logger-multichain.js --network cronos", "deploy:reporter:chain138": "node scripts/ccip-deployment/deploy-ccip-reporter.js", + "zchain:complete": "powershell -ExecutionPolicy Bypass -File scripts/deployment/complete-z-chain.ps1", + "zchain:deploy-portal": "powershell -ExecutionPolicy Bypass -File scripts/deployment/deploy-z-ecosystem.ps1", + "zchain:go-live": "powershell -ExecutionPolicy Bypass -File scripts/deployment/go-live-z-ecosystem.ps1", + "zchain:deploy-production": "bash scripts/deployment/deploy-z-ecosystem-production.sh", + "zchain:compile": "hardhat compile --config hardhat.zchain.config.js", + "zchain:deploy": "hardhat run scripts/deployment/deploy-z-chain-contracts.js --network zchain --config hardhat.zchain.config.js", "verify:logger": "hardhat verify --network mainnet", "verify:reporter": "hardhat verify --network chain138", "keeper": "node scripts/reserve/keeper-service.js", diff --git a/script/z-chain/DeployZChainEmoney.s.sol b/script/z-chain/DeployZChainEmoney.s.sol new file mode 100644 index 0000000..84fd7eb --- /dev/null +++ b/script/z-chain/DeployZChainEmoney.s.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Script.sol"; +import "forge-std/console.sol"; +import "../../contracts/z-chain/ZChainUSDC.sol"; +import "../../contracts/emoney/AccountWalletRegistry.sol"; + +/** + * @title DeployZChainEmoney + * @notice Deploys cUSDC + AccountWalletRegistry on Z Chain (900002) + */ +contract DeployZChainEmoney is Script { + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address deployer = vm.addr(deployerPrivateKey); + string memory rpcUrl = vm.envOr("CHAIN_900002_RPC_URL", string("http://127.0.0.1:8546")); + + console.log("Deployer:", deployer); + console.log("RPC URL:", rpcUrl); + console.log("Chain ID: 900002"); + + vm.startBroadcast(deployerPrivateKey); + + ZChainUSDC cUsdc = new ZChainUSDC(deployer); + AccountWalletRegistry registry = new AccountWalletRegistry(deployer); + + vm.stopBroadcast(); + + console.log("\n=== Z Chain deployment ==="); + console.log("cUSDC:", address(cUsdc)); + console.log("AccountWalletRegistry:", address(registry)); + console.log("\nUpdate config/z-chain-deployed.v1.json and hybx-omnl-cross-chain-lines.json chains.900002"); + } +} diff --git a/scripts/deployment/bootstrap-z-chain-anvil.ps1 b/scripts/deployment/bootstrap-z-chain-anvil.ps1 new file mode 100644 index 0000000..26d406d --- /dev/null +++ b/scripts/deployment/bootstrap-z-chain-anvil.ps1 @@ -0,0 +1,11 @@ +# Local Z Chain without Docker — Anvil (chainId 900002) +$ErrorActionPreference = "Stop" +$RpcPort = if ($env:Z_CHAIN_RPC_PORT) { $env:Z_CHAIN_RPC_PORT } else { "8546" } + +if (-not (Get-Command anvil -ErrorAction SilentlyContinue)) { + Write-Error "Install Foundry (anvil): https://book.getfoundry.sh/getting-started/installation" +} + +Write-Host "Starting Anvil as Z Chain on http://127.0.0.1:$RpcPort (chainId 900002)" +Write-Host "Use Anvil account #0 private key as PRIVATE_KEY for deploy." +anvil --chain-id 900002 --port $RpcPort --host 127.0.0.1 diff --git a/scripts/deployment/bootstrap-z-chain-anvil.sh b/scripts/deployment/bootstrap-z-chain-anvil.sh new file mode 100644 index 0000000..0393777 --- /dev/null +++ b/scripts/deployment/bootstrap-z-chain-anvil.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Local Z Chain without Docker — Anvil (chainId 900002) for contract deploy / wallet dev. +set -euo pipefail + +RPC_PORT="${Z_CHAIN_RPC_PORT:-8546}" +CHAIN_ID=900002 + +if ! command -v anvil &>/dev/null; then + echo "ERROR: install Foundry (anvil) — https://book.getfoundry.sh/getting-started/installation" + exit 1 +fi + +echo "Starting Anvil as Z Chain on http://127.0.0.1:$RPC_PORT (chainId $CHAIN_ID)" +echo "Account #0 private key is printed by Anvil — use as PRIVATE_KEY for deploy." +echo "" +exec anvil --chain-id "$CHAIN_ID" --port "$RPC_PORT" --host 127.0.0.1 diff --git a/scripts/deployment/bootstrap-z-chain-local.ps1 b/scripts/deployment/bootstrap-z-chain-local.ps1 new file mode 100644 index 0000000..ad10422 --- /dev/null +++ b/scripts/deployment/bootstrap-z-chain-local.ps1 @@ -0,0 +1,66 @@ +# Bootstrap local Z Chain (chainId 900002) with Hyperledger Besu via Docker. +$ErrorActionPreference = "Stop" + +$Root = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path +$NetworkDir = if ($env:Z_CHAIN_NETWORK_DIR) { $env:Z_CHAIN_NETWORK_DIR } else { Join-Path $Root "logs\z-chain-network" } +$GenesisConfig = Join-Path $Root "config\z-chain-network-config.json" +$RpcPort = if ($env:Z_CHAIN_RPC_PORT) { $env:Z_CHAIN_RPC_PORT } else { "8546" } +$ContainerName = if ($env:Z_CHAIN_BESU_CONTAINER) { $env:Z_CHAIN_BESU_CONTAINER } else { "z-chain-besu" } + +try { + docker info 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { throw "Docker daemon not running" } +} catch { + Write-Error "Docker is installed but not running. Start Docker Desktop or run: scripts\deployment\complete-z-chain.ps1 (Hardhat fallback)" +} + +if (-not (Test-Path $GenesisConfig)) { + Write-Error "Missing $GenesisConfig" +} + +New-Item -ItemType Directory -Force -Path $NetworkDir | Out-Null + +$GenesisOut = Join-Path $NetworkDir "genesis.json" +if (-not (Test-Path $GenesisOut)) { + Write-Host "Generating IBFT2 network files in $NetworkDir ..." + docker run --rm ` + -v "${Root}\config:/config:ro" ` + -v "${NetworkDir}:/network" ` + hyperledger/besu:latest ` + operator generate-blockchain-config ` + --config-file=/config/z-chain-network-config.json ` + --to=/network +} + +$existing = docker ps -a --format "{{.Names}}" 2>$null | Where-Object { $_ -eq $ContainerName } +if ($existing) { + Write-Host "Removing existing container $ContainerName ..." + docker rm -f $ContainerName | Out-Null +} + +$NodeKey = Join-Path $NetworkDir "keys\node1\key" +if (-not (Test-Path $NodeKey) -or -not (Test-Path $GenesisOut)) { + Write-Error "Generated network files missing under $NetworkDir" +} + +Write-Host "Starting Z Chain Besu on http://127.0.0.1:$RpcPort (chainId 900002) ..." +docker run -d ` + --name $ContainerName ` + -p "${RpcPort}:8545" ` + -v "${NetworkDir}:/network:ro" ` + hyperledger/besu:latest ` + --data-path=/tmp/besu ` + --genesis-file=/network/genesis.json ` + --node-private-key-file=/network/keys/node1/key ` + --rpc-http-enabled ` + --rpc-http-api=ETH,NET,WEB3,IBFT,ADMIN,DEBUG ` + --rpc-http-host=0.0.0.0 ` + --rpc-http-port=8545 ` + --rpc-http-cors-origins="*" ` + --host-allowlist="*" ` + --min-gas-price=0 | Out-Null + +Write-Host "" +Write-Host "Z Chain local RPC: http://127.0.0.1:$RpcPort" +Write-Host 'Set env: CHAIN_900002_RPC_URL=http://127.0.0.1:' + $RpcPort +Write-Host 'Set env: VITE_RPC_URL_900002=http://127.0.0.1:' + $RpcPort diff --git a/scripts/deployment/bootstrap-z-chain-local.sh b/scripts/deployment/bootstrap-z-chain-local.sh new file mode 100644 index 0000000..f45eec2 --- /dev/null +++ b/scripts/deployment/bootstrap-z-chain-local.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# Bootstrap local Z Chain (chainId 900002) with Hyperledger Besu via Docker. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +NETWORK_DIR="${Z_CHAIN_NETWORK_DIR:-$ROOT/logs/z-chain-network}" +GENESIS_CONFIG="$ROOT/config/z-chain-network-config.json" +RPC_PORT="${Z_CHAIN_RPC_PORT:-8546}" +CONTAINER_NAME="${Z_CHAIN_BESU_CONTAINER:-z-chain-besu}" + +cd "$ROOT" + +if ! command -v docker &>/dev/null; then + echo "ERROR: docker is required. Install Docker and retry." + exit 1 +fi + +if [[ ! -f "$GENESIS_CONFIG" ]]; then + echo "ERROR: missing $GENESIS_CONFIG" + exit 1 +fi + +mkdir -p "$NETWORK_DIR" + +if [[ ! -f "$NETWORK_DIR/genesis.json" ]]; then + echo "Generating IBFT2 network files in $NETWORK_DIR ..." + docker run --rm \ + -v "$ROOT/config:/config:ro" \ + -v "$NETWORK_DIR:/network" \ + hyperledger/besu:latest \ + operator generate-blockchain-config \ + --config-file=/config/z-chain-network-config.json \ + --to=/network +fi + +if docker ps -a --format '{{.Names}}' | grep -qx "$CONTAINER_NAME"; then + echo "Removing existing container $CONTAINER_NAME ..." + docker rm -f "$CONTAINER_NAME" >/dev/null +fi + +NODE_KEY="$NETWORK_DIR/keys/node1/key" +GENESIS="$NETWORK_DIR/genesis.json" + +if [[ ! -f "$NODE_KEY" || ! -f "$GENESIS" ]]; then + echo "ERROR: generated network files missing under $NETWORK_DIR" + exit 1 +fi + +echo "Starting Z Chain Besu on http://127.0.0.1:$RPC_PORT (chainId 900002) ..." +docker run -d \ + --name "$CONTAINER_NAME" \ + -p "${RPC_PORT}:8545" \ + -v "$NETWORK_DIR:/network:ro" \ + hyperledger/besu:latest \ + --data-path=/tmp/besu \ + --genesis-file=/network/genesis.json \ + --node-private-key-file=/network/keys/node1/key \ + --rpc-http-enabled \ + --rpc-http-api=ETH,NET,WEB3,IBFT,ADMIN,DEBUG \ + --rpc-http-host=0.0.0.0 \ + --rpc-http-port=8545 \ + --rpc-http-cors-origins="*" \ + --host-allowlist="*" \ + --min-gas-price=0 + +echo "" +echo "Z Chain local RPC: http://127.0.0.1:$RPC_PORT" +echo "Export: CHAIN_900002_RPC_URL=http://127.0.0.1:$RPC_PORT" +echo "Export: VITE_RPC_URL_900002=http://127.0.0.1:$RPC_PORT" +echo "Verify: cast chain-id --rpc-url http://127.0.0.1:$RPC_PORT" diff --git a/scripts/deployment/bootstrap-z-chain-production.sh b/scripts/deployment/bootstrap-z-chain-production.sh new file mode 100644 index 0000000..3224367 --- /dev/null +++ b/scripts/deployment/bootstrap-z-chain-production.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Production Z Chain Besu bootstrap (chainId 900002) on a banking LXC or VM. +# Generates IBFT2 keys once, then runs Besu with persistent data under /opt/besu-z-chain. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +NETWORK_DIR="${Z_CHAIN_NETWORK_DIR:-/opt/besu-z-chain/network}" +DATA_PATH="${Z_CHAIN_DATA_PATH:-/opt/besu-z-chain/data}" +GENESIS_CONFIG="$ROOT/config/z-chain-network-config.json" +RPC_PORT="${Z_CHAIN_RPC_PORT:-8546}" +CONTAINER_NAME="${Z_CHAIN_BESU_CONTAINER:-z-chain-besu-prod}" + +if [[ $EUID -ne 0 ]]; then + echo "Run as root on the target host (or use sudo)." + exit 1 +fi + +if ! command -v docker &>/dev/null; then + echo "ERROR: docker required on production host" + exit 1 +fi + +mkdir -p "$NETWORK_DIR" "$DATA_PATH" + +if [[ ! -f "$NETWORK_DIR/genesis.json" ]]; then + echo "Generating IBFT2 network files in $NETWORK_DIR ..." + docker run --rm \ + -v "$ROOT/config:/config:ro" \ + -v "$NETWORK_DIR:/network" \ + hyperledger/besu:latest \ + operator generate-blockchain-config \ + --config-file=/config/z-chain-network-config.json \ + --to=/network +fi + +docker rm -f "$CONTAINER_NAME" 2>/dev/null || true + +echo "Starting production Z Chain Besu on 0.0.0.0:$RPC_PORT ..." +docker run -d \ + --name "$CONTAINER_NAME" \ + --restart unless-stopped \ + -p "127.0.0.1:${RPC_PORT}:8545" \ + -v "$NETWORK_DIR:/network:ro" \ + -v "$DATA_PATH:/data" \ + hyperledger/besu:latest \ + --data-path=/data \ + --genesis-file=/network/genesis.json \ + --node-private-key-file=/network/keys/node1/key \ + --rpc-http-enabled \ + --rpc-http-api=ETH,NET,WEB3,IBFT,ADMIN \ + --rpc-http-host=127.0.0.1 \ + --rpc-http-port=8545 \ + --host-allowlist="*" \ + --min-gas-price=0 + +echo "" +echo "Production Z Chain RPC (localhost): http://127.0.0.1:$RPC_PORT" +echo "Expose via nginx/Cloudflare as https://rpc.z.omdnl.org" +echo "Set CHAIN_900002_RPC_URL on portal LXC after tunnel/nginx is wired." diff --git a/scripts/deployment/complete-z-chain.ps1 b/scripts/deployment/complete-z-chain.ps1 new file mode 100644 index 0000000..00269cd --- /dev/null +++ b/scripts/deployment/complete-z-chain.ps1 @@ -0,0 +1,90 @@ +# Complete Z Chain local setup: RPC node + contract deploy + config sync. +# Uses Docker Besu when available; otherwise Hardhat node chainId 900002. +$ErrorActionPreference = "Stop" + +$Root = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path +$RpcPort = if ($env:Z_CHAIN_RPC_PORT) { $env:Z_CHAIN_RPC_PORT } else { "8546" } +$RpcUrl = "http://127.0.0.1:$RpcPort" +$NodeLog = Join-Path $Root "logs\z-chain-node.log" +$ErrLog = Join-Path $Root "logs\z-chain-node.err.log" +$PidFile = Join-Path $Root "logs\z-chain-node.pid" + +Set-Location $Root +New-Item -ItemType Directory -Force -Path (Join-Path $Root "logs") | Out-Null + +function Test-RpcReady { + try { + $body = '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}' + $r = Invoke-RestMethod -Uri $RpcUrl -Method Post -Body $body -ContentType "application/json" -TimeoutSec 2 + return ($r.result -eq "0xdbba2") + } catch { + return $false + } +} + +function Stop-ZChainNode { + if (Test-Path $PidFile) { + $oldPid = Get-Content $PidFile -ErrorAction SilentlyContinue + if ($oldPid) { + Stop-Process -Id $oldPid -Force -ErrorAction SilentlyContinue + } + Remove-Item $PidFile -Force -ErrorAction SilentlyContinue + } + try { + docker rm -f z-chain-besu 2>$null | Out-Null + } catch {} + $conn = Get-NetTCPConnection -LocalPort $RpcPort -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($conn -and $conn.OwningProcess) { + Stop-Process -Id $conn.OwningProcess -Force -ErrorAction SilentlyContinue + Start-Sleep -Seconds 2 + } +} + +$dockerOk = $false +try { + docker info 2>&1 | Out-Null + if ($LASTEXITCODE -eq 0) { $dockerOk = $true } +} catch {} + +Stop-ZChainNode + +if ($dockerOk) { + Write-Host "Starting Z Chain via Docker Besu ..." + & "$PSScriptRoot\bootstrap-z-chain-local.ps1" +} else { + $env:Z_CHAIN_LOCAL = "1" + $hardhat = Join-Path $Root "node_modules\.bin\hardhat.cmd" + $nodeCmd = "set Z_CHAIN_LOCAL=1&& `"$hardhat`" node --port $RpcPort --hostname 127.0.0.1" + $nodeProc = Start-Process -FilePath "cmd.exe" -ArgumentList @("/c", $nodeCmd) -WorkingDirectory $Root -PassThru -WindowStyle Hidden -RedirectStandardOutput $NodeLog -RedirectStandardError $ErrLog + $nodeProc.Id | Set-Content $PidFile + Start-Sleep -Seconds 8 +} + +$deadline = (Get-Date).AddSeconds(90) +while (-not (Test-RpcReady)) { + if ((Get-Date) -gt $deadline) { + Write-Error "Z Chain RPC not ready at $RpcUrl after 90s. See $NodeLog and $ErrLog" + } + Start-Sleep -Seconds 2 +} +Write-Host "Z Chain RPC ready at $RpcUrl" + +$env:CHAIN_900002_RPC_URL = $RpcUrl +$env:Z_CHAIN_LOCAL = "1" +if (-not $env:PRIVATE_KEY) { + $env:PRIVATE_KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" +} + +Write-Host "Compiling Z Chain contracts ..." +npx hardhat compile --config hardhat.zchain.config.js 2>&1 | Out-Null +if ($LASTEXITCODE -ne 0) { throw "zchain compile failed" } + +Write-Host "Deploying Z Chain contracts ..." +npx hardhat run scripts/deployment/deploy-z-chain-contracts.js --network zchain --config hardhat.zchain.config.js +if ($LASTEXITCODE -ne 0) { throw "contract deploy failed" } + +Write-Host "" +Write-Host "Z Chain setup complete." +Write-Host " RPC: $RpcUrl" +Write-Host " Set VITE_ENABLE_Z_WALLET=true and VITE_RPC_URL_900002=$RpcUrl" +Write-Host " Updated config/z-chain-deployed.v1.json" diff --git a/scripts/deployment/deploy-omnl-bank-production.sh b/scripts/deployment/deploy-omnl-bank-production.sh index a2f812d..b2593e4 100644 --- a/scripts/deployment/deploy-omnl-bank-production.sh +++ b/scripts/deployment/deploy-omnl-bank-production.sh @@ -98,6 +98,9 @@ cd "$REPO_DIR/frontend-dapp" export VITE_TOKEN_AGGREGATION_URL="${VITE_TOKEN_AGGREGATION_URL:-/api/v1}" export VITE_SETTLEMENT_MIDDLEWARE_URL="${VITE_SETTLEMENT_MIDDLEWARE_URL:-/settlement}" export VITE_DBIS_EXCHANGE_URL="${VITE_DBIS_EXCHANGE_URL:-/exchange}" +export VITE_ENABLE_Z_WALLET="${VITE_ENABLE_Z_WALLET:-true}" +export VITE_RPC_URL_900002="${VITE_RPC_URL_900002:-${CHAIN_900002_RPC_URL:-https://rpc.z.omdnl.org}}" +export VITE_Z_CHAIN_EXPLORER_URL="${VITE_Z_CHAIN_EXPLORER_URL:-https://explorer.z.omdnl.org}" # Never embed operator API keys in production frontend — nginx injects server-side. unset VITE_OMNL_API_KEY if command -v pnpm >/dev/null 2>&1; then @@ -147,6 +150,11 @@ start_svc() { SWIFT_LISTENER_URL="${SWIFT_LISTENER_URL:-http://127.0.0.1:8788}" \ RPC_URL_138="${RPC_URL_138:-${CHAIN_138_RPC_URL:-}}" \ CHAIN_138_RPC_URL="${CHAIN_138_RPC_URL:-${RPC_URL_138:-}}" \ + CHAIN_900002_RPC_URL="${CHAIN_900002_RPC_URL:-}" \ + AZURE_OPENAI_ENDPOINT="${AZURE_OPENAI_ENDPOINT:-}" \ + AZURE_OPENAI_API_KEY="${AZURE_OPENAI_API_KEY:-}" \ + AZURE_OPENAI_DEPLOYMENT="${AZURE_OPENAI_DEPLOYMENT:-}" \ + AZURE_OPENAI_API_VERSION="${AZURE_OPENAI_API_VERSION:-2024-02-15-preview}" \ OMNL_MINT_OPERATOR_PRIVATE_KEY="${OMNL_MINT_OPERATOR_PRIVATE_KEY:-}" \ bash -c "$cmd" >"$LOG_DIR/$name.log" 2>&1 & echo $! >"$LOG_DIR/$name.pid" @@ -179,6 +187,10 @@ 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" +curl -sf -o /dev/null -w "z-hub=%{http_code}\n" "http://127.0.0.1:$FRONTEND_PORT/z" +curl -sf -o /dev/null -w "z-wallet=%{http_code}\n" "http://127.0.0.1:$FRONTEND_PORT/z-wallet" +curl -sf "http://127.0.0.1:3000/api/v1/z-bot/health" | head -c 200 || true +echo curl -sf "http://127.0.0.1:8788/health" 2>/dev/null | head -c 80 || log "WARN: SWIFT listener :8788 not reachable" if [[ -n "${DATABASE_URL:-}" ]] && [[ "${SEED_SUPER_ADMIN_USERS:-1}" == "1" ]]; then diff --git a/scripts/deployment/deploy-omnl-domains.sh b/scripts/deployment/deploy-omnl-domains.sh index b656be6..157ba00 100644 --- a/scripts/deployment/deploy-omnl-domains.sh +++ b/scripts/deployment/deploy-omnl-domains.sh @@ -41,6 +41,7 @@ check_host exchange.omdnl.org /trade check_host secure.d-bis.org /central-bank check_host office24.omdnl.org /office-24 check_host digital.omdnl.org /swap +check_host digital.omdnl.org /z log "OMNL domain deploy complete" log "DNS: point all domains A record to 76.53.10.34 (or Cloudflare proxy to origin)" diff --git a/scripts/deployment/deploy-z-chain-contracts.js b/scripts/deployment/deploy-z-chain-contracts.js new file mode 100644 index 0000000..1a506ff --- /dev/null +++ b/scripts/deployment/deploy-z-chain-contracts.js @@ -0,0 +1,74 @@ +const { ethers } = require('hardhat'); +const fs = require('fs'); +const path = require('path'); +require('dotenv').config(); + +const ROOT = path.resolve(__dirname, '../..'); + +function syncAddresses(cUsdc, registry) { + const today = new Date().toISOString().slice(0, 10); + + const deployedPath = path.join(ROOT, 'config/z-chain-deployed.v1.json'); + const deployed = JSON.parse(fs.readFileSync(deployedPath, 'utf8')); + deployed.updated = today; + deployed.status = 'active'; + deployed.contracts.cUSDC = cUsdc; + deployed.contracts.accountWalletRegistry = registry; + fs.writeFileSync(deployedPath, `${JSON.stringify(deployed, null, 2)}\n`); + + const integrationPath = path.join(ROOT, 'config/z-chain-integration.v1.json'); + const integration = JSON.parse(fs.readFileSync(integrationPath, 'utf8')); + integration.contracts.cUSDC = cUsdc; + integration.contracts.accountWalletRegistry = registry; + fs.writeFileSync(integrationPath, `${JSON.stringify(integration, null, 2)}\n`); + + const hybxPath = path.join(ROOT, 'config/hybx-omnl-cross-chain-lines.json'); + const hybx = JSON.parse(fs.readFileSync(hybxPath, 'utf8')); + for (const line of hybx.lines || []) { + if (line.chains?.['900002']) { + line.chains['900002'].tokenM1 = cUsdc; + } + } + fs.writeFileSync(hybxPath, `${JSON.stringify(hybx, null, 2)}\n`); + + const broadcastDir = path.join(ROOT, 'broadcast/DeployZChainEmoney.s.sol/900002'); + fs.mkdirSync(broadcastDir, { recursive: true }); + const broadcast = { + transactions: [ + { contractName: 'ZChainUSDC', contractAddress: cUsdc }, + { contractName: 'AccountWalletRegistryZ', contractAddress: registry }, + ], + }; + fs.writeFileSync(path.join(broadcastDir, 'run-latest.json'), `${JSON.stringify(broadcast, null, 2)}\n`); + + console.log('\nConfig updated:'); + console.log(' cUSDC:', cUsdc); + console.log(' AccountWalletRegistry:', registry); +} + +async function main() { + const [deployer] = await ethers.getSigners(); + const network = await ethers.provider.getNetwork(); + console.log('Deployer:', deployer.address); + console.log('Chain ID:', network.chainId.toString()); + console.log('Balance:', ethers.formatEther(await ethers.provider.getBalance(deployer.address))); + + const ZChainUSDC = await ethers.getContractFactory('ZChainUSDC'); + const cUsdc = await ZChainUSDC.deploy(deployer.address); + await cUsdc.waitForDeployment(); + const cUsdcAddress = await cUsdc.getAddress(); + console.log('ZChainUSDC deployed:', cUsdcAddress); + + const AccountWalletRegistry = await ethers.getContractFactory('AccountWalletRegistryZ'); + const registry = await AccountWalletRegistry.deploy(deployer.address); + await registry.waitForDeployment(); + const registryAddress = await registry.getAddress(); + console.log('AccountWalletRegistry deployed:', registryAddress); + + syncAddresses(cUsdcAddress, registryAddress); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/deployment/deploy-z-chain-contracts.sh b/scripts/deployment/deploy-z-chain-contracts.sh new file mode 100644 index 0000000..c707cc6 --- /dev/null +++ b/scripts/deployment/deploy-z-chain-contracts.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Deploy Z Chain contracts (cUSDC + AccountWalletRegistry) on chainId 900002. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +cd "$ROOT" + +RPC="${CHAIN_900002_RPC_URL:-http://127.0.0.1:8546}" + +if [[ -z "${PRIVATE_KEY:-}" ]]; then + echo "ERROR: set PRIVATE_KEY (Besu dev key #1 works for local bootstrap)" + exit 1 +fi + +echo "Deploying to Z Chain at $RPC ..." +forge script script/z-chain/DeployZChainEmoney.s.sol:DeployZChainEmoney \ + --rpc-url "$RPC" \ + --chain-id 900002 \ + --broadcast \ + --profile zchain + +echo "Done. Copy deployed addresses into config/z-chain-deployed.v1.json" +echo "Or run: node scripts/deployment/sync-z-chain-deployed-addresses.mjs" diff --git a/scripts/deployment/deploy-z-ecosystem-production.sh b/scripts/deployment/deploy-z-ecosystem-production.sh new file mode 100644 index 0000000..2465627 --- /dev/null +++ b/scripts/deployment/deploy-z-ecosystem-production.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Production deploy: Z Chain env + OMNL banking portal (LXC / server). +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +ENV_FILE="${Z_ECOSYSTEM_ENV:-${OMNL_BANK_ENV:-$ROOT/.env}}" +PROD_CFG="$ROOT/config/z-chain-production.v1.json" + +log() { echo "[$(date -Iseconds)] $*"; } + +if [[ -f "$ENV_FILE" ]]; then + set -a + set +u + # shellcheck disable=SC1090 + source "$ENV_FILE" + set -u + set +a +fi + +export CHAIN_900002_RPC_URL="${CHAIN_900002_RPC_URL:-https://rpc.z.omdnl.org}" +export VITE_RPC_URL_900002="${VITE_RPC_URL_900002:-$CHAIN_900002_RPC_URL}" +export VITE_Z_CHAIN_EXPLORER_URL="${VITE_Z_CHAIN_EXPLORER_URL:-https://explorer.z.omdnl.org}" +export VITE_ENABLE_Z_WALLET="${VITE_ENABLE_Z_WALLET:-true}" + +log "Z Ecosystem production deploy" +log " config: $PROD_CFG" +log " rpc: $CHAIN_900002_RPC_URL" + +if [[ "${Z_CHAIN_BOOTSTRAP_PROD:-0}" == "1" ]]; then + log "Bootstrapping Besu Z Chain on this host..." + sudo bash "$ROOT/scripts/deployment/bootstrap-z-chain-production.sh" +fi + +if [[ "${Z_CHAIN_DEPLOY_CONTRACTS:-0}" == "1" ]]; then + log "Deploying Z Chain contracts..." + export CHAIN_900002_RPC_URL + node "$ROOT/scripts/deployment/deploy-z-chain-contracts.js" --network zchain + node "$ROOT/scripts/deployment/sync-z-chain-deployed-addresses.mjs" +fi + +log "Building and starting OMNL stack (includes Z Wallet routes)..." +bash "$ROOT/scripts/deployment/deploy-omnl-bank-production.sh" + +if [[ "${Z_ECOSYSTEM_PUSH_PCT:-0}" == "1" ]]; then + log "Pushing portal bundle to Proxmox LXC..." + bash "$ROOT/scripts/deployment/deploy-omnl-banking-portal-pct.sh" +fi + +log "Done. Verify:" +log " curl -sf http://127.0.0.1:3011/api/v1/settlement/health" +log " curl -sf http://127.0.0.1:3000/api/v1/z-bot/health" +log " Portal: https://digital.omdnl.org/z-wallet (after nginx/Cloudflare)" diff --git a/scripts/deployment/deploy-z-ecosystem.ps1 b/scripts/deployment/deploy-z-ecosystem.ps1 new file mode 100644 index 0000000..ea3a348 --- /dev/null +++ b/scripts/deployment/deploy-z-ecosystem.ps1 @@ -0,0 +1,133 @@ +# Deploy Z Ecosystem portal locally (chain + settlement + API + portal) +$ErrorActionPreference = "Stop" +$Root = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path +$LogDir = Join-Path $Root "logs" +New-Item -ItemType Directory -Force -Path $LogDir | Out-Null + +function Import-DotEnvFile([string]$Path) { + if (-not (Test-Path $Path)) { return } + Get-Content $Path | ForEach-Object { + $line = $_.Trim() + if ($line -eq "" -or $line.StartsWith("#")) { return } + if ($line -match '^\s*([^=]+)=(.*)$') { + $name = $matches[1].Trim() + $value = $matches[2].Trim().Trim('"').Trim("'") + Set-Item -Path "env:$name" -Value $value + } + } +} + +function Test-ZChainRpc { + try { + $body = '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}' + $r = Invoke-RestMethod -Uri "http://127.0.0.1:8546" -Method Post -Body $body -ContentType "application/json" -TimeoutSec 2 + return ($r.result -eq "0xdbba2") + } catch { return $false } +} + +function Stop-Port($port) { + $conn = Get-NetTCPConnection -LocalPort $port -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($conn -and $conn.OwningProcess) { + Stop-Process -Id $conn.OwningProcess -Force -ErrorAction SilentlyContinue + Start-Sleep -Seconds 1 + } +} + +function Start-BackgroundService([string]$Name, [string]$WorkDir, [string]$Command, [string]$LogFile) { + Write-Host "Starting $Name ..." + Start-Process -FilePath "cmd.exe" -ArgumentList @("/c", "$Command > `"$LogFile`" 2>&1") ` + -WorkingDirectory $WorkDir -WindowStyle Hidden +} + +Set-Location $Root + +Import-DotEnvFile (Join-Path $Root ".env") +Import-DotEnvFile (Join-Path $Root "config\z-ecosystem.local.env") + +$env:CHAIN_900002_RPC_URL = if ($env:CHAIN_900002_RPC_URL) { $env:CHAIN_900002_RPC_URL } else { "http://127.0.0.1:8546" } +$env:SETTLEMENT_MIDDLEWARE_URL = if ($env:SETTLEMENT_MIDDLEWARE_URL) { $env:SETTLEMENT_MIDDLEWARE_URL } else { "http://127.0.0.1:3011" } +$env:SETTLEMENT_URL = $env:SETTLEMENT_MIDDLEWARE_URL +$env:TOKEN_AGGREGATION_URL = "http://127.0.0.1:3000" + +if (-not $env:SETTLEMENT_MIDDLEWARE_CONFIG) { + $env:SETTLEMENT_MIDDLEWARE_CONFIG = Join-Path $Root "config\settlement-middleware.production.v1.json" +} +if (-not $env:OMNL_SUPPORTED_CHAINS_CONFIG) { + $env:OMNL_SUPPORTED_CHAINS_CONFIG = Join-Path $Root "config\omnl-supported-chains.v1.json" +} + +if (-not (Test-ZChainRpc)) { + Write-Host "Z Chain RPC not running - starting complete-z-chain ..." + & "$PSScriptRoot\complete-z-chain.ps1" +} + +Write-Host "Building packages (settlement-core) ..." +foreach ($pkg in @("packages\integration-foundation", "packages\settlement-core")) { + $pkgDir = Join-Path $Root $pkg + if (Test-Path $pkgDir) { + Set-Location $pkgDir + if (-not (Test-Path node_modules)) { npm install --legacy-peer-deps 2>&1 | Out-Null } + npm run build + if ($LASTEXITCODE -ne 0) { throw "$pkg build failed" } + } +} + +Write-Host "Building settlement-middleware ..." +Set-Location "$Root\services\settlement-middleware" +if (-not (Test-Path node_modules)) { npm install --legacy-peer-deps 2>&1 | Out-Null } +npm run build +if ($LASTEXITCODE -ne 0) { throw "settlement-middleware build failed" } + +Write-Host "Building token-aggregation ..." +Set-Location "$Root\services\token-aggregation" +if (-not (Test-Path node_modules)) { npm install --legacy-peer-deps 2>&1 | Out-Null } +npm run build +if ($LASTEXITCODE -ne 0) { throw "token-aggregation build failed" } + +Write-Host "Building frontend-dapp (Z Wallet routes) ..." +Set-Location "$Root\frontend-dapp" +if (-not (Test-Path node_modules)) { npm install --legacy-peer-deps 2>&1 | Out-Null } +npm run build +if ($LASTEXITCODE -ne 0) { throw "frontend build failed" } + +Stop-Port 3011 +Stop-Port 3000 +Stop-Port 3002 + +$settlementLog = Join-Path $LogDir "settlement-middleware.log" +$tokenLog = Join-Path $LogDir "token-aggregation.log" +$portalLog = Join-Path $LogDir "portal-serve.log" + +Start-BackgroundService "settlement-middleware on :3011" "$Root\services\settlement-middleware" "npm start" $settlementLog +Start-Sleep -Seconds 3 + +Start-BackgroundService "token-aggregation on :3000" "$Root\services\token-aggregation" "npm start" $tokenLog +Start-Sleep -Seconds 4 + +$env:SETTLEMENT_URL = "http://127.0.0.1:3011" +Start-BackgroundService "portal on :3002" "$Root\frontend-dapp" "npm run serve:portal" $portalLog +Start-Sleep -Seconds 5 + +$llmStatus = "rule-based" +try { + $health = Invoke-RestMethod -Uri "http://localhost:3000/api/v1/z-bot/health" -TimeoutSec 5 + $llmStatus = $health.llm.provider +} catch { } + +$settlementStatus = "unknown" +try { + $settlementStatus = (Invoke-RestMethod -Uri "http://localhost:3011/api/v1/settlement/health" -TimeoutSec 5).status +} catch { $settlementStatus = "unreachable" } + +Write-Host "" +Write-Host "Z Ecosystem deployed locally:" +Write-Host " Hub: http://localhost:3002/z" +Write-Host " Wallet: http://localhost:3002/z-wallet" +Write-Host " Z Bot API: http://localhost:3000/api/v1/z-bot/health ($llmStatus)" +Write-Host " Settlement: http://localhost:3011/api/v1/settlement/health ($settlementStatus)" +Write-Host " Z Chain RPC: $($env:CHAIN_900002_RPC_URL)" +Write-Host " Logs: $LogDir" +if ($llmStatus -eq "rule-based") { + Write-Host "" + Write-Host "Azure OpenAI: copy config/z-ecosystem.local.env.example to config/z-ecosystem.local.env and set AZURE_OPENAI_* then redeploy." +} diff --git a/scripts/deployment/go-live-z-ecosystem.ps1 b/scripts/deployment/go-live-z-ecosystem.ps1 new file mode 100644 index 0000000..01dd70a --- /dev/null +++ b/scripts/deployment/go-live-z-ecosystem.ps1 @@ -0,0 +1,136 @@ +# Go live — Z Ecosystem (production build + tunnel + deploy bundle) +$ErrorActionPreference = "Stop" +$Root = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path +$LogDir = Join-Path $Root "logs" +$BundleDir = Join-Path $Root "dist\go-live" +$BundleTar = Join-Path $BundleDir "z-ecosystem-portal.tar.gz" +New-Item -ItemType Directory -Force -Path $LogDir, $BundleDir | Out-Null + +function Import-DotEnvFile([string]$Path) { + if (-not (Test-Path $Path)) { return } + Get-Content $Path | ForEach-Object { + $line = $_.Trim() + if ($line -eq "" -or $line.StartsWith("#")) { return } + if ($line -match '^\s*([^=]+)=(.*)$') { + Set-Item -Path "env:$($matches[1].Trim())" -Value $matches[2].Trim().Trim('"').Trim("'") + } + } +} + +function Stop-Port($port) { + $conn = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($conn -and $conn.OwningProcess) { + Stop-Process -Id $conn.OwningProcess -Force -ErrorAction SilentlyContinue + Start-Sleep -Seconds 1 + } +} + +Set-Location $Root +Import-DotEnvFile (Join-Path $Root ".env") +Import-DotEnvFile (Join-Path $Root "config\z-ecosystem.local.env") + +# Production frontend env (same-origin paths on digital.omdnl.org) +$env:VITE_TOKEN_AGGREGATION_URL = "/api/v1" +$env:VITE_SETTLEMENT_MIDDLEWARE_URL = "/settlement" +$env:VITE_DBIS_EXCHANGE_URL = "/exchange" +$env:VITE_ENABLE_Z_WALLET = "true" +$env:VITE_RPC_URL_900002 = if ($env:VITE_RPC_URL_900002) { $env:VITE_RPC_URL_900002 } else { "https://rpc.z.omdnl.org" } +$env:VITE_Z_CHAIN_EXPLORER_URL = if ($env:VITE_Z_CHAIN_EXPLORER_URL) { $env:VITE_Z_CHAIN_EXPLORER_URL } else { "https://explorer.z.omdnl.org" } +Remove-Item Env:VITE_OMNL_API_KEY -ErrorAction SilentlyContinue + +Write-Host "=== 1/4 Local stack (Z Chain + APIs + portal) ===" +& "$PSScriptRoot\deploy-z-ecosystem.ps1" + +Write-Host "" +Write-Host "=== 2/4 Production frontend build ===" +Set-Location "$Root\frontend-dapp" +npm run build +if ($LASTEXITCODE -ne 0) { throw "production frontend build failed" } + +Write-Host "" +Write-Host "=== 3/4 Stage deploy bundle ===" +$stageRoot = Join-Path $BundleDir "stage" +if (Test-Path $stageRoot) { Remove-Item -Recurse -Force $stageRoot } +New-Item -ItemType Directory -Force -Path $stageRoot | Out-Null + +$copyPaths = @( + "config", + "frontend-dapp\dist", + "packages\integration-foundation\package.json", + "packages\integration-foundation\dist", + "packages\settlement-core\package.json", + "packages\settlement-core\dist", + "services\token-aggregation\dist", + "services\token-aggregation\package.json", + "services\token-aggregation\package-lock.json", + "services\settlement-middleware\dist", + "services\settlement-middleware\package.json", + "services\settlement-middleware\package-lock.json", + "scripts\deployment\start-omnl-banking-portal.sh", + "scripts\deployment\deploy-z-ecosystem-production.sh", + "deploy\nginx\omnl-lxc-portal.conf", + "deploy\nginx\omnl-portal-auth-current.conf.inc" +) + +foreach ($rel in $copyPaths) { + $src = Join-Path $Root $rel + if (-not (Test-Path $src)) { + Write-Host " skip missing: $rel" + continue + } + $dst = Join-Path $stageRoot $rel + New-Item -ItemType Directory -Force -Path (Split-Path $dst -Parent) | Out-Null + Copy-Item -Path $src -Destination $dst -Recurse -Force +} + +if (Get-Command tar -ErrorAction SilentlyContinue) { + if (Test-Path $BundleTar) { Remove-Item -Force $BundleTar } + tar -czf $BundleTar -C $stageRoot . + Write-Host " bundle: $BundleTar" +} else { + Write-Host " tar not found - bundle folder: $stageRoot" +} + +Write-Host "" +Write-Host "=== 4/4 Public tunnel (cloudflared) ===" +Stop-Port 4040 +$tunnelLog = Join-Path $LogDir "cloudflared-z-ecosystem.log" +$cf = "C:\Program Files (x86)\cloudflared\cloudflared.exe" +if (-not (Test-Path $cf)) { $cf = "cloudflared" } + +$tunnelErr = Join-Path $LogDir "cloudflared-z-ecosystem.err.log" +Start-Process -FilePath "cmd.exe" -ArgumentList @("/c", "`"$cf`" tunnel --url http://localhost:3002 --metrics localhost:4040 > `"$tunnelLog`" 2> `"$tunnelErr`"") ` + -WindowStyle Hidden + +$tunnelUrl = $null +for ($i = 0; $i -lt 30; $i++) { + Start-Sleep -Seconds 1 + if (Test-Path $tunnelLog) { + $log = Get-Content $tunnelLog -Raw -ErrorAction SilentlyContinue + if ($log -match '(https://[a-z0-9-]+\.trycloudflare\.com)') { + $tunnelUrl = $matches[1] + break + } + } +} + +Write-Host "" +Write-Host "Z Ecosystem GO LIVE" +Write-Host "===================" +if ($tunnelUrl) { + Write-Host " Public tunnel: $tunnelUrl/z" + Write-Host " Z Wallet: $tunnelUrl/z-wallet" + Write-Host " Z Bot health: $tunnelUrl/api/v1/z-bot/health" +} else { + Write-Host " Tunnel starting - see $tunnelLog" + Write-Host " Local: http://localhost:3002/z" +} +Write-Host "" +Write-Host "Production origin digital.omdnl.org - run on dev-bis-ali:" +Write-Host ' cd ~/smom-dbis-138; git pull; bash scripts/deployment/deploy-z-ecosystem-production.sh' +Write-Host ' # or push bundle to LXC:' +Write-Host ' Z_ECOSYSTEM_PUSH_PCT=1 bash scripts/deployment/deploy-z-ecosystem-production.sh' +Write-Host "" +Write-Host "DNS still required for Z Chain RPC:" +Write-Host ' rpc.z.omdnl.org -> Besu host port 8546' +Write-Host ' explorer.z.omdnl.org -> explorer optional' diff --git a/scripts/deployment/sync-z-chain-deployed-addresses.mjs b/scripts/deployment/sync-z-chain-deployed-addresses.mjs new file mode 100644 index 0000000..196d973 --- /dev/null +++ b/scripts/deployment/sync-z-chain-deployed-addresses.mjs @@ -0,0 +1,69 @@ +#!/usr/bin/env node +/** + * Sync forge broadcast output into z-chain-deployed.v1.json and hybx cross-chain lines. + * Usage: node scripts/deployment/sync-z-chain-deployed-addresses.mjs [broadcast-run.json] + */ +import { readFileSync, writeFileSync, existsSync } from 'fs'; +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = resolve(__dirname, '../..'); + +const broadcastPath = + process.argv[2] || + resolve(root, 'broadcast/DeployZChainEmoney.s.sol/900002/run-latest.json'); + +if (!existsSync(broadcastPath)) { + console.error(`Broadcast file not found: ${broadcastPath}`); + console.error('Deploy first: bash scripts/deployment/deploy-z-chain-contracts.sh'); + process.exit(1); +} + +const broadcast = JSON.parse(readFileSync(broadcastPath, 'utf8')); +const txs = broadcast.transactions || []; +let cUsdc = null; +let registry = null; + +for (const tx of txs) { + const name = tx.contractName; + const addr = tx.contractAddress; + if (!addr) continue; + if (name === 'ZChainUSDC') cUsdc = addr; + if (name === 'AccountWalletRegistry' || name === 'AccountWalletRegistryZ') registry = addr; +} + +if (!cUsdc || !registry) { + console.error('Could not find ZChainUSDC and AccountWalletRegistry in broadcast'); + process.exit(1); +} + +const deployedPath = resolve(root, 'config/z-chain-deployed.v1.json'); +const deployed = JSON.parse(readFileSync(deployedPath, 'utf8')); +deployed.updated = new Date().toISOString().slice(0, 10); +deployed.status = 'active'; +deployed.contracts.cUSDC = cUsdc; +deployed.contracts.accountWalletRegistry = registry; +writeFileSync(deployedPath, `${JSON.stringify(deployed, null, 2)}\n`); + +const integrationPath = resolve(root, 'config/z-chain-integration.v1.json'); +const integration = JSON.parse(readFileSync(integrationPath, 'utf8')); +integration.contracts.cUSDC = cUsdc; +integration.contracts.accountWalletRegistry = registry; +writeFileSync(integrationPath, `${JSON.stringify(integration, null, 2)}\n`); + +const hybxPath = resolve(root, 'config/hybx-omnl-cross-chain-lines.json'); +const hybx = JSON.parse(readFileSync(hybxPath, 'utf8')); +for (const line of hybx.lines || []) { + if (line.chains?.['900002']) { + line.chains['900002'].tokenM1 = cUsdc; + } +} +writeFileSync(hybxPath, `${JSON.stringify(hybx, null, 2)}\n`); + +console.log('Updated:'); +console.log(' cUSDC:', cUsdc); +console.log(' AccountWalletRegistry:', registry); +console.log(' config/z-chain-deployed.v1.json'); +console.log(' config/z-chain-integration.v1.json'); +console.log(' config/hybx-omnl-cross-chain-lines.json (chains.900002.tokenM1)'); diff --git a/services/token-aggregation/src/api/routes/z-bot.ts b/services/token-aggregation/src/api/routes/z-bot.ts new file mode 100644 index 0000000..513a315 --- /dev/null +++ b/services/token-aggregation/src/api/routes/z-bot.ts @@ -0,0 +1,35 @@ +import { Router, type Request, type Response } from 'express'; +import { zBotReply, zBotLlmStatus } from '../../services/z-bot-context'; + +const router = Router(); + +router.get('/z-bot/health', (_req: Request, res: Response) => { + const llm = zBotLlmStatus(); + res.json({ + service: 'z-bot', + status: 'ok', + llm, + fallback: 'rule-based when Azure OpenAI is not configured', + }); +}); + +router.post('/z-bot/chat', async (req: Request, res: Response) => { + const { message, walletAddress, chainId } = req.body as { + message?: string; + sessionId?: string; + walletAddress?: string; + chainId?: number; + }; + if (!message?.trim()) { + res.status(400).json({ error: 'message is required' }); + return; + } + try { + const reply = await zBotReply(message.trim(), walletAddress, chainId); + res.json({ reply, sessionId: req.body.sessionId, llm: zBotLlmStatus().provider }); + } catch (e) { + res.status(500).json({ error: e instanceof Error ? e.message : String(e) }); + } +}); + +export default router; diff --git a/services/token-aggregation/src/api/routes/z-wallet.ts b/services/token-aggregation/src/api/routes/z-wallet.ts new file mode 100644 index 0000000..190164f --- /dev/null +++ b/services/token-aggregation/src/api/routes/z-wallet.ts @@ -0,0 +1,43 @@ +import { Router, type Request, type Response } from 'express'; +import { loadZChainIntegration, fetchSettlementStatus } from '../../services/z-bot-context'; + +const router = Router(); + +router.get('/z-wallet/corridors', (_req: Request, res: Response) => { + try { + const integration = loadZChainIntegration() as { + internationalCorridors?: unknown[]; + brand?: unknown; + chain?: unknown; + }; + res.json({ + brand: integration.brand, + chain: integration.chain, + corridors: integration.internationalCorridors ?? [], + }); + } catch (e) { + res.status(500).json({ error: e instanceof Error ? e.message : String(e) }); + } +}); + +router.get('/z-wallet/status/:ref', async (req: Request, res: Response) => { + const ref = String(req.params.ref); + if (!ref) { + res.status(400).json({ error: 'Missing settlement reference' }); + return; + } + const rec = await fetchSettlementStatus(ref); + if (!rec) { + res.status(404).json({ error: 'Settlement not found', settlementId: ref }); + return; + } + res.json({ + settlementId: rec.settlementId ?? ref, + phase: rec.phase, + status: rec.phase ?? rec.status, + chainTxHash: rec.chainTxHash, + error: rec.error, + }); +}); + +export default router; diff --git a/services/token-aggregation/src/api/server.ts b/services/token-aggregation/src/api/server.ts index af603ff..30713df 100644 --- a/services/token-aggregation/src/api/server.ts +++ b/services/token-aggregation/src/api/server.ts @@ -26,6 +26,8 @@ import omnlIpsasRoutes from './routes/omnl-ipsas'; import omnlPayoutJournalRoutes from './routes/omnl-payout-journal-routes'; import omnlComplianceRoutes from './routes/omnl-compliance-routes'; import omnlTerminalRoutes from './routes/omnl-terminal-routes'; +import zWalletRoutes from './routes/z-wallet'; +import zBotRoutes from './routes/z-bot'; import checkpointRoutes from './routes/checkpoint'; import reserveCapacityRoutes from './routes/reserve-capacity'; import hoLiquidityRoutes from './routes/ho-liquidity-routes'; @@ -305,6 +307,8 @@ export class ApiServer { this.app.use('/api/v1', omnlRoutes); this.app.use('/api/v1', omnlIpsasRoutes); this.app.use('/api/v1', omnlPayoutJournalRoutes); + this.app.use('/api/v1', zWalletRoutes); + this.app.use('/api/v1', zBotRoutes); this.app.use('/api/v2', plannerV2Routes); // Admin routes (stricter rate limit) diff --git a/services/token-aggregation/src/services/z-bot-context.ts b/services/token-aggregation/src/services/z-bot-context.ts new file mode 100644 index 0000000..dae6c79 --- /dev/null +++ b/services/token-aggregation/src/services/z-bot-context.ts @@ -0,0 +1,200 @@ +import { existsSync, readFileSync } from 'fs'; +import { resolve } from 'path'; + +function repoRoot(): string { + return resolve(__dirname, '../../../../..'); +} + +export function loadZChainIntegration(): Record { + const p = resolve(repoRoot(), 'config/z-chain-integration.v1.json'); + if (!existsSync(p)) { + throw new Error('config/z-chain-integration.v1.json not found'); + } + return JSON.parse(readFileSync(p, 'utf8')) as Record; +} + +export function loadZEcosystem(): Record { + const p = resolve(repoRoot(), 'config/z-ecosystem.v1.json'); + if (!existsSync(p)) { + throw new Error('config/z-ecosystem.v1.json not found'); + } + return JSON.parse(readFileSync(p, 'utf8')) as Record; +} + +function rpcUrl(chainId: number): string | undefined { + if (chainId === 138) return process.env.RPC_URL_138 || process.env.RPC_URL; + return process.env[`CHAIN_${chainId}_RPC_URL`]; +} + +export async function fetchWalletBalance( + walletAddress: string, + chainId: number, +): Promise { + const rpc = rpcUrl(chainId); + if (!rpc) return null; + try { + const res = await fetch(rpc, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'eth_getBalance', + params: [walletAddress, 'latest'], + }), + }); + const json = (await res.json()) as { result?: string }; + if (!json.result) return null; + const wei = BigInt(json.result); + const eth = Number(wei) / 1e18; + return eth.toFixed(6); + } catch { + return null; + } +} + +export async function fetchSettlementStatus(ref: string): Promise | null> { + const base = + process.env.SETTLEMENT_MIDDLEWARE_URL?.replace(/\/$/, '') || + 'http://127.0.0.1:3011'; + const path = base.includes('/settlement') + ? `${base}/api/v1/settlement/settlements/${encodeURIComponent(ref)}` + : `${base}/api/v1/settlement/settlements/${encodeURIComponent(ref)}`; + const headers: Record = {}; + const key = process.env.OMNL_API_KEY?.trim(); + if (key) headers.Authorization = `Bearer ${key}`; + try { + const res = await fetch(path, { headers }); + if (!res.ok) return null; + return (await res.json()) as Record; + } catch { + return null; + } +} + +type ChatContext = { + walletAddress?: string; + chainId?: number; + balance?: string | null; + corridors: unknown[]; + ecosystemRoutes: Record; +}; + +export async function buildZBotContext( + walletAddress?: string, + chainId?: number, +): Promise { + const integration = loadZChainIntegration() as { + internationalCorridors?: unknown[]; + brand?: { wallet?: string; bank?: string }; + }; + const ecosystem = loadZEcosystem() as { routes?: Record }; + const cid = chainId ?? 900002; + const balance = walletAddress ? await fetchWalletBalance(walletAddress, cid) : null; + return { + walletAddress, + chainId: cid, + balance, + corridors: integration.internationalCorridors ?? [], + ecosystemRoutes: ecosystem.routes ?? {}, + }; +} + +function ruleBasedReply(message: string, ctx: ChatContext): string { + const lower = message.toLowerCase(); + if (lower.includes('balance')) { + if (!ctx.walletAddress) { + return 'Connect your International Z Wallet first, then ask again for your balance.'; + } + if (ctx.balance != null) { + return `Your Z Chain native balance is approximately ${ctx.balance} Z. For token balances, open the wallet panel.`; + } + return 'I could not read your on-chain balance right now. Check RPC configuration (CHAIN_900002_RPC_URL).'; + } + if (lower.includes('iraq') || lower.includes('iran') || lower.includes('corridor') || lower.includes('send')) { + const lines = (ctx.corridors as { id: string; label: string; rails: string[] }[]) + .map((c) => `• ${c.label} (${c.id}) — rails: ${c.rails.join(', ')}`) + .join('\n'); + return `International corridors:\n${lines}\n\nUse the International transfer panel in the wallet to submit a payment. I cannot sign transactions.`; + } + if (lower.includes('transfer') && (lower.includes('status') || lower.includes('where'))) { + return 'Enter your settlement reference in the transfer status card after submitting, or paste the reference here and I will look it up on the next message with "status REF".'; + } + const statusMatch = lower.match(/status\s+([a-f0-9-]{8,})/i); + if (statusMatch) { + return `Check transfer status in the wallet UI for reference ${statusMatch[1]}, or call GET /api/v1/z-wallet/status/${statusMatch[1]}.`; + } + if (lower.includes('bank') || lower.includes('z online')) { + return `Z Online Bank is at ${ctx.ecosystemRoutes.onlineBank ?? '/central-bank'} for M0–M4 settlement.`; + } + return `I can help with balance, international corridors (Iraq, Iran, EU, global), transfer status, and links to Z Online Bank. Try: "What is my balance?" or "How do I send to Iraq?"`; +} + +export function zBotLlmStatus(): { + provider: 'azure-openai' | 'rule-based'; + configured: boolean; + deployment?: string; + apiVersion: string; +} { + const endpoint = process.env.AZURE_OPENAI_ENDPOINT?.trim(); + const key = process.env.AZURE_OPENAI_API_KEY?.trim(); + const deployment = process.env.AZURE_OPENAI_DEPLOYMENT?.trim(); + const apiVersion = process.env.AZURE_OPENAI_API_VERSION?.trim() || '2024-02-15-preview'; + const configured = Boolean(endpoint && key && deployment); + return { + provider: configured ? 'azure-openai' : 'rule-based', + configured, + deployment: deployment || undefined, + apiVersion, + }; +} + +export async function azureOpenAiReply( + message: string, + ctx: ChatContext, +): Promise { + const endpoint = process.env.AZURE_OPENAI_ENDPOINT?.replace(/\/$/, ''); + const key = process.env.AZURE_OPENAI_API_KEY; + const deployment = process.env.AZURE_OPENAI_DEPLOYMENT; + if (!endpoint || !key || !deployment) return null; + + const system = `You are Z Bot, assistant for International Z Wallet on Z Chain (chainId ${ctx.chainId}). +Answer briefly. Never offer to sign transactions. Context: ${JSON.stringify(ctx)}`; + const apiVersion = process.env.AZURE_OPENAI_API_VERSION?.trim() || '2024-02-15-preview'; + const url = `${endpoint}/openai/deployments/${deployment}/chat/completions?api-version=${apiVersion}`; + try { + const res = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'api-key': key, + }, + body: JSON.stringify({ + messages: [ + { role: 'system', content: system }, + { role: 'user', content: message }, + ], + max_tokens: 400, + temperature: 0.3, + }), + }); + if (!res.ok) return null; + const data = (await res.json()) as { + choices?: { message?: { content?: string } }[]; + }; + return data.choices?.[0]?.message?.content?.trim() ?? null; + } catch { + return null; + } +} + +export async function zBotReply( + message: string, + walletAddress?: string, + chainId?: number, +): Promise { + const ctx = await buildZBotContext(walletAddress, chainId); + const llm = await azureOpenAiReply(message, ctx); + if (llm) return llm; + return ruleBasedReply(message, ctx); +}