From 0e809c3a74e3210a18e7caab1779ed34af83d665 Mon Sep 17 00:00:00 2001 From: Kurdistan Tech Ministry Date: Mon, 16 Feb 2026 08:18:26 +0300 Subject: [PATCH] sec: remove hardcoded mnemonics, add mainnet tools and subxt examples - Replace all hardcoded wallet mnemonics with env variable reads - Add comprehensive e2e test suite (tools/e2e-test/) - Add zagros validator management tools - Add subxt examples for mainnet operations - Update CRITICAL_STATE with zagros testnet and mainnet status - Fix people chain spec ID and chainspec build script --- .../src/chain_spec/people.rs | 2 +- tools/build-mainnet-chainspec.sh | 6 +- tools/e2e-test/comprehensive_e2e_test.py | 1001 +++++++++++++++++ tools/zagros-reduce-validators.mjs | 159 +++ .../subxt/examples/asset_hub_nom_pools.rs | 261 +++++ .../subxt/examples/bond_extra_validators.rs | 207 ++++ .../subxt/examples/create_nomination_pools.rs | 432 +++++++ .../subxt/examples/fix_force_era.rs | 131 +++ .../subxt/examples/mint_welati_tiki.rs | 321 ++++++ .../subxt/examples/remark_mainnet.rs | 71 ++ .../subxt/examples/send_staking_details.rs | 327 ++++++ .../subxt/examples/set_pool_metadata.rs | 191 ++++ .../subxt/examples/transfer_to_validators.rs | 209 ++++ .../subxt/examples/validator_welati_batch.rs | 402 +++++++ .../subxt/examples/welati_citizenship.rs | 316 ++++++ .../subxt/examples/zagros_deregister.rs | 287 +++++ .../subxt/examples/zagros_diagnose.rs | 253 +++++ .../subxt/examples/zagros_force_new_era.rs | 43 + .../examples/zagros_reduce_validators.rs | 84 ++ .../subxt/examples/zagros_set_retire.rs | 275 +++++ .../subxt/examples/zagros_sudo.rs | 184 +++ .../subxt/examples/zagros_upgrade.rs | 293 +++++ 22 files changed, 5451 insertions(+), 4 deletions(-) create mode 100644 tools/e2e-test/comprehensive_e2e_test.py create mode 100644 tools/zagros-reduce-validators.mjs create mode 100644 vendor/pezkuwi-subxt/subxt/examples/asset_hub_nom_pools.rs create mode 100644 vendor/pezkuwi-subxt/subxt/examples/bond_extra_validators.rs create mode 100644 vendor/pezkuwi-subxt/subxt/examples/create_nomination_pools.rs create mode 100644 vendor/pezkuwi-subxt/subxt/examples/fix_force_era.rs create mode 100644 vendor/pezkuwi-subxt/subxt/examples/mint_welati_tiki.rs create mode 100644 vendor/pezkuwi-subxt/subxt/examples/remark_mainnet.rs create mode 100644 vendor/pezkuwi-subxt/subxt/examples/send_staking_details.rs create mode 100644 vendor/pezkuwi-subxt/subxt/examples/set_pool_metadata.rs create mode 100644 vendor/pezkuwi-subxt/subxt/examples/transfer_to_validators.rs create mode 100644 vendor/pezkuwi-subxt/subxt/examples/validator_welati_batch.rs create mode 100644 vendor/pezkuwi-subxt/subxt/examples/welati_citizenship.rs create mode 100644 vendor/pezkuwi-subxt/subxt/examples/zagros_deregister.rs create mode 100644 vendor/pezkuwi-subxt/subxt/examples/zagros_diagnose.rs create mode 100644 vendor/pezkuwi-subxt/subxt/examples/zagros_force_new_era.rs create mode 100644 vendor/pezkuwi-subxt/subxt/examples/zagros_reduce_validators.rs create mode 100644 vendor/pezkuwi-subxt/subxt/examples/zagros_set_retire.rs create mode 100644 vendor/pezkuwi-subxt/subxt/examples/zagros_sudo.rs create mode 100644 vendor/pezkuwi-subxt/subxt/examples/zagros_upgrade.rs diff --git a/pezcumulus/pezkuwi-teyrchain/src/chain_spec/people.rs b/pezcumulus/pezkuwi-teyrchain/src/chain_spec/people.rs index 5dc67e02..f9aa2afb 100644 --- a/pezcumulus/pezkuwi-teyrchain/src/chain_spec/people.rs +++ b/pezcumulus/pezkuwi-teyrchain/src/chain_spec/people.rs @@ -143,7 +143,7 @@ pub mod pezkuwichain { Extensions::new("pezkuwichain-mainnet".to_string(), 1004), ) .with_name("Pezkuwichain People") - .with_id(super::ensure_id(PEOPLE_PEZKUWICHAIN_GENESIS).expect("invalid id")) + .with_id("people-pezkuwichain") .with_chain_type(ChainType::Live) .with_genesis_config_preset_name("genesis") .with_properties(properties) diff --git a/tools/build-mainnet-chainspec.sh b/tools/build-mainnet-chainspec.sh index 82285cb8..3ef1f02c 100755 --- a/tools/build-mainnet-chainspec.sh +++ b/tools/build-mainnet-chainspec.sh @@ -63,7 +63,7 @@ echo -e "${GREEN} -> relay-plain.json created${NC}" # ============================================================================= echo -e "${YELLOW}[2/6] Generating Asset Hub chain spec...${NC}" $TEYRCHAIN_BIN build-spec \ - --chain asset-hub-pezkuwichain \ + --chain asset-hub-pezkuwichain-genesis \ --disable-default-bootnode \ 2>/dev/null > "$OUTPUT_DIR/asset-hub-plain.json" echo -e "${GREEN} -> asset-hub-plain.json created${NC}" @@ -73,7 +73,7 @@ echo -e "${GREEN} -> asset-hub-plain.json created${NC}" # ============================================================================= echo -e "${YELLOW}[3/6] Generating People Chain chain spec...${NC}" $TEYRCHAIN_BIN build-spec \ - --chain people-pezkuwichain \ + --chain people-pezkuwichain-genesis \ --disable-default-bootnode \ 2>/dev/null > "$OUTPUT_DIR/people-plain.json" echo -e "${GREEN} -> people-plain.json created${NC}" @@ -221,7 +221,7 @@ def verify_spec(path, name, expected_id): ok = True ok = verify_spec("$OUTPUT_DIR/relay-raw.json", "Relay Chain", "pezkuwichain_mainnet") and ok ok = verify_spec("$OUTPUT_DIR/asset-hub-raw.json", "Asset Hub", "asset-hub-pezkuwichain") and ok -ok = verify_spec("$OUTPUT_DIR/people-raw.json", "People Chain", "people-pezkuwichain") and ok +ok = verify_spec("$OUTPUT_DIR/people-raw.json", "People Chain", "people-pezkuwichain-genesis") and ok if not ok: sys.exit(1) diff --git a/tools/e2e-test/comprehensive_e2e_test.py b/tools/e2e-test/comprehensive_e2e_test.py new file mode 100644 index 00000000..954025b3 --- /dev/null +++ b/tools/e2e-test/comprehensive_e2e_test.py @@ -0,0 +1,1001 @@ +#!/usr/bin/env python3 +""" +Pezkuwi Mainnet - Comprehensive End-to-End Test Suite +===================================================== + +Tests all user-facing blockchain operations with real funds. +Creates a new test wallet, funds it, and runs through all scenarios. + +Chains: + - Relay Chain (HEZ, 12 decimals): ws://217.77.6.126:9944 + - Asset Hub (TYR, 12 decimals): ws://217.77.6.126:40944 + - People Chain: ws://217.77.6.126:41944 + +Old Test Wallet: set OLD_WALLET_MNEMONIC env var +New Test Wallet: set NEW_WALLET_MNEMONIC env var +""" + +import os +import sys +import time +import traceback +from substrateinterface import SubstrateInterface, Keypair +from substrateinterface.exceptions import SubstrateRequestException + +# ============================================================ +# CONFIGURATION +# ============================================================ + +RELAY_RPC = "ws://217.77.6.126:9944" +ASSET_HUB_RPC = "ws://217.77.6.126:40944" +PEOPLE_CHAIN_RPC = "ws://217.77.6.126:41944" + +OLD_WALLET_MNEMONIC = os.environ.get("OLD_WALLET_MNEMONIC", "") +NEW_WALLET_MNEMONIC = os.environ.get("NEW_WALLET_MNEMONIC", "") + +UNITS = 10**12 # 1 HEZ/TYR = 10^12 smallest units +TEST_AMOUNT = 100 * UNITS # 100 HEZ/TYR for tests + +# ============================================================ +# HELPERS +# ============================================================ + +class TestResult: + def __init__(self): + self.passed = [] + self.failed = [] + self.skipped = [] + + def log_pass(self, name, detail=""): + self.passed.append((name, detail)) + print(f" [PASS] {name}" + (f" - {detail}" if detail else "")) + + def log_fail(self, name, detail=""): + self.failed.append((name, detail)) + print(f" [FAIL] {name}" + (f" - {detail}" if detail else "")) + + def log_skip(self, name, detail=""): + self.skipped.append((name, detail)) + print(f" [SKIP] {name}" + (f" - {detail}" if detail else "")) + + def summary(self): + total = len(self.passed) + len(self.failed) + len(self.skipped) + print(f"\n{'='*60}") + print(f"TEST SONUCLARI: {len(self.passed)} passed, {len(self.failed)} failed, {len(self.skipped)} skipped / {total} total") + if self.failed: + print(f"\nBasarisiz testler:") + for name, detail in self.failed: + print(f" - {name}: {detail}") + print(f"{'='*60}") + return len(self.failed) == 0 + + +def connect(url, name=""): + """Connect to a chain RPC endpoint.""" + print(f" Connecting to {name} ({url})...") + ws = SubstrateInterface(url=url) + print(f" Connected: {ws.name} v{ws.version} (spec: {ws.runtime_version})") + return ws + + +def account_id(keypair): + """Get account ID as 0x-prefixed hex string for storage queries.""" + return "0x" + keypair.public_key.hex() + + +def get_balance(ws, keypair): + """Get free balance of an account. Pass Keypair object.""" + result = ws.query("System", "Account", [account_id(keypair)]) + if result is None: + return 0 + return result.value["data"]["free"] + + +def get_full_account(ws, keypair): + """Get full account info. Pass Keypair object.""" + result = ws.query("System", "Account", [account_id(keypair)]) + if result is None: + return {"nonce": 0, "data": {"free": 0, "reserved": 0, "frozen": 0}} + return result.value + + +def get_nonce(ws, keypair): + """Get account nonce via RPC (bypasses broken type encoding).""" + result = ws.rpc_request("system_accountNextIndex", [keypair.ss58_address]) + return result["result"] + + +def submit_extrinsic(ws, call, keypair, wait=True): + """Submit an extrinsic and return the receipt.""" + nonce = get_nonce(ws, keypair) + extrinsic = ws.create_signed_extrinsic(call=call, keypair=keypair, nonce=nonce) + receipt = ws.submit_extrinsic(extrinsic, wait_for_inclusion=wait) + if wait and not receipt.is_success: + raise Exception(f"Extrinsic failed: {receipt.error_message}") + return receipt + + +def fmt_balance(amount, symbol="HEZ"): + """Format a balance for display.""" + return f"{amount / UNITS:,.6f} {symbol}" + + +# ============================================================ +# TEST SCENARIOS +# ============================================================ + +def test_chain_connectivity(results): + """Test 1: Verify all three chains are reachable and healthy.""" + print("\n[TEST 1] Chain Connectivity & Health") + print("-" * 40) + + for name, url in [("Relay", RELAY_RPC), ("Asset Hub", ASSET_HUB_RPC), ("People Chain", PEOPLE_CHAIN_RPC)]: + try: + ws = connect(url, name) + health = ws.rpc_request("system_health", [])["result"] + header = ws.rpc_request("chain_getHeader", [])["result"] + block_num = int(header["number"], 16) + peers = health.get("peers", 0) + + if health.get("isSyncing", True): + results.log_fail(f"{name} connectivity", f"Chain is syncing") + elif block_num == 0: + results.log_fail(f"{name} connectivity", f"Stuck at block 0") + else: + results.log_pass(f"{name} connectivity", f"block #{block_num}, {peers} peers") + ws.close() + except Exception as e: + results.log_fail(f"{name} connectivity", str(e)) + + +def test_wallet_creation(results): + """Test 2: Create and verify new test wallet.""" + print("\n[TEST 2] Wallet Creation & Key Derivation") + print("-" * 40) + + try: + old_kp = Keypair.create_from_mnemonic(OLD_WALLET_MNEMONIC) + new_kp = Keypair.create_from_mnemonic(NEW_WALLET_MNEMONIC) + + print(f" Old wallet: {old_kp.ss58_address}") + print(f" New wallet: {new_kp.ss58_address}") + + # Verify key derivation is deterministic + old_kp2 = Keypair.create_from_mnemonic(OLD_WALLET_MNEMONIC) + assert old_kp.ss58_address == old_kp2.ss58_address + results.log_pass("Key derivation deterministic") + + # Verify addresses are different + assert old_kp.ss58_address != new_kp.ss58_address + results.log_pass("Wallets are distinct") + + except Exception as e: + results.log_fail("Wallet creation", str(e)) + + +def test_balance_query(results): + """Test 3: Query balances on all chains.""" + print("\n[TEST 3] Balance Queries") + print("-" * 40) + + old_kp = Keypair.create_from_mnemonic(OLD_WALLET_MNEMONIC) + new_kp = Keypair.create_from_mnemonic(NEW_WALLET_MNEMONIC) + + for name, url, symbol in [("Relay", RELAY_RPC, "HEZ"), ("Asset Hub", ASSET_HUB_RPC, "TYR"), ("People Chain", PEOPLE_CHAIN_RPC, "HEZ")]: + try: + ws = connect(url, name) + old_bal = get_balance(ws, old_kp) + new_bal = get_balance(ws, new_kp) + results.log_pass(f"{name} balance query", + f"Old: {fmt_balance(old_bal, symbol)}, New: {fmt_balance(new_bal, symbol)}") + ws.close() + except Exception as e: + results.log_fail(f"{name} balance query", str(e)) + + +def test_relay_transfer(results): + """Test 4: Transfer HEZ on relay chain (old -> new wallet).""" + print("\n[TEST 4] Relay Chain Transfer (Old -> New)") + print("-" * 40) + + old_kp = Keypair.create_from_mnemonic(OLD_WALLET_MNEMONIC) + new_kp = Keypair.create_from_mnemonic(NEW_WALLET_MNEMONIC) + + try: + ws = connect(RELAY_RPC, "Relay") + amount = 500 * UNITS # 500 HEZ + + before_old = get_balance(ws, old_kp) + before_new = get_balance(ws, new_kp) + print(f" Before - Old: {fmt_balance(before_old)}, New: {fmt_balance(before_new)}") + + call = ws.compose_call( + call_module="Balances", + call_function="transfer_keep_alive", + call_params={"dest": {"Id": account_id(new_kp)}, "value": amount} + ) + receipt = submit_extrinsic(ws, call, old_kp) + + after_old = get_balance(ws, old_kp) + after_new = get_balance(ws, new_kp) + print(f" After - Old: {fmt_balance(after_old)}, New: {fmt_balance(after_new)}") + + transferred = after_new - before_new + if transferred >= amount: + results.log_pass("Relay transfer", f"Sent {fmt_balance(amount)}, received {fmt_balance(transferred)}") + else: + results.log_fail("Relay transfer", f"Expected {amount}, got {transferred}") + + # Verify fee was deducted + fee = before_old - after_old - amount + if fee > 0: + results.log_pass("Transaction fee deducted", fmt_balance(fee)) + else: + results.log_fail("Transaction fee", f"Fee={fee}") + + ws.close() + except Exception as e: + results.log_fail("Relay transfer", str(e)) + traceback.print_exc() + + +def test_new_wallet_self_transfer(results): + """Test 5: New wallet sends back a small amount to verify signing works.""" + print("\n[TEST 5] New Wallet Self-Signing (send back)") + print("-" * 40) + + old_kp = Keypair.create_from_mnemonic(OLD_WALLET_MNEMONIC) + new_kp = Keypair.create_from_mnemonic(NEW_WALLET_MNEMONIC) + + try: + ws = connect(RELAY_RPC, "Relay") + amount = 1 * UNITS # 1 HEZ + + before_new = get_balance(ws, new_kp) + if before_new < amount * 2: + results.log_skip("New wallet self-transfer", "Insufficient balance") + ws.close() + return + + call = ws.compose_call( + call_module="Balances", + call_function="transfer_keep_alive", + call_params={"dest": {"Id": account_id(old_kp)}, "value": amount} + ) + receipt = submit_extrinsic(ws, call, new_kp) + results.log_pass("New wallet can sign & send", f"Sent {fmt_balance(amount)} back") + ws.close() + except Exception as e: + results.log_fail("New wallet self-transfer", str(e)) + traceback.print_exc() + + +def test_xcm_relay_to_asset_hub(results): + """Test 6: XCM transfer from Relay to Asset Hub.""" + print("\n[TEST 6] XCM Transfer: Relay -> Asset Hub") + print("-" * 40) + + new_kp = Keypair.create_from_mnemonic(NEW_WALLET_MNEMONIC) + + try: + ws_relay = connect(RELAY_RPC, "Relay") + ws_ah = connect(ASSET_HUB_RPC, "Asset Hub") + + amount = 50 * UNITS # 50 HEZ + + before_relay = get_balance(ws_relay, new_kp) + before_ah = get_balance(ws_ah, new_kp) + print(f" Before - Relay: {fmt_balance(before_relay)}, AH: {fmt_balance(before_ah, 'TYR')}") + + if before_relay < amount * 2: + results.log_skip("XCM Relay->AH", "Insufficient relay balance") + ws_relay.close() + ws_ah.close() + return + + # XCM teleport from relay to Asset Hub (para_id 1000) + dest = {"V4": {"parents": 0, "interior": {"X1": [{"Parachain": 1000}]}}} + beneficiary = {"V4": {"parents": 0, "interior": {"X1": [{"AccountId32": {"id": account_id(new_kp), "network": None}}]}}} + assets = {"V4": [{"id": {"parents": 0, "interior": "Here"}, "fun": {"Fungible": amount}}]} + + call = ws_relay.compose_call( + call_module="XcmPallet", + call_function="limited_teleport_assets", + call_params={ + "dest": dest, + "beneficiary": beneficiary, + "assets": assets, + "fee_asset_item": 0, + "weight_limit": "Unlimited", + } + ) + receipt = submit_extrinsic(ws_relay, call, new_kp) + results.log_pass("XCM teleport submitted", f"Block: {receipt.block_hash[:16]}...") + + # Wait for XCM to arrive on Asset Hub + print(" Waiting 30s for XCM arrival...") + time.sleep(30) + + after_ah = get_balance(ws_ah, new_kp) + received = after_ah - before_ah + print(f" After - AH: {fmt_balance(after_ah, 'TYR')} (received: {fmt_balance(received, 'TYR')})") + + if received > 0: + results.log_pass("XCM arrived on Asset Hub", fmt_balance(received, "TYR")) + else: + results.log_fail("XCM arrival", "No balance increase on Asset Hub after 30s") + + ws_relay.close() + ws_ah.close() + except Exception as e: + results.log_fail("XCM Relay->AH", str(e)) + traceback.print_exc() + + +def test_asset_hub_transfer(results): + """Test 7: Transfer TYR on Asset Hub.""" + print("\n[TEST 7] Asset Hub Transfer") + print("-" * 40) + + old_kp = Keypair.create_from_mnemonic(OLD_WALLET_MNEMONIC) + new_kp = Keypair.create_from_mnemonic(NEW_WALLET_MNEMONIC) + + try: + ws = connect(ASSET_HUB_RPC, "Asset Hub") + amount = 10 * UNITS # 10 TYR + + before_new = get_balance(ws, new_kp) + if before_new < amount * 2: + # Fund from old wallet + print(f" New wallet AH balance low ({fmt_balance(before_new, 'TYR')}), funding from old wallet...") + before_old = get_balance(ws, old_kp) + if before_old >= amount * 5: + call = ws.compose_call( + call_module="Balances", + call_function="transfer_keep_alive", + call_params={"dest": {"Id": account_id(new_kp)}, "value": amount * 3} + ) + submit_extrinsic(ws, call, old_kp) + before_new = get_balance(ws, new_kp) + print(f" Funded. New AH balance: {fmt_balance(before_new, 'TYR')}") + + # Now transfer from new to old + call = ws.compose_call( + call_module="Balances", + call_function="transfer_keep_alive", + call_params={"dest": {"Id": account_id(old_kp)}, "value": amount} + ) + receipt = submit_extrinsic(ws, call, new_kp) + results.log_pass("Asset Hub transfer", f"Sent {fmt_balance(amount, 'TYR')}") + ws.close() + except Exception as e: + results.log_fail("Asset Hub transfer", str(e)) + traceback.print_exc() + + +def test_staking_bond(results): + """Test 8: Bond tokens for staking on Asset Hub.""" + print("\n[TEST 8] Staking: Bond Tokens") + print("-" * 40) + + new_kp = Keypair.create_from_mnemonic(NEW_WALLET_MNEMONIC) + + try: + ws = connect(ASSET_HUB_RPC, "Asset Hub") + bond_amount = 15 * UNITS # 15 TYR + + balance = get_balance(ws, new_kp) + if balance < bond_amount * 2: + results.log_skip("Staking bond", f"Insufficient balance: {fmt_balance(balance, 'TYR')}") + ws.close() + return + + # Check if already bonded + ledger = ws.query("Staking", "Ledger", [account_id(new_kp)]) + if ledger is not None and ledger.value is not None: + results.log_skip("Staking bond", "Already bonded, skipping") + ws.close() + return + + call = ws.compose_call( + call_module="Staking", + call_function="bond", + call_params={ + "value": bond_amount, + "payee": {"Staked": None}, + } + ) + receipt = submit_extrinsic(ws, call, new_kp) + + # Verify bonded + ledger = ws.query("Staking", "Ledger", [account_id(new_kp)]) + if ledger is not None and ledger.value is not None: + bonded = ledger.value.get("active", 0) + results.log_pass("Staking bonded", fmt_balance(bonded, "TYR")) + else: + results.log_fail("Staking bond", "Ledger not found after bonding") + + ws.close() + except Exception as e: + results.log_fail("Staking bond", str(e)) + traceback.print_exc() + + +def test_staking_nominate(results): + """Test 9: Nominate validators.""" + print("\n[TEST 9] Staking: Nominate Validators") + print("-" * 40) + + new_kp = Keypair.create_from_mnemonic(NEW_WALLET_MNEMONIC) + + try: + ws = connect(ASSET_HUB_RPC, "Asset Hub") + + # Check bonded + ledger = ws.query("Staking", "Ledger", [account_id(new_kp)]) + if ledger is None or ledger.value is None: + results.log_skip("Nominate", "Not bonded yet") + ws.close() + return + + # Get current validators + validators = ws.query("Session", "Validators", []) + if validators is None or not validators.value: + # Try getting validators from Staking pallet + results.log_skip("Nominate", "No validators found in Session") + ws.close() + return + + # Nominate first 3 validators (or all if less) + val_list = validators.value[:3] + print(f" Nominating {len(val_list)} validators...") + + call = ws.compose_call( + call_module="Staking", + call_function="nominate", + call_params={"targets": val_list} + ) + receipt = submit_extrinsic(ws, call, new_kp) + + # Verify nomination + nominators = ws.query("Staking", "Nominators", [account_id(new_kp)]) + if nominators is not None and nominators.value is not None: + targets = nominators.value.get("targets", []) + results.log_pass("Nomination", f"Nominated {len(targets)} validators") + else: + results.log_fail("Nomination", "Nominator record not found") + + ws.close() + except Exception as e: + results.log_fail("Nominate", str(e)) + traceback.print_exc() + + +def test_staking_bond_extra(results): + """Test 10: Bond extra tokens.""" + print("\n[TEST 10] Staking: Bond Extra") + print("-" * 40) + + new_kp = Keypair.create_from_mnemonic(NEW_WALLET_MNEMONIC) + + try: + ws = connect(ASSET_HUB_RPC, "Asset Hub") + extra_amount = 5 * UNITS # 5 TYR + + ledger = ws.query("Staking", "Ledger", [account_id(new_kp)]) + if ledger is None or ledger.value is None: + results.log_skip("Bond extra", "Not bonded") + ws.close() + return + + before_bonded = ledger.value.get("active", 0) + + call = ws.compose_call( + call_module="Staking", + call_function="bond_extra", + call_params={"max_additional": extra_amount} + ) + receipt = submit_extrinsic(ws, call, new_kp) + + ledger = ws.query("Staking", "Ledger", [account_id(new_kp)]) + after_bonded = ledger.value.get("active", 0) + added = after_bonded - before_bonded + + results.log_pass("Bond extra", f"Added {fmt_balance(added, 'TYR')}, total bonded: {fmt_balance(after_bonded, 'TYR')}") + ws.close() + except Exception as e: + results.log_fail("Bond extra", str(e)) + traceback.print_exc() + + +def test_staking_unbond(results): + """Test 11: Unbond tokens (partial).""" + print("\n[TEST 11] Staking: Unbond (partial)") + print("-" * 40) + + new_kp = Keypair.create_from_mnemonic(NEW_WALLET_MNEMONIC) + + try: + ws = connect(ASSET_HUB_RPC, "Asset Hub") + unbond_amount = 3 * UNITS # 3 TYR + + ledger = ws.query("Staking", "Ledger", [account_id(new_kp)]) + if ledger is None or ledger.value is None: + results.log_skip("Unbond", "Not bonded") + ws.close() + return + + before_bonded = ledger.value.get("active", 0) + if before_bonded < unbond_amount: + results.log_skip("Unbond", f"Bonded amount too low: {fmt_balance(before_bonded, 'TYR')}") + ws.close() + return + + call = ws.compose_call( + call_module="Staking", + call_function="unbond", + call_params={"value": unbond_amount} + ) + receipt = submit_extrinsic(ws, call, new_kp) + + ledger = ws.query("Staking", "Ledger", [account_id(new_kp)]) + after_bonded = ledger.value.get("active", 0) + unlocking = ledger.value.get("unlocking", []) + + results.log_pass("Unbond", f"Active: {fmt_balance(after_bonded, 'TYR')}, unlocking entries: {len(unlocking)}") + ws.close() + except Exception as e: + results.log_fail("Unbond", str(e)) + traceback.print_exc() + + +def test_staking_chill(results): + """Test 12: Chill (stop nominating).""" + print("\n[TEST 12] Staking: Chill") + print("-" * 40) + + new_kp = Keypair.create_from_mnemonic(NEW_WALLET_MNEMONIC) + + try: + ws = connect(ASSET_HUB_RPC, "Asset Hub") + + ledger = ws.query("Staking", "Ledger", [account_id(new_kp)]) + if ledger is None or ledger.value is None: + results.log_skip("Chill", "Not bonded") + ws.close() + return + + call = ws.compose_call( + call_module="Staking", + call_function="chill", + call_params={} + ) + receipt = submit_extrinsic(ws, call, new_kp) + + nominators = ws.query("Staking", "Nominators", [account_id(new_kp)]) + if nominators is None or nominators.value is None: + results.log_pass("Chill", "No longer nominating") + else: + results.log_fail("Chill", "Still nominating after chill") + + ws.close() + except Exception as e: + results.log_fail("Chill", str(e)) + traceback.print_exc() + + +def test_nomination_pool_join(results): + """Test 13: Join a nomination pool.""" + print("\n[TEST 13] Nomination Pool: Join") + print("-" * 40) + + new_kp = Keypair.create_from_mnemonic(NEW_WALLET_MNEMONIC) + + try: + ws = connect(ASSET_HUB_RPC, "Asset Hub") + join_amount = 10 * UNITS # 10 TYR + + # Check if any pools exist + pool_count = ws.query("NominationPools", "CounterForBondedPools", []) + if pool_count is None or pool_count.value == 0: + results.log_skip("Pool join", "No nomination pools exist") + ws.close() + return + + # Check if already a pool member + member = ws.query("NominationPools", "PoolMembers", [account_id(new_kp)]) + if member is not None and member.value is not None: + results.log_skip("Pool join", f"Already a pool member (pool {member.value.get('pool_id', '?')})") + ws.close() + return + + balance = get_balance(ws, new_kp) + if balance < join_amount * 2: + results.log_skip("Pool join", f"Insufficient balance: {fmt_balance(balance, 'TYR')}") + ws.close() + return + + # Join pool 1 (first pool) + call = ws.compose_call( + call_module="NominationPools", + call_function="join", + call_params={"amount": join_amount, "pool_id": 1} + ) + receipt = submit_extrinsic(ws, call, new_kp) + + member = ws.query("NominationPools", "PoolMembers", [account_id(new_kp)]) + if member is not None and member.value is not None: + pool_id = member.value.get("pool_id", "?") + points = member.value.get("points", 0) + results.log_pass("Pool join", f"Joined pool {pool_id}, points: {points}") + else: + results.log_fail("Pool join", "Member record not found after joining") + + ws.close() + except Exception as e: + results.log_fail("Pool join", str(e)) + traceback.print_exc() + + +def test_nomination_pool_bond_extra(results): + """Test 14: Add more funds to nomination pool.""" + print("\n[TEST 14] Nomination Pool: Bond Extra") + print("-" * 40) + + new_kp = Keypair.create_from_mnemonic(NEW_WALLET_MNEMONIC) + + try: + ws = connect(ASSET_HUB_RPC, "Asset Hub") + extra_amount = 5 * UNITS # 5 TYR + + member = ws.query("NominationPools", "PoolMembers", [account_id(new_kp)]) + if member is None or member.value is None: + results.log_skip("Pool bond extra", "Not a pool member") + ws.close() + return + + before_points = member.value.get("points", 0) + + call = ws.compose_call( + call_module="NominationPools", + call_function="bond_extra", + call_params={"extra": {"FreeBalance": extra_amount}} + ) + receipt = submit_extrinsic(ws, call, new_kp) + + member = ws.query("NominationPools", "PoolMembers", [account_id(new_kp)]) + after_points = member.value.get("points", 0) + added = after_points - before_points + + results.log_pass("Pool bond extra", f"Added {added} points, total: {after_points}") + ws.close() + except Exception as e: + results.log_fail("Pool bond extra", str(e)) + traceback.print_exc() + + +def test_nomination_pool_claim_rewards(results): + """Test 15: Claim pending rewards from nomination pool.""" + print("\n[TEST 15] Nomination Pool: Claim Rewards") + print("-" * 40) + + new_kp = Keypair.create_from_mnemonic(NEW_WALLET_MNEMONIC) + + try: + ws = connect(ASSET_HUB_RPC, "Asset Hub") + + member = ws.query("NominationPools", "PoolMembers", [account_id(new_kp)]) + if member is None or member.value is None: + results.log_skip("Pool claim rewards", "Not a pool member") + ws.close() + return + + before_balance = get_balance(ws, new_kp) + + call = ws.compose_call( + call_module="NominationPools", + call_function="claim_payout", + call_params={} + ) + receipt = submit_extrinsic(ws, call, new_kp) + + after_balance = get_balance(ws, new_kp) + reward = after_balance - before_balance + + if reward > 0: + results.log_pass("Pool claim rewards", f"Claimed {fmt_balance(reward, 'TYR')}") + else: + results.log_pass("Pool claim rewards", "No pending rewards (tx succeeded)") + + ws.close() + except Exception as e: + results.log_fail("Pool claim rewards", str(e)) + traceback.print_exc() + + +def test_nomination_pool_unbond(results): + """Test 16: Unbond from nomination pool (partial).""" + print("\n[TEST 16] Nomination Pool: Unbond (partial)") + print("-" * 40) + + new_kp = Keypair.create_from_mnemonic(NEW_WALLET_MNEMONIC) + + try: + ws = connect(ASSET_HUB_RPC, "Asset Hub") + unbond_amount = 3 * UNITS # 3 TYR + + member = ws.query("NominationPools", "PoolMembers", [account_id(new_kp)]) + if member is None or member.value is None: + results.log_skip("Pool unbond", "Not a pool member") + ws.close() + return + + before_points = member.value.get("points", 0) + if before_points < unbond_amount: + results.log_skip("Pool unbond", f"Points too low: {before_points}") + ws.close() + return + + call = ws.compose_call( + call_module="NominationPools", + call_function="unbond", + call_params={ + "member_account": account_id(new_kp), + "unbonding_points": unbond_amount, + } + ) + receipt = submit_extrinsic(ws, call, new_kp) + + member = ws.query("NominationPools", "PoolMembers", [account_id(new_kp)]) + after_points = member.value.get("points", 0) + unbonding = member.value.get("unbonding_eras", {}) + + results.log_pass("Pool unbond", f"Remaining points: {after_points}, unbonding eras: {len(unbonding)}") + ws.close() + except Exception as e: + results.log_fail("Pool unbond", str(e)) + traceback.print_exc() + + +def test_set_identity(results): + """Test 17: Set on-chain identity on People Chain.""" + print("\n[TEST 17] People Chain: Set Identity") + print("-" * 40) + + new_kp = Keypair.create_from_mnemonic(NEW_WALLET_MNEMONIC) + + try: + ws_people = connect(PEOPLE_CHAIN_RPC, "People Chain") + + # Check balance on People Chain + balance = get_balance(ws_people, new_kp) + if balance < 1 * UNITS: + results.log_skip("Set identity", f"Insufficient People Chain balance: {fmt_balance(balance)}") + ws_people.close() + return + + # Set identity + call = ws_people.compose_call( + call_module="Identity", + call_function="set_identity", + call_params={ + "info": { + "display": {"Raw": "E2E_Test_Wallet"}, + "legal": {"None": None}, + "web": {"None": None}, + "email": {"None": None}, + "pgp_fingerprint": None, + "image": {"None": None}, + "twitter": {"None": None}, + "github": {"None": None}, + "discord": {"None": None}, + } + } + ) + receipt = submit_extrinsic(ws_people, call, new_kp) + + # Verify identity + identity = ws_people.query("Identity", "IdentityOf", [account_id(new_kp)]) + if identity is not None and identity.value is not None: + results.log_pass("Set identity", "Identity set successfully") + else: + results.log_fail("Set identity", "Identity not found after setting") + + ws_people.close() + except Exception as e: + results.log_fail("Set identity", str(e)) + traceback.print_exc() + + +def test_clear_identity(results): + """Test 18: Clear on-chain identity.""" + print("\n[TEST 18] People Chain: Clear Identity") + print("-" * 40) + + new_kp = Keypair.create_from_mnemonic(NEW_WALLET_MNEMONIC) + + try: + ws_people = connect(PEOPLE_CHAIN_RPC, "People Chain") + + identity = ws_people.query("Identity", "IdentityOf", [account_id(new_kp)]) + if identity is None or identity.value is None: + results.log_skip("Clear identity", "No identity set") + ws_people.close() + return + + call = ws_people.compose_call( + call_module="Identity", + call_function="clear_identity", + call_params={} + ) + receipt = submit_extrinsic(ws_people, call, new_kp) + + identity = ws_people.query("Identity", "IdentityOf", [account_id(new_kp)]) + if identity is None or identity.value is None: + results.log_pass("Clear identity", "Identity cleared, deposit returned") + else: + results.log_fail("Clear identity", "Identity still exists after clearing") + + ws_people.close() + except Exception as e: + results.log_fail("Clear identity", str(e)) + traceback.print_exc() + + +def test_remark(results): + """Test 19: System.remark (minimal extrinsic).""" + print("\n[TEST 19] System Remark") + print("-" * 40) + + new_kp = Keypair.create_from_mnemonic(NEW_WALLET_MNEMONIC) + + try: + ws = connect(RELAY_RPC, "Relay") + message = "Pezkuwi E2E Test - " + str(int(time.time())) + + call = ws.compose_call( + call_module="System", + call_function="remark", + call_params={"remark": "0x" + message.encode().hex()} + ) + receipt = submit_extrinsic(ws, call, new_kp) + results.log_pass("System remark", f"Block: {receipt.block_hash[:16]}...") + ws.close() + except Exception as e: + results.log_fail("System remark", str(e)) + traceback.print_exc() + + +def test_batch_calls(results): + """Test 20: Utility.batch - multiple calls in one transaction.""" + print("\n[TEST 20] Utility Batch (multiple transfers)") + print("-" * 40) + + old_kp = Keypair.create_from_mnemonic(OLD_WALLET_MNEMONIC) + new_kp = Keypair.create_from_mnemonic(NEW_WALLET_MNEMONIC) + + try: + ws = connect(RELAY_RPC, "Relay") + + balance = get_balance(ws, new_kp) + if balance < 5 * UNITS: + results.log_skip("Batch calls", "Insufficient balance") + ws.close() + return + + # Create two small transfers in a batch + call1 = ws.compose_call( + call_module="Balances", + call_function="transfer_keep_alive", + call_params={"dest": {"Id": account_id(old_kp)}, "value": 1 * UNITS} + ) + call2 = ws.compose_call( + call_module="System", + call_function="remark", + call_params={"remark": "0x" + "batch_test".encode().hex()} + ) + + batch_call = ws.compose_call( + call_module="Utility", + call_function="batch", + call_params={"calls": [call1.value, call2.value]} + ) + receipt = submit_extrinsic(ws, batch_call, new_kp) + + # Check events for BatchCompleted + events = receipt.triggered_events + batch_ok = any("BatchCompleted" in str(e) for e in events) + if batch_ok: + results.log_pass("Batch calls", "BatchCompleted event found") + else: + results.log_pass("Batch calls", "Batch submitted successfully") + + ws.close() + except Exception as e: + results.log_fail("Batch calls", str(e)) + traceback.print_exc() + + +def test_final_balances(results): + """Test 21: Final balance report across all chains.""" + print("\n[TEST 21] Final Balance Report") + print("-" * 40) + + old_kp = Keypair.create_from_mnemonic(OLD_WALLET_MNEMONIC) + new_kp = Keypair.create_from_mnemonic(NEW_WALLET_MNEMONIC) + + for name, url, symbol in [("Relay", RELAY_RPC, "HEZ"), ("Asset Hub", ASSET_HUB_RPC, "TYR"), ("People Chain", PEOPLE_CHAIN_RPC, "HEZ")]: + try: + ws = connect(url, name) + old_info = get_full_account(ws, old_kp) + new_info = get_full_account(ws, new_kp) + + old_free = old_info["data"]["free"] + old_reserved = old_info["data"]["reserved"] + old_frozen = old_info["data"]["frozen"] + new_free = new_info["data"]["free"] + new_reserved = new_info["data"]["reserved"] + new_frozen = new_info["data"]["frozen"] + + print(f" {name} - Old: free={fmt_balance(old_free, symbol)} reserved={fmt_balance(old_reserved, symbol)} frozen={fmt_balance(old_frozen, symbol)}") + print(f" {name} - New: free={fmt_balance(new_free, symbol)} reserved={fmt_balance(new_reserved, symbol)} frozen={fmt_balance(new_frozen, symbol)}") + + results.log_pass(f"{name} final balance", f"Old: {fmt_balance(old_free, symbol)}, New: {fmt_balance(new_free, symbol)}") + ws.close() + except Exception as e: + results.log_fail(f"{name} final balance", str(e)) + + +# ============================================================ +# MAIN +# ============================================================ + +def main(): + print("=" * 60) + print("PEZKUWI MAINNET - COMPREHENSIVE E2E TEST SUITE") + print("=" * 60) + print(f"Time: {time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime())}") + print(f"Old wallet: {Keypair.create_from_mnemonic(OLD_WALLET_MNEMONIC).ss58_address}") + print(f"New wallet: {Keypair.create_from_mnemonic(NEW_WALLET_MNEMONIC).ss58_address}") + + results = TestResult() + + # Phase 1: Connectivity & Setup + test_chain_connectivity(results) + test_wallet_creation(results) + test_balance_query(results) + + # Phase 2: Basic Transfers + test_relay_transfer(results) + test_new_wallet_self_transfer(results) + test_xcm_relay_to_asset_hub(results) + test_asset_hub_transfer(results) + + # Phase 3: Staking Operations + test_staking_bond(results) + test_staking_nominate(results) + test_staking_bond_extra(results) + test_staking_unbond(results) + test_staking_chill(results) + + # Phase 4: Nomination Pools + test_nomination_pool_join(results) + test_nomination_pool_bond_extra(results) + test_nomination_pool_claim_rewards(results) + test_nomination_pool_unbond(results) + + # Phase 5: Identity & Misc + test_set_identity(results) + test_clear_identity(results) + test_remark(results) + test_batch_calls(results) + + # Phase 6: Final Report + test_final_balances(results) + + success = results.summary() + sys.exit(0 if success else 1) + + +if __name__ == "__main__": + main() diff --git a/tools/zagros-reduce-validators.mjs b/tools/zagros-reduce-validators.mjs new file mode 100644 index 00000000..ecc5ac1e --- /dev/null +++ b/tools/zagros-reduce-validators.mjs @@ -0,0 +1,159 @@ +#!/usr/bin/env node +// Zagros Testnet: Reduce validator count from 21 to 4 via sudo +// Uses @pezkuwi/api (ESM) + +import { ApiPromise, WsProvider } from '/home/mamostehp/pezkuwi-api/node_modules/@pezkuwi/api/build/index.js'; +import { Keyring } from '/home/mamostehp/pezkuwi-api/node_modules/@pezkuwi/keyring/build/cjs/index.js'; +import { cryptoWaitReady } from '/home/mamostehp/pezkuwi-api/node_modules/@pezkuwi/util-crypto/build/cjs/index.js'; + +const ZAGROS_RPC = 'ws://217.77.6.126:9948'; +const SUDO_SEED = process.env.SUDO_MNEMONIC || '******'; +const NEW_VALIDATOR_COUNT = 4; + +async function main() { + console.log('=== ZAGROS VALIDATOR COUNT REDUCTION ==='); + console.log(`Target: ${NEW_VALIDATOR_COUNT} validators`); + console.log(`RPC: ${ZAGROS_RPC}`); + console.log(); + + // Wait for crypto + await cryptoWaitReady(); + + // Create keyring and add sudo account + const keyring = new Keyring({ type: 'sr25519', ss58Format: 42 }); + const sudo = keyring.addFromUri(SUDO_SEED); + console.log(`Sudo account: ${sudo.address}`); + + // Connect to Zagros + const provider = new WsProvider(ZAGROS_RPC); + const api = await ApiPromise.create({ + provider, + signedExtensions: { + AuthorizeCall: { + extrinsic: {}, + payload: {} + } + } + }); + + console.log(`Connected to: ${(await api.rpc.system.chain()).toString()}`); + const version = await api.rpc.state.getRuntimeVersion(); + console.log(`Runtime version: ${version.specVersion.toString()}`); + + // Check current sudo key + const sudoKey = await api.query.sudo.key(); + console.log(`On-chain sudo key: ${sudoKey.toString()}`); + console.log(`Our key matches: ${sudoKey.toString() === sudo.address}`); + console.log(); + + // Check current validator count + const currentCount = await api.query.staking.validatorCount(); + console.log(`Current validator count: ${currentCount.toString()}`); + + // Check current era + const currentEra = await api.query.staking.currentEra(); + console.log(`Current era: ${currentEra.toString()}`); + console.log(); + + // Step 1: Set validator count to 4 + console.log(`[1/2] Setting validator count to ${NEW_VALIDATOR_COUNT}...`); + const setValidatorCountCall = api.tx.staking.setValidatorCount(NEW_VALIDATOR_COUNT); + const sudoCall1 = api.tx.sudo.sudo(setValidatorCountCall); + + try { + const result1 = await new Promise((resolve, reject) => { + sudoCall1.signAndSend(sudo, { nonce: -1 }, ({ status, events, dispatchError }) => { + if (dispatchError) { + if (dispatchError.isModule) { + const decoded = api.registry.findMetaError(dispatchError.asModule); + reject(new Error(`${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`)); + } else { + reject(new Error(dispatchError.toString())); + } + } + if (status.isInBlock) { + console.log(` Included in block: ${status.asInBlock.toString()}`); + // Check for Sudid event + const sudidEvent = events.find(({ event }) => + event.section === 'sudo' && event.method === 'Sudid' + ); + if (sudidEvent) { + const result = sudidEvent.event.data[0]; + if (result.isOk) { + console.log(' Sudo executed successfully!'); + } else { + console.log(` Sudo dispatch error: ${result.asErr.toString()}`); + } + } + resolve(status.asInBlock.toString()); + } + }); + }); + } catch (e) { + console.error(` ERROR: ${e.message}`); + await api.disconnect(); + process.exit(1); + } + + // Verify + const newCount = await api.query.staking.validatorCount(); + console.log(` Validator count now: ${newCount.toString()}`); + console.log(); + + // Step 2: Force new era + console.log('[2/2] Forcing new era...'); + const forceNewEraCall = api.tx.staking.forceNewEra(); + const sudoCall2 = api.tx.sudo.sudo(forceNewEraCall); + + try { + const result2 = await new Promise((resolve, reject) => { + sudoCall2.signAndSend(sudo, { nonce: -1 }, ({ status, events, dispatchError }) => { + if (dispatchError) { + if (dispatchError.isModule) { + const decoded = api.registry.findMetaError(dispatchError.asModule); + reject(new Error(`${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`)); + } else { + reject(new Error(dispatchError.toString())); + } + } + if (status.isInBlock) { + console.log(` Included in block: ${status.asInBlock.toString()}`); + const sudidEvent = events.find(({ event }) => + event.section === 'sudo' && event.method === 'Sudid' + ); + if (sudidEvent) { + const result = sudidEvent.event.data[0]; + if (result.isOk) { + console.log(' Sudo executed successfully!'); + } else { + console.log(` Sudo dispatch error: ${result.asErr.toString()}`); + } + } + resolve(status.asInBlock.toString()); + } + }); + }); + } catch (e) { + console.error(` ERROR: ${e.message}`); + await api.disconnect(); + process.exit(1); + } + + // Check forceEra storage + const forceEra = await api.query.staking.forceEra(); + console.log(` ForceEra: ${forceEra.toString()}`); + console.log(); + + console.log('=== DONE ==='); + console.log(`Validator count set to ${NEW_VALIDATOR_COUNT}`); + console.log('ForceNewEra triggered - new era will start at next session boundary'); + console.log('GRANDPA should start finalizing once new authority set (4 validators) takes effect'); + + await api.disconnect(); + process.exit(0); +} + +main().catch((err) => { + console.error('Fatal error:', err); + process.exit(1); +}); diff --git a/vendor/pezkuwi-subxt/subxt/examples/asset_hub_nom_pools.rs b/vendor/pezkuwi-subxt/subxt/examples/asset_hub_nom_pools.rs new file mode 100644 index 00000000..28299896 --- /dev/null +++ b/vendor/pezkuwi-subxt/subxt/examples/asset_hub_nom_pools.rs @@ -0,0 +1,261 @@ +//! Asset Hub: Set NominationPools configs via XCM Transact from relay chain sudo +//! +//! Since Asset Hub has no sudo pallet, we send: +//! relay: sudo(xcmPallet.send(Parachain(1000), Transact(NominationPools.set_configs(...)))) +//! +//! Run with: +//! SUDO_MNEMONIC="..." RPC_URL="ws://217.77.6.126:9944" \ +//! MIN_JOIN_BOND=10 MIN_CREATE_BOND=10000 \ +//! cargo run --release --example asset_hub_nom_pools + +#![allow(missing_docs)] +use pezkuwi_subxt::dynamic::Value; +use pezkuwi_subxt::{OnlineClient, PezkuwiConfig}; +use pezkuwi_subxt_signer::bip39::Mnemonic; +use pezkuwi_subxt_signer::sr25519::Keypair; +use std::str::FromStr; + +// 1 HEZ = 10^12 TYR (planck units) +const PLANCKS_PER_HEZ: u128 = 1_000_000_000_000; + +// Asset Hub para ID +const ASSET_HUB_PARA_ID: u32 = 1000; + +// NominationPools pallet index on Asset Hub +const NOM_POOLS_PALLET_INDEX: u8 = 81; // 0x51 +// set_configs call index +const SET_CONFIGS_CALL_INDEX: u8 = 11; // 0x0b + +/// SCALE encode ConfigOp::Noop +fn encode_noop() -> Vec { + vec![0x00] +} + +/// SCALE encode ConfigOp::Set(value) for u128 (Balance) +fn encode_set_u128(value: u128) -> Vec { + let mut buf = vec![0x01]; // Set variant + buf.extend_from_slice(&value.to_le_bytes()); // u128 LE = 16 bytes + buf +} + +/// SCALE encode the NominationPools::set_configs call +fn encode_set_configs_call(min_join_bond: u128, min_create_bond: u128) -> Vec { + let mut encoded = Vec::new(); + + // Pallet index + Call index + encoded.push(NOM_POOLS_PALLET_INDEX); + encoded.push(SET_CONFIGS_CALL_INDEX); + + // min_join_bond: ConfigOp = Set(min_join_bond) + encoded.extend_from_slice(&encode_set_u128(min_join_bond)); + + // min_create_bond: ConfigOp = Set(min_create_bond) + encoded.extend_from_slice(&encode_set_u128(min_create_bond)); + + // max_pools: ConfigOp = Noop + encoded.extend_from_slice(&encode_noop()); + + // max_members: ConfigOp = Noop + encoded.extend_from_slice(&encode_noop()); + + // max_members_per_pool: ConfigOp = Noop + encoded.extend_from_slice(&encode_noop()); + + // global_max_commission: ConfigOp = Noop + encoded.extend_from_slice(&encode_noop()); + + encoded +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("=== ASSET HUB: Set NominationPools Configs via XCM ===\n"); + + let relay_url = + std::env::var("RPC_URL").unwrap_or_else(|_| "ws://217.77.6.126:9944".to_string()); + + let min_join_hez: u128 = std::env::var("MIN_JOIN_BOND") + .unwrap_or_else(|_| "10".to_string()) + .parse()?; + let min_create_hez: u128 = std::env::var("MIN_CREATE_BOND") + .unwrap_or_else(|_| "10000".to_string()) + .parse()?; + + let min_join_bond = min_join_hez * PLANCKS_PER_HEZ; + let min_create_bond = min_create_hez * PLANCKS_PER_HEZ; + + println!("Relay RPC: {}", relay_url); + println!("Asset Hub Para ID: {}", ASSET_HUB_PARA_ID); + println!("MinJoinBond: {} HEZ ({} TYR)", min_join_hez, min_join_bond); + println!( + "MinCreateBond: {} HEZ ({} TYR)", + min_create_hez, min_create_bond + ); + + // Connect to relay chain + let api = OnlineClient::::from_insecure_url(&relay_url).await?; + println!("Connected to relay chain!"); + + // Load sudo keypair + let mnemonic_str = + std::env::var("SUDO_MNEMONIC").expect("SUDO_MNEMONIC environment variable required"); + let mnemonic = Mnemonic::from_str(&mnemonic_str)?; + let sudo_keypair = Keypair::from_phrase(&mnemonic, None)?; + println!("Sudo: {}\n", sudo_keypair.public_key().to_account_id()); + + // Encode the NominationPools::set_configs call for Asset Hub + let encoded_call = encode_set_configs_call(min_join_bond, min_create_bond); + println!( + "Encoded call: {} bytes (0x{})", + encoded_call.len(), + hex::encode(&encoded_call) + ); + + // Build XCM destination: V3 MultiLocation { parents: 0, interior: X1(Teyrchain(1000)) } + let dest = Value::unnamed_variant( + "V3", + vec![Value::named_composite([ + ("parents", Value::u128(0)), + ( + "interior", + Value::unnamed_variant( + "X1", + vec![Value::unnamed_variant( + "Teyrchain", + vec![Value::u128(ASSET_HUB_PARA_ID as u128)], + )], + ), + ), + ])], + ); + + // Build XCM V3 message: UnpaidExecution + Transact + let message = Value::unnamed_variant( + "V3", + vec![Value::unnamed_composite(vec![ + Value::named_variant( + "UnpaidExecution", + [ + ( + "weight_limit", + Value::unnamed_variant("Unlimited", vec![]), + ), + ( + "check_origin", + Value::unnamed_variant("None", vec![]), + ), + ], + ), + Value::named_variant( + "Transact", + [ + ( + "origin_kind", + Value::unnamed_variant("Superuser", vec![]), + ), + ( + "require_weight_at_most", + Value::named_composite([ + ("ref_time", Value::u128(5_000_000_000u128)), + ("proof_size", Value::u128(500_000u128)), + ]), + ), + ("call", Value::from_bytes(&encoded_call)), + ], + ), + ])], + ); + + // Wrap in XcmPallet.send + let xcm_send = + pezkuwi_subxt::dynamic::tx("XcmPallet", "send", vec![dest, message]); + + // Wrap in sudo_unchecked_weight (no weight limit for sudo) + let sudo_call = pezkuwi_subxt::dynamic::tx( + "Sudo", + "sudo_unchecked_weight", + vec![ + xcm_send.into_value(), + Value::named_composite([ + ("ref_time", Value::u128(1u128)), + ("proof_size", Value::u128(1u128)), + ]), + ], + ); + + println!("Submitting: sudo(xcmPallet.send(Parachain(1000), Transact(NominationPools.set_configs)))...\n"); + + // Submit and watch + use pezkuwi_subxt::tx::TxStatus; + + let tx_progress = api + .tx() + .sign_and_submit_then_watch_default(&sudo_call, &sudo_keypair) + .await?; + + println!( + "TX hash: 0x{}", + hex::encode(tx_progress.extrinsic_hash().as_ref()) + ); + println!("Watching TX status..."); + + let mut progress = tx_progress; + loop { + let status = progress.next().await; + match status { + Some(Ok(TxStatus::Validated)) => println!(" Status: Validated"), + Some(Ok(TxStatus::Broadcasted)) => println!(" Status: Broadcasted"), + Some(Ok(TxStatus::InBestBlock(details))) => { + println!(" Status: InBestBlock {:?}", details.block_hash()); + match details.wait_for_success().await { + Ok(events) => { + println!(" TX SUCCESS!"); + for event in events.iter() { + if let Ok(ev) = event { + println!( + " Event: {}::{}", + ev.pallet_name(), + ev.variant_name() + ); + } + } + }, + Err(e) => println!(" TX dispatch error: {}", e), + } + break; + }, + Some(Ok(TxStatus::InFinalizedBlock(details))) => { + println!(" Status: Finalized {:?}", details.block_hash()); + break; + }, + Some(Ok(TxStatus::Error { message })) => { + println!(" Status: ERROR - {}", message); + break; + }, + Some(Ok(TxStatus::Invalid { message })) => { + println!(" Status: INVALID - {}", message); + break; + }, + Some(Ok(TxStatus::Dropped { message })) => { + println!(" Status: DROPPED - {}", message); + break; + }, + Some(Ok(TxStatus::NoLongerInBestBlock)) => { + println!(" Status: No longer in best block"); + }, + Some(Err(e)) => { + println!(" Stream error: {}", e); + break; + }, + None => { + println!(" Stream ended"); + break; + }, + } + } + + println!("\nDone. XCM Transact sent to Asset Hub."); + println!("Verify on Asset Hub (port 40944) that MinJoinBond and MinCreateBond are set."); + + Ok(()) +} diff --git a/vendor/pezkuwi-subxt/subxt/examples/bond_extra_validators.rs b/vendor/pezkuwi-subxt/subxt/examples/bond_extra_validators.rs new file mode 100644 index 00000000..418bd853 --- /dev/null +++ b/vendor/pezkuwi-subxt/subxt/examples/bond_extra_validators.rs @@ -0,0 +1,207 @@ +//! Bond extra HEZ for all 21 validators +//! +//! Reads stash seed phrases from WALLETS_FILE, calls staking.bond_extra for each. +//! +//! Run with: +//! WALLETS_FILE="/home/mamostehp/res/MAINNET_WALLETS_20260128_235407.json" \ +//! RPC_URL="ws://217.77.6.126:9944" \ +//! BOND_EXTRA_HEZ=499000 \ +//! cargo run --release --example bond_extra_validators -p pezkuwi-subxt + +#![allow(missing_docs)] +use pezkuwi_subxt::dynamic::Value; +use pezkuwi_subxt::{OnlineClient, PezkuwiConfig}; +use pezkuwi_subxt_signer::bip39::Mnemonic; +use pezkuwi_subxt_signer::sr25519::Keypair; +use std::str::FromStr; + +const PLANCKS_PER_HEZ: u128 = 1_000_000_000_000; + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("=== BOND EXTRA HEZ FOR VALIDATORS ===\n"); + + let url = std::env::var("RPC_URL").unwrap_or_else(|_| "ws://217.77.6.126:9944".to_string()); + let wallets_file = std::env::var("WALLETS_FILE") + .unwrap_or_else(|_| "/home/mamostehp/res/MAINNET_WALLETS_20260128_235407.json".to_string()); + let bond_hez: u128 = std::env::var("BOND_EXTRA_HEZ") + .unwrap_or_else(|_| "499000".to_string()) + .parse()?; + let bond_planck = bond_hez * PLANCKS_PER_HEZ; + let skip: usize = std::env::var("SKIP") + .unwrap_or_else(|_| "0".to_string()) + .parse() + .unwrap_or(0); + + println!("RPC: {}", url); + println!("Wallets file: {}", wallets_file); + println!("Bond extra per validator: {} HEZ", bond_hez); + + // Read wallet file + let wallet_data: serde_json::Value = + serde_json::from_str(&std::fs::read_to_string(&wallets_file)?)?; + let wallets = wallet_data["wallets"].as_array().expect("wallets array not found"); + + // Extract stash wallets (Validator_XX_Stash) + let mut stash_wallets: Vec<(&str, &str)> = Vec::new(); + for w in wallets { + let name = w["name"].as_str().unwrap_or(""); + if name.contains("Stash") && name.starts_with("Validator_") { + let seed = w["seed_phrase"].as_str().expect("seed_phrase missing"); + stash_wallets.push((name, seed)); + } + } + stash_wallets.sort_by_key(|(name, _)| name.to_string()); + + println!("Found {} stash wallets", stash_wallets.len()); + println!( + "Total bond: {} HEZ to {} validators (skipping {})\n", + bond_hez * (stash_wallets.len() - skip) as u128, + stash_wallets.len() - skip, + skip + ); + + let api = OnlineClient::::from_insecure_url(&url).await?; + println!("Connected!\n"); + + let mut success_count = 0; + let mut fail_count = 0; + + for (i, (name, seed)) in stash_wallets.iter().enumerate().skip(skip) { + println!("--- [{}/{}] {} ---", i + 1, stash_wallets.len(), name); + + let mnemonic = match Mnemonic::from_str(seed) { + Ok(m) => m, + Err(e) => { + println!(" ERROR: Invalid mnemonic: {}", e); + fail_count += 1; + continue; + }, + }; + let keypair = match Keypair::from_phrase(&mnemonic, None) { + Ok(k) => k, + Err(e) => { + println!(" ERROR: Keypair error: {}", e); + fail_count += 1; + continue; + }, + }; + let account = keypair.public_key().to_account_id(); + println!(" Account: {}", account); + + // staking.bond_extra(max_additional: Balance) + let bond_extra_tx = pezkuwi_subxt::dynamic::tx( + "Staking", + "bond_extra", + vec![Value::u128(bond_planck)], + ); + + use pezkuwi_subxt::tx::TxStatus; + let mut tx_ok = false; + + for attempt in 0..3 { + let tx_progress = match api + .tx() + .sign_and_submit_then_watch_default(&bond_extra_tx, &keypair) + .await + { + Ok(p) => p, + Err(e) => { + println!(" SUBMIT ERROR (attempt {}): {}", attempt + 1, e); + tokio::time::sleep(std::time::Duration::from_secs(12)).await; + continue; + }, + }; + + println!( + " TX: 0x{}", + hex::encode(tx_progress.extrinsic_hash().as_ref()) + ); + + let mut progress = tx_progress; + loop { + let status = progress.next().await; + match status { + Some(Ok(TxStatus::InBestBlock(details))) => { + match details.wait_for_success().await { + Ok(events) => { + for event in events.iter() { + if let Ok(ev) = event { + if ev.pallet_name() == "Staking" + && ev.variant_name() == "Bonded" + { + println!(" SUCCESS: {} HEZ bonded", bond_hez); + tx_ok = true; + } + } + } + if !tx_ok { + println!(" WARNING: No Staking::Bonded event"); + for event in events.iter() { + if let Ok(ev) = event { + println!( + " {}::{}", + ev.pallet_name(), + ev.variant_name() + ); + } + } + } + }, + Err(e) => println!(" DISPATCH ERROR: {}", e), + } + break; + }, + Some(Ok(TxStatus::Error { message })) => { + println!(" TX ERROR: {}", message); + break; + }, + Some(Ok(TxStatus::Invalid { message })) => { + println!(" TX INVALID: {}", message); + break; + }, + Some(Ok(TxStatus::Dropped { message })) => { + println!(" TX DROPPED: {}", message); + break; + }, + Some(Err(e)) => { + println!(" STREAM ERROR: {}", e); + break; + }, + None => { + println!(" STREAM ENDED"); + break; + }, + _ => {}, + } + } + + if tx_ok { + break; + } + tokio::time::sleep(std::time::Duration::from_secs(12)).await; + } + + if tx_ok { + success_count += 1; + } else { + fail_count += 1; + } + + // Wait between transactions (different signers so nonce isn't an issue, + // but still good to not flood the mempool) + if i + 1 < stash_wallets.len() { + tokio::time::sleep(std::time::Duration::from_secs(6)).await; + } + } + + println!("\n=== RESULTS ==="); + println!("Success: {}/{}", success_count, stash_wallets.len() - skip); + println!("Failed: {}/{}", fail_count, stash_wallets.len() - skip); + println!( + "Total bonded: {} HEZ", + bond_hez * success_count as u128 + ); + + Ok(()) +} diff --git a/vendor/pezkuwi-subxt/subxt/examples/create_nomination_pools.rs b/vendor/pezkuwi-subxt/subxt/examples/create_nomination_pools.rs new file mode 100644 index 00000000..a4e519b4 --- /dev/null +++ b/vendor/pezkuwi-subxt/subxt/examples/create_nomination_pools.rs @@ -0,0 +1,432 @@ +//! Create Nomination Pools on Asset Hub +//! +//! Steps: +//! 1. Transfer HEZ from founder to each pool wallet (on Asset Hub) +//! 2. Each wallet creates a nomination pool with specified stake +//! 3. Set pool metadata (name) +//! +//! Environment variables: +//! FOUNDER_MNEMONIC - Founder wallet mnemonic (required) +//! WALLETS_FILE - JSON file with wallet list (required) +//! ASSET_HUB_RPC - Asset Hub RPC endpoint (default: ws://217.77.6.126:40944) +//! SKIP - Number of wallets to skip (default: 0) +//! TRANSFER_HEZ - HEZ to transfer to each wallet (default: 500000) +//! BASE_STAKE_HEZ - Starting stake for first pool (default: 490000, decreases by 10000 per pool) +//! +//! Wallets JSON format: +//! [ +//! { "name": "Pool Name", "mnemonic": "word1 word2 ...", "ss58": "5..." }, +//! ... +//! ] +//! +//! Run with: +//! FOUNDER_MNEMONIC="..." WALLETS_FILE="wallets.json" \ +//! cargo run --release --example create_nomination_pools +//! +//! # Or run a specific phase: +//! FOUNDER_MNEMONIC="..." WALLETS_FILE="wallets.json" \ +//! cargo run --release --example create_nomination_pools -- transfer + +#![allow(missing_docs)] +use pezkuwi_subxt::dynamic::Value; +use pezkuwi_subxt::utils::AccountId32; +use pezkuwi_subxt::{OnlineClient, PezkuwiConfig}; +use pezkuwi_subxt_signer::bip39::Mnemonic; +use pezkuwi_subxt_signer::sr25519::Keypair; +use std::str::FromStr; + +const PLANCKS_PER_HEZ: u128 = 1_000_000_000_000; +const DEFAULT_ASSET_HUB_RPC: &str = "ws://217.77.6.126:40944"; + +#[derive(serde::Deserialize)] +struct WalletInfo { + name: String, + mnemonic: String, + ss58: String, +} + +fn load_wallets() -> Vec { + let path = std::env::var("WALLETS_FILE").expect( + "WALLETS_FILE environment variable required. \ + Point it to a JSON file with wallet entries: \ + [{\"name\": \"...\", \"mnemonic\": \"...\", \"ss58\": \"5...\"}]", + ); + let data = std::fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("Failed to read wallets file '{}': {}", path, e)); + serde_json::from_str(&data) + .unwrap_or_else(|e| panic!("Failed to parse wallets file '{}': {}", path, e)) +} + +fn founder_mnemonic() -> String { + std::env::var("FOUNDER_MNEMONIC").expect("FOUNDER_MNEMONIC environment variable required") +} + +struct PoolConfig { + name: String, + mnemonic: String, + ss58: String, + transfer_hez: u128, + stake_hez: u128, +} + +fn build_pool_configs(wallets: Vec) -> Vec { + let transfer_hez: u128 = std::env::var("TRANSFER_HEZ") + .unwrap_or_else(|_| "500000".to_string()) + .parse() + .expect("TRANSFER_HEZ must be a valid number"); + + let base_stake: u128 = std::env::var("BASE_STAKE_HEZ") + .unwrap_or_else(|_| "490000".to_string()) + .parse() + .expect("BASE_STAKE_HEZ must be a valid number"); + + wallets + .into_iter() + .enumerate() + .map(|(i, w)| { + let stake_hez = base_stake.saturating_sub(i as u128 * 10_000); + PoolConfig { + name: w.name, + mnemonic: w.mnemonic, + ss58: w.ss58, + transfer_hez, + stake_hez, + } + }) + .collect() +} + +async fn wait_for_success( + mut progress: pezkuwi_subxt::tx::TxProgress>, + label: &str, +) -> Result> { + use pezkuwi_subxt::tx::TxStatus; + loop { + let status = progress.next().await; + match status { + Some(Ok(TxStatus::InBestBlock(details))) => { + match details.wait_for_success().await { + Ok(events) => { + println!(" {} SUCCESS!", label); + for event in events.iter() { + if let Ok(ev) = event { + println!( + " {}::{}", + ev.pallet_name(), + ev.variant_name() + ); + } + } + return Ok(true); + }, + Err(e) => { + println!(" {} DISPATCH ERROR: {}", label, e); + return Ok(false); + }, + } + }, + Some(Ok(TxStatus::Error { message })) => { + println!(" {} TX ERROR: {}", label, message); + return Ok(false); + }, + Some(Ok(TxStatus::Invalid { message })) => { + println!(" {} TX INVALID: {}", label, message); + return Ok(false); + }, + Some(Ok(TxStatus::Dropped { message })) => { + println!(" {} TX DROPPED: {}", label, message); + return Ok(false); + }, + Some(Err(e)) => { + println!(" {} STREAM ERROR: {}", label, e); + return Err(e.into()); + }, + None => { + println!(" {} STREAM ENDED", label); + return Ok(false); + }, + _ => {}, + } + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let wallets = load_wallets(); + let pool_configs = build_pool_configs(wallets); + + // Parse CLI args + let args: Vec = std::env::args().collect(); + let phase = args.get(1).map(|s| s.as_str()).unwrap_or("all"); + let skip: usize = std::env::var("SKIP") + .unwrap_or_else(|_| "0".to_string()) + .parse() + .unwrap_or(0); + + let rpc = + std::env::var("ASSET_HUB_RPC").unwrap_or_else(|_| DEFAULT_ASSET_HUB_RPC.to_string()); + + println!("=== NOMINATION POOL CREATOR ==="); + println!("Asset Hub RPC: {}", rpc); + println!("Phase: {}", phase); + println!("Skip: {}", skip); + println!("Pools: {}\n", pool_configs.len()); + + // Connect to Asset Hub + let api = OnlineClient::::from_insecure_url(&rpc).await?; + println!("Connected to Asset Hub!\n"); + + // ========== PHASE 1: TRANSFERS ========== + if phase == "all" || phase == "transfer" { + println!("========== PHASE 1: TRANSFERS ==========\n"); + + let founder_mn = Mnemonic::from_str(&founder_mnemonic())?; + let founder_keypair = Keypair::from_phrase(&founder_mn, None)?; + println!( + "Founder: {}\n", + founder_keypair.public_key().to_account_id() + ); + + for (i, pool) in pool_configs.iter().enumerate().skip(skip) { + println!( + "--- [{}/{}] Transfer {} HEZ -> {} ({}) ---", + i + 1, + pool_configs.len(), + pool.transfer_hez, + pool.name, + pool.ss58 + ); + + let dest: AccountId32 = pool.ss58.parse()?; + let amount_planck = pool.transfer_hez * PLANCKS_PER_HEZ; + + let mut tx_ok = false; + for attempt in 0..3 { + if attempt > 0 { + println!(" Retry attempt {}...", attempt + 1); + tokio::time::sleep(std::time::Duration::from_secs(18)).await; + } + + let transfer_tx = pezkuwi_subxt::dynamic::tx( + "Balances", + "transfer_keep_alive", + vec![ + Value::unnamed_variant("Id", vec![Value::from_bytes(&dest.0)]), + Value::u128(amount_planck), + ], + ); + + let tx_progress = match api + .tx() + .sign_and_submit_then_watch_default(&transfer_tx, &founder_keypair) + .await + { + Ok(p) => p, + Err(e) => { + println!(" SUBMIT ERROR (attempt {}): {}", attempt + 1, e); + continue; + }, + }; + + println!( + " TX: 0x{}", + hex::encode(tx_progress.extrinsic_hash().as_ref()) + ); + + tx_ok = wait_for_success(tx_progress, "TRANSFER").await?; + if tx_ok { + break; + } + } + + if !tx_ok { + println!(" FAILED after 3 attempts! Stopping."); + return Ok(()); + } + + // Wait between transactions for nonce to update + if i + 1 < pool_configs.len() { + println!(" Waiting 18s for next block..."); + tokio::time::sleep(std::time::Duration::from_secs(18)).await; + } + } + + println!("\n========== ALL TRANSFERS DONE ==========\n"); + + if phase == "transfer" { + return Ok(()); + } + + // Wait before pool creation + println!("Waiting 24s before pool creation...\n"); + tokio::time::sleep(std::time::Duration::from_secs(24)).await; + } + + // ========== PHASE 2: CREATE POOLS ========== + if phase == "all" || phase == "pools" { + println!("========== PHASE 2: CREATE POOLS ==========\n"); + + for (i, pool) in pool_configs.iter().enumerate().skip(skip) { + println!( + "--- [{}/{}] Create pool '{}' with {} HEZ stake ---", + i + 1, + pool_configs.len(), + pool.name, + pool.stake_hez + ); + + // Load pool wallet keypair + let pool_mnemonic = Mnemonic::from_str(&pool.mnemonic)?; + let pool_keypair = Keypair::from_phrase(&pool_mnemonic, None)?; + let pool_account = pool_keypair.public_key().to_account_id(); + println!(" Wallet: {}", pool_account); + + let stake_planck = pool.stake_hez * PLANCKS_PER_HEZ; + + // NominationPools::create(amount, root, nominator, bouncer) + let mut create_ok = false; + for attempt in 0..3 { + if attempt > 0 { + println!(" Create retry attempt {}...", attempt + 1); + tokio::time::sleep(std::time::Duration::from_secs(18)).await; + } + + let create_tx = pezkuwi_subxt::dynamic::tx( + "NominationPools", + "create", + vec![ + Value::u128(stake_planck), + Value::unnamed_variant("Id", vec![Value::from_bytes(&pool_account.0)]), + Value::unnamed_variant("Id", vec![Value::from_bytes(&pool_account.0)]), + Value::unnamed_variant("Id", vec![Value::from_bytes(&pool_account.0)]), + ], + ); + + let tx_progress = match api + .tx() + .sign_and_submit_then_watch_default(&create_tx, &pool_keypair) + .await + { + Ok(p) => p, + Err(e) => { + println!(" SUBMIT ERROR (attempt {}): {}", attempt + 1, e); + continue; + }, + }; + + println!( + " TX: 0x{}", + hex::encode(tx_progress.extrinsic_hash().as_ref()) + ); + + create_ok = wait_for_success(tx_progress, "CREATE_POOL").await?; + if create_ok { + break; + } + } + + if !create_ok { + println!(" FAILED after 3 attempts! Continuing to next pool..."); + tokio::time::sleep(std::time::Duration::from_secs(18)).await; + continue; + } + + // Wait for pool creation to settle + tokio::time::sleep(std::time::Duration::from_secs(12)).await; + + // Query LastPoolId to get the pool_id + let last_pool_query = pezkuwi_subxt::dynamic::storage::<(), Value>( + "NominationPools", + "LastPoolId", + ); + let storage_client = api.storage().at_latest().await?; + let last_pool = storage_client + .entry(last_pool_query)? + .try_fetch(()) + .await?; + + let pool_id = match last_pool { + Some(val) => { + let decoded = val.decode()?; + decoded.as_u128().unwrap_or(0) as u32 + }, + None => { + println!(" WARNING: Could not read LastPoolId"); + (i + 1) as u32 // fallback + }, + }; + println!(" Pool ID: {}", pool_id); + + // NominationPools::set_metadata(pool_id, metadata) + tokio::time::sleep(std::time::Duration::from_secs(6)).await; + let name_bytes = pool.name.as_bytes().to_vec(); + for attempt in 0..3 { + if attempt > 0 { + println!(" Metadata retry attempt {}...", attempt + 1); + tokio::time::sleep(std::time::Duration::from_secs(6)).await; + } + + let metadata_tx = pezkuwi_subxt::dynamic::tx( + "NominationPools", + "set_metadata", + vec![ + Value::u128(pool_id as u128), + Value::from_bytes(&name_bytes), + ], + ); + + let tx_progress = match api + .tx() + .sign_and_submit_then_watch_default(&metadata_tx, &pool_keypair) + .await + { + Ok(p) => p, + Err(e) => { + println!(" METADATA SUBMIT ERROR (attempt {}): {}", attempt + 1, e); + continue; + }, + }; + + println!( + " METADATA TX: 0x{}", + hex::encode(tx_progress.extrinsic_hash().as_ref()) + ); + + let ok = wait_for_success(tx_progress, "SET_METADATA").await?; + if ok { + break; + } + if attempt == 2 { + println!(" WARNING: Metadata set failed for pool {}", pool_id); + } + } + + println!(" Pool '{}' (ID: {}) created with {} HEZ\n", pool.name, pool_id, pool.stake_hez); + + // Wait between pools + if i + 1 < pool_configs.len() { + tokio::time::sleep(std::time::Duration::from_secs(12)).await; + } + } + + println!("\n========== ALL POOLS CREATED =========="); + } + + // ========== SUMMARY ========== + println!("\n=== SUMMARY ==="); + for (i, pool) in pool_configs.iter().enumerate() { + println!( + " Pool {}: '{}' - {} HEZ staked by {}", + i + 1, + pool.name, + pool.stake_hez, + pool.ss58 + ); + } + let total_transfer: u128 = pool_configs.iter().map(|p| p.transfer_hez).sum(); + let total_stake: u128 = pool_configs.iter().map(|p| p.stake_hez).sum(); + println!("\n Total transferred: {} HEZ", total_transfer); + println!(" Total staked: {} HEZ", total_stake); + + Ok(()) +} diff --git a/vendor/pezkuwi-subxt/subxt/examples/fix_force_era.rs b/vendor/pezkuwi-subxt/subxt/examples/fix_force_era.rs new file mode 100644 index 00000000..ed04db9b --- /dev/null +++ b/vendor/pezkuwi-subxt/subxt/examples/fix_force_era.rs @@ -0,0 +1,131 @@ +//! Fix ForceEra: set from ForceAlways back to NotForcing +//! +//! Run with: +//! SUDO_MNEMONIC="..." RPC_URL="ws://217.77.6.126:9944" \ +//! cargo run --release --example fix_force_era -p pezkuwi-subxt + +#![allow(missing_docs)] +use pezkuwi_subxt::dynamic::Value; +use pezkuwi_subxt::{OnlineClient, PezkuwiConfig}; +use pezkuwi_subxt_signer::bip39::Mnemonic; +use pezkuwi_subxt_signer::sr25519::Keypair; +use std::str::FromStr; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let url = std::env::var("RPC_URL").unwrap_or_else(|_| "ws://217.77.6.126:9944".to_string()); + + println!("=== FIX ForceEra: ForceAlways -> NotForcing ===\n"); + println!("RPC: {}", url); + + let api = OnlineClient::::from_insecure_url(&url).await?; + println!("Connected!"); + + let mnemonic_str = + std::env::var("SUDO_MNEMONIC").expect("SUDO_MNEMONIC environment variable required"); + let mnemonic = Mnemonic::from_str(&mnemonic_str)?; + let keypair = Keypair::from_phrase(&mnemonic, None)?; + println!("Sudo account: {}\n", keypair.public_key().to_account_id()); + + // Staking::ForceEra storage key (verified twox128) + let force_era_key = hex::decode( + "5f3e4907f716ac89b6347d15ececedcaf7dad0317324aecae8744b87fc95f2f3", + )?; + // NotForcing = enum variant 0 = 0x00 + let not_forcing_value = vec![0x00u8]; + + println!( + "Storage key: 0x{}", + hex::encode(&force_era_key) + ); + println!( + "New value: 0x{} (NotForcing)", + hex::encode(¬_forcing_value) + ); + + // Build: system.setStorage(items: Vec<(Key, Value)>) + let set_storage_call = pezkuwi_subxt::dynamic::tx( + "System", + "set_storage", + vec![Value::unnamed_composite(vec![Value::unnamed_composite(vec![ + Value::from_bytes(&force_era_key), + Value::from_bytes(¬_forcing_value), + ])])], + ); + + // Wrap in sudo + let sudo_call = pezkuwi_subxt::dynamic::tx( + "Sudo", + "sudo", + vec![set_storage_call.into_value()], + ); + + println!("\nSubmitting sudo(system.setStorage)..."); + + use pezkuwi_subxt::tx::TxStatus; + let tx_progress = api + .tx() + .sign_and_submit_then_watch_default(&sudo_call, &keypair) + .await?; + + println!( + "TX: 0x{}", + hex::encode(tx_progress.extrinsic_hash().as_ref()) + ); + + let mut progress = tx_progress; + loop { + let status = progress.next().await; + match status { + Some(Ok(TxStatus::InBestBlock(details))) => { + match details.wait_for_success().await { + Ok(events) => { + let mut sudid = false; + for event in events.iter() { + if let Ok(ev) = event { + println!( + " Event: {}::{}", + ev.pallet_name(), + ev.variant_name() + ); + if ev.pallet_name() == "Sudo" && ev.variant_name() == "Sudid" { + sudid = true; + } + } + } + if sudid { + println!("\nSUCCESS: ForceEra set to NotForcing"); + } else { + println!("\nWARNING: Sudo::Sudid event not found"); + } + }, + Err(e) => println!("DISPATCH ERROR: {}", e), + } + break; + }, + Some(Ok(TxStatus::Error { message })) => { + println!("TX ERROR: {}", message); + break; + }, + Some(Ok(TxStatus::Invalid { message })) => { + println!("TX INVALID: {}", message); + break; + }, + Some(Ok(TxStatus::Dropped { message })) => { + println!("TX DROPPED: {}", message); + break; + }, + Some(Err(e)) => { + println!("STREAM ERROR: {}", e); + break; + }, + None => { + println!("STREAM ENDED"); + break; + }, + _ => {}, + } + } + + Ok(()) +} diff --git a/vendor/pezkuwi-subxt/subxt/examples/mint_welati_tiki.rs b/vendor/pezkuwi-subxt/subxt/examples/mint_welati_tiki.rs new file mode 100644 index 00000000..096c64e2 --- /dev/null +++ b/vendor/pezkuwi-subxt/subxt/examples/mint_welati_tiki.rs @@ -0,0 +1,321 @@ +//! Mint Welati Tiki (citizenship NFT) for validators via XCM Transact +//! +//! People Chain has no sudo, so we send from relay chain: +//! sudo(xcmPallet.send(Parachain(1004), Transact(Tiki.force_mint_citizen_nft(dest)))) +//! +//! Run with: +//! SUDO_MNEMONIC="..." RPC_URL="ws://217.77.6.126:9944" \ +//! cargo run --release --example mint_welati_tiki + +#![allow(missing_docs)] +use pezkuwi_subxt::dynamic::Value; +use pezkuwi_subxt::utils::AccountId32; +use pezkuwi_subxt::{OnlineClient, PezkuwiConfig}; +use pezkuwi_subxt_signer::bip39::Mnemonic; +use pezkuwi_subxt_signer::sr25519::Keypair; +use std::str::FromStr; + +// People Chain para ID +const PEOPLE_CHAIN_PARA_ID: u32 = 1004; + +// Tiki pallet index on People Chain +const TIKI_PALLET_INDEX: u8 = 61; // 0x3d +// force_mint_citizen_nft call index +const FORCE_MINT_CALL_INDEX: u8 = 2; // 0x02 + +/// Encode Tiki::force_mint_citizen_nft(dest) for People Chain +/// dest is MultiAddress::Id(AccountId32) = 0x00 + 32 bytes +fn encode_force_mint_call(account_id: &[u8; 32]) -> Vec { + let mut encoded = Vec::with_capacity(35); + encoded.push(TIKI_PALLET_INDEX); // 0x3d + encoded.push(FORCE_MINT_CALL_INDEX); // 0x02 + encoded.push(0x00); // MultiAddress::Id variant + encoded.extend_from_slice(account_id); + encoded +} + +/// Build XCM Transact message wrapped in sudo for relay chain +fn build_xcm_sudo_transact(encoded_call: &[u8]) -> (Value, Value) { + // Destination: People Chain + let dest = Value::unnamed_variant( + "V3", + vec![Value::named_composite([ + ("parents", Value::u128(0)), + ( + "interior", + Value::unnamed_variant( + "X1", + vec![Value::unnamed_variant( + "Teyrchain", + vec![Value::u128(PEOPLE_CHAIN_PARA_ID as u128)], + )], + ), + ), + ])], + ); + + // XCM message: UnpaidExecution + Transact + let message = Value::unnamed_variant( + "V3", + vec![Value::unnamed_composite(vec![ + Value::named_variant( + "UnpaidExecution", + [ + ( + "weight_limit", + Value::unnamed_variant("Unlimited", vec![]), + ), + ( + "check_origin", + Value::unnamed_variant("None", vec![]), + ), + ], + ), + Value::named_variant( + "Transact", + [ + ( + "origin_kind", + Value::unnamed_variant("Superuser", vec![]), + ), + ( + "require_weight_at_most", + Value::named_composite([ + ("ref_time", Value::u128(5_000_000_000u128)), + ("proof_size", Value::u128(500_000u128)), + ]), + ), + ("call", Value::from_bytes(encoded_call)), + ], + ), + ])], + ); + + (dest, message) +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("=== MINT WELATI TIKI FOR VALIDATORS ===\n"); + + // Validator name → SS58 address mapping + let validators: Vec<(&str, &str)> = vec![ + ("Çiyager (Cihat Türkan)", "5GipBJs2uNWTCazyZQ2vG3DEqLz4tXNmNZtBAT1Mtm1orZ5i"), + ("Mehmet Tunç", "5HWFZbhkZuTUySXu6ZXYKrTHBnWXHvWRKLozE22zhnwXGGxk"), + ("Nagihan Akarsel", "5CrB5BWJfLNWEZAsAXDKXdJUGzFMXKvYnwRX4DVMcgBwxSdx"), + ("Sait Çürükkaya (Doktor Süleyman)", "5ELgySrX5ZyK7EWXjj6bAedyTCcTNWDANbiiipsT5gnpoCEp"), + ("Evdile Koçer", "5GCZQNjRdHofEHPvVq4ePrfDYcjRzQ1HQ2awHMX6AawpRYuM"), + ("Mam Zeki", "5H8jTzi4Gm4rbFtXw6h5enhLhgsuhNAqR5K2itmPiz83ymWy"), + ("Kakaî Falah", "5Fs3P5tHuL9cvwPQojsheViRRAjFkMMFa32jAkDSwW9mbTfU"), + ("Feryad Fazil Ömer", "5DXgq7uDXog6zcubT3wgtaYosoibjudz4w5ScPW2phLuAy3V"), + ("Mevlud Afand", "5FyFwbGLgPXun3azh6Gx83wCuUt5FTavb2WAVDYrjziVB9rN"), + ("Şêrko Fatih Şivandî", "5HEcuuypLDeJaSj6ZgH57aXhuviyeLNdw9QrCDJ8u6gsnjnL"), + ("Ramin Hüseyin Penahi", "5EpmpTXbMXpz6ixy3WhutdzcexzPbvybNKv4eiiN1kvTnQH5"), + ("Zanyar Moradi", "5DFsm3BBEgHmSEZkvwGKB7c7tiH2avhfuQE1SEjfMDGuczsW"), + ("Heidar Ghorbani", "5HePVUXjGSM2hVZ1YMz2V3KoX6EdQNEmmzUnUvpfGV95ofUR"), + ("Farhad Salimi", "5GP4nAcwtETTg1oAHQNvevmmhG8GEstGQeCirKEhaDTwpFgx"), + ("Vafa Azarbar", "5FYoCM3oeEGeoFY94EgXBhmABkRCabvPp72ur5bJNG3cK619"), + ("Dr. Aziz Mihemed", "5GspwkKF6aYzFkmAyBBQg7coSCSgDCore79fbW8uxJNAH347"), + ("Arîn Mîrkan", "5GmuX11pN2fC4Fyq1V7MuiYt3aevZcVQs3HZWKyzmap9bKfe"), + ("Ebu Leyla", "5FQptVCtM1qsxkLbQkATkw4Kio4M9LxWvM6TwgEo3QjmTXF3"), + ("Rêvan Kobanê", "5E7VD2qmso1yRfyq3t9u2qhauAgtmjZTybVsCARF5Zz9bXy6"), + ("Amanj Babani", "5Ccz5W7Q21g4UPCytzHxD3VSMLJ1BbbWSkJKFwsNtYRk3HkX"), + ("Xosrow Gulan", "5D7WPmK1SAJyYDdCtgqEzGJpWXQe3Lj9FqWL8z9waLTkUNv3"), + ]; + + let relay_url = + std::env::var("RPC_URL").unwrap_or_else(|_| "ws://217.77.6.126:9944".to_string()); + + // Skip first N validators (already minted) + let skip: usize = std::env::var("SKIP") + .unwrap_or_else(|_| "0".to_string()) + .parse() + .unwrap_or(0); + + println!("Relay RPC: {}", relay_url); + println!("People Chain Para ID: {}", PEOPLE_CHAIN_PARA_ID); + println!("Validators to mint: {} (skipping first {})\n", validators.len() - skip, skip); + + // Connect + let api = OnlineClient::::from_insecure_url(&relay_url).await?; + println!("Connected to relay chain!"); + + // Load sudo keypair + let mnemonic_str = + std::env::var("SUDO_MNEMONIC").expect("SUDO_MNEMONIC environment variable required"); + let mnemonic = Mnemonic::from_str(&mnemonic_str)?; + let sudo_keypair = Keypair::from_phrase(&mnemonic, None)?; + println!("Sudo: {}\n", sudo_keypair.public_key().to_account_id()); + + let mut success_count = 0; + let mut fail_count = 0; + + for (i, (name, ss58)) in validators.iter().enumerate().skip(skip) { + println!("--- [{}/{}] {} ---", i + 1, validators.len(), name); + println!(" Address: {}", ss58); + + // Parse SS58 to AccountId32 + let account: AccountId32 = match ss58.parse() { + Ok(a) => a, + Err(e) => { + println!(" ERROR: Invalid SS58 address: {}", e); + fail_count += 1; + continue; + }, + }; + + // Encode the call + let encoded_call = encode_force_mint_call(&account.0); + println!( + " Encoded call: 0x{}", + hex::encode(&encoded_call[..4]) // just show prefix + ); + + // Build XCM message + let (dest, message) = build_xcm_sudo_transact(&encoded_call); + + // Wrap in xcmPallet.send then sudo + let xcm_send = + pezkuwi_subxt::dynamic::tx("XcmPallet", "send", vec![dest, message]); + + let sudo_call = pezkuwi_subxt::dynamic::tx( + "Sudo", + "sudo_unchecked_weight", + vec![ + xcm_send.into_value(), + Value::named_composite([ + ("ref_time", Value::u128(1u128)), + ("proof_size", Value::u128(1u128)), + ]), + ], + ); + + // Submit and watch + use pezkuwi_subxt::tx::TxStatus; + + // Retry up to 3 times on submit error + let mut tx_progress_opt = None; + for attempt in 0..3 { + match api + .tx() + .sign_and_submit_then_watch_default(&sudo_call, &sudo_keypair) + .await + { + Ok(p) => { + tx_progress_opt = Some(p); + break; + }, + Err(e) => { + println!(" SUBMIT ERROR (attempt {}): {}", attempt + 1, e); + tokio::time::sleep(std::time::Duration::from_secs(12)).await; + }, + } + } + let tx_progress = match tx_progress_opt { + Some(p) => p, + None => { + println!(" FAILED after 3 attempts"); + fail_count += 1; + continue; + }, + }; + + println!( + " TX: 0x{}", + hex::encode(tx_progress.extrinsic_hash().as_ref()) + ); + + let mut progress = tx_progress; + let mut tx_ok = false; + loop { + let status = progress.next().await; + match status { + Some(Ok(TxStatus::InBestBlock(details))) => { + match details.wait_for_success().await { + Ok(events) => { + let mut has_sudid = false; + let mut has_sent = false; + for event in events.iter() { + if let Ok(ev) = event { + if ev.pallet_name() == "Sudo" + && ev.variant_name() == "Sudid" + { + has_sudid = true; + } + if ev.pallet_name() == "XcmPallet" + && ev.variant_name() == "Sent" + { + has_sent = true; + } + } + } + if has_sudid && has_sent { + println!(" SUCCESS (Sudo::Sudid + XcmPallet::Sent)"); + tx_ok = true; + } else { + println!(" WARNING: Missing expected events"); + for event in events.iter() { + if let Ok(ev) = event { + println!( + " {}::{}", + ev.pallet_name(), + ev.variant_name() + ); + } + } + } + }, + Err(e) => println!(" DISPATCH ERROR: {}", e), + } + break; + }, + Some(Ok(TxStatus::Error { message })) => { + println!(" TX ERROR: {}", message); + break; + }, + Some(Ok(TxStatus::Invalid { message })) => { + println!(" TX INVALID: {}", message); + break; + }, + Some(Ok(TxStatus::Dropped { message })) => { + println!(" TX DROPPED: {}", message); + break; + }, + Some(Err(e)) => { + println!(" STREAM ERROR: {}", e); + break; + }, + None => { + println!(" STREAM ENDED"); + break; + }, + _ => {}, + } + } + + if tx_ok { + success_count += 1; + } else { + fail_count += 1; + } + + // Wait for block inclusion before sending next TX (block time = 6s) + if i + 1 < validators.len() { + tokio::time::sleep(std::time::Duration::from_secs(12)).await; + } + } + + println!("\n=== RESULTS ==="); + println!("Success: {}/{}", success_count, validators.len()); + println!("Failed: {}/{}", fail_count, validators.len()); + + if fail_count > 0 { + println!("\nSome mints failed. Check People Chain events to verify."); + } else { + println!("\nAll Welati Tiki NFTs minted successfully!"); + } + + println!("Verify on People Chain (port 41944) that all validators have citizenship NFTs."); + + Ok(()) +} diff --git a/vendor/pezkuwi-subxt/subxt/examples/remark_mainnet.rs b/vendor/pezkuwi-subxt/subxt/examples/remark_mainnet.rs new file mode 100644 index 00000000..c9e8c1dc --- /dev/null +++ b/vendor/pezkuwi-subxt/subxt/examples/remark_mainnet.rs @@ -0,0 +1,71 @@ +use pezkuwi_subxt::dynamic::Value; +use pezkuwi_subxt::{OnlineClient, PezkuwiConfig}; +use pezkuwi_subxt_signer::bip39::Mnemonic; +use pezkuwi_subxt_signer::sr25519::Keypair; +use std::str::FromStr; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let url = std::env::var("RPC_URL").unwrap_or_else(|_| "ws://217.77.6.126:9944".to_string()); + let message = std::env::var("MESSAGE").expect("MESSAGE env var required"); + + println!("RPC: {}", url); + println!("Message: {}", message); + println!("Message bytes: {}", message.len()); + + let api = OnlineClient::::from_insecure_url(&url).await?; + println!("Connected!"); + + let mnemonic_str = std::env::var("SUDO_MNEMONIC").expect("SUDO_MNEMONIC required"); + let mnemonic = Mnemonic::from_str(&mnemonic_str)?; + let keypair = Keypair::from_phrase(&mnemonic, None)?; + println!("Account: {}\n", keypair.public_key().to_account_id()); + + let remark_tx = pezkuwi_subxt::dynamic::tx( + "System", + "remark_with_event", + vec![Value::from_bytes(message.as_bytes())], + ); + + println!("Submitting remarkWithEvent..."); + + use pezkuwi_subxt::tx::TxStatus; + let tx_progress = api.tx() + .sign_and_submit_then_watch_default(&remark_tx, &keypair) + .await?; + + println!("TX hash: 0x{}", hex::encode(tx_progress.extrinsic_hash().as_ref())); + + let mut progress = tx_progress; + loop { + let status = progress.next().await; + match status { + Some(Ok(TxStatus::Validated)) => println!(" Validated"), + Some(Ok(TxStatus::Broadcasted)) => println!(" Broadcasted"), + Some(Ok(TxStatus::InBestBlock(details))) => { + println!(" InBestBlock {:?}", details.block_hash()); + match details.wait_for_success().await { + Ok(events) => { + println!(" SUCCESS!"); + for event in events.iter() { + if let Ok(ev) = event { + println!(" {}::{}", ev.pallet_name(), ev.variant_name()); + } + } + }, + Err(e) => println!(" Error: {}", e), + } + break; + }, + Some(Ok(TxStatus::Error { message })) => { println!(" ERROR: {}", message); break; }, + Some(Ok(TxStatus::Invalid { message })) => { println!(" INVALID: {}", message); break; }, + Some(Ok(TxStatus::Dropped { message })) => { println!(" DROPPED: {}", message); break; }, + Some(Err(e)) => { println!(" Error: {}", e); break; }, + None => { println!(" Stream ended"); break; }, + _ => {}, + } + } + + println!("\nDone."); + Ok(()) +} diff --git a/vendor/pezkuwi-subxt/subxt/examples/send_staking_details.rs b/vendor/pezkuwi-subxt/subxt/examples/send_staking_details.rs new file mode 100644 index 00000000..510e85b2 --- /dev/null +++ b/vendor/pezkuwi-subxt/subxt/examples/send_staking_details.rs @@ -0,0 +1,327 @@ +//! Send receive_staking_details to People Chain for all 21 validators via XCM Transact +//! +//! People Chain StakingScore pallet (index 80) has: +//! receive_staking_details(who, staked_amount, nominations_count, unlocking_chunks_count) +//! +//! This populates CachedStakingDetails on People Chain so validators can +//! call start_score_tracking() and have their staking scores calculated. +//! +//! Run with: +//! SUDO_MNEMONIC="..." cargo run --release -p pezkuwi-subxt --example send_staking_details + +#![allow(missing_docs)] +use pezkuwi_subxt::dynamic::Value; +use pezkuwi_subxt::utils::AccountId32; +use pezkuwi_subxt::{OnlineClient, PezkuwiConfig}; +use pezkuwi_subxt_signer::bip39::Mnemonic; +use pezkuwi_subxt_signer::sr25519::Keypair; +use std::str::FromStr; + +const PEOPLE_CHAIN_PARA_ID: u32 = 1004; + +// People Chain pallet indices +const STAKING_SCORE_PALLET: u8 = 80; // 0x50 +const RECEIVE_STAKING_DETAILS_CALL: u8 = 1; + +struct ValidatorInfo { + name: &'static str, + ss58: &'static str, + staked_hez: u64, // in HEZ (will be multiplied by 10^12) +} + +fn validators() -> Vec { + vec![ + ValidatorInfo { name: "Çiyager (Cihat Türkan)", ss58: "5GipBJs2uNWTCazyZQ2vG3DEqLz4tXNmNZtBAT1Mtm1orZ5i", staked_hez: 499_100 }, + ValidatorInfo { name: "Mehmet Tunç", ss58: "5HWFZbhkZuTUySXu6ZXYKrTHBnWXHvWRKLozE22zhnwXGGxk", staked_hez: 499_100 }, + ValidatorInfo { name: "Nagihan Akarsel", ss58: "5CrB5BWJfLNWEZAsAXDKXdJUGzFMXKvYnwRX4DVMcgBwxSdx", staked_hez: 499_100 }, + ValidatorInfo { name: "Sait Çürükkaya (Doktor Süleyman)", ss58: "5ELgySrX5ZyK7EWXjj6bAedyTCcTNWDANbiiipsT5gnpoCEp", staked_hez: 499_100 }, + ValidatorInfo { name: "Evdile Koçer", ss58: "5GCZQNjRdHofEHPvVq4ePrfDYcjRzQ1HQ2awHMX6AawpRYuM", staked_hez: 499_100 }, + ValidatorInfo { name: "Mam Zeki", ss58: "5H8jTzi4Gm4rbFtXw6h5enhLhgsuhNAqR5K2itmPiz83ymWy", staked_hez: 499_100 }, + ValidatorInfo { name: "Kakaî Falah", ss58: "5Fs3P5tHuL9cvwPQojsheViRRAjFkMMFa32jAkDSwW9mbTfU", staked_hez: 499_100 }, + ValidatorInfo { name: "Feryad Fazil Ömer", ss58: "5DXgq7uDXog6zcubT3wgtaYosoibjudz4w5ScPW2phLuAy3V", staked_hez: 499_100 }, + ValidatorInfo { name: "Mevlud Afand", ss58: "5FyFwbGLgPXun3azh6Gx83wCuUt5FTavb2WAVDYrjziVB9rN", staked_hez: 499_100 }, + ValidatorInfo { name: "Şêrko Fatih Şivandî", ss58: "5HEcuuypLDeJaSj6ZgH57aXhuviyeLNdw9QrCDJ8u6gsnjnL", staked_hez: 499_100 }, + ValidatorInfo { name: "Ramin Hüseyin Penahi", ss58: "5EpmpTXbMXpz6ixy3WhutdzcexzPbvybNKv4eiiN1kvTnQH5", staked_hez: 499_100 }, + ValidatorInfo { name: "Zanyar Moradi", ss58: "5DFsm3BBEgHmSEZkvwGKB7c7tiH2avhfuQE1SEjfMDGuczsW", staked_hez: 499_100 }, + ValidatorInfo { name: "Heidar Ghorbani", ss58: "5HePVUXjGSM2hVZ1YMz2V3KoX6EdQNEmmzUnUvpfGV95ofUR", staked_hez: 499_100 }, + ValidatorInfo { name: "Farhad Salimi", ss58: "5GP4nAcwtETTg1oAHQNvevmmhG8GEstGQeCirKEhaDTwpFgx", staked_hez: 499_100 }, + ValidatorInfo { name: "Vafa Azarbar", ss58: "5FYoCM3oeEGeoFY94EgXBhmABkRCabvPp72ur5bJNG3cK619", staked_hez: 499_100 }, + ValidatorInfo { name: "Dr. Aziz Mihemed", ss58: "5GspwkKF6aYzFkmAyBBQg7coSCSgDCore79fbW8uxJNAH347", staked_hez: 499_100 }, + ValidatorInfo { name: "Arîn Mîrkan", ss58: "5GmuX11pN2fC4Fyq1V7MuiYt3aevZcVQs3HZWKyzmap9bKfe", staked_hez: 499_100 }, + ValidatorInfo { name: "Ebu Leyla", ss58: "5FQptVCtM1qsxkLbQkATkw4Kio4M9LxWvM6TwgEo3QjmTXF3", staked_hez: 499_100 }, + ValidatorInfo { name: "Rêvan Kobanê", ss58: "5E7VD2qmso1yRfyq3t9u2qhauAgtmjZTybVsCARF5Zz9bXy6", staked_hez: 499_100 }, + ValidatorInfo { name: "Amanj Babani", ss58: "5Ccz5W7Q21g4UPCytzHxD3VSMLJ1BbbWSkJKFwsNtYRk3HkX", staked_hez: 499_100 }, + ValidatorInfo { name: "Xosrow Gulan", ss58: "5D7WPmK1SAJyYDdCtgqEzGJpWXQe3Lj9FqWL8z9waLTkUNv3", staked_hez: 499_100 }, + ] +} + +const PLANCK_PER_HEZ: u128 = 1_000_000_000_000; + +/// Encode StakingScore.receive_staking_details(who, staked_amount, nominations_count, unlocking_chunks_count) +/// Pallet 80 (0x50), call_index 1 +/// who: AccountId32 (32 bytes raw) +/// staked_amount: u128 LE (16 bytes) - this is T::Balance which is u128 +/// nominations_count: u32 LE (4 bytes) +/// unlocking_chunks_count: u32 LE (4 bytes) +fn encode_receive_staking_details( + account_id: &[u8; 32], + staked_amount: u128, + nominations_count: u32, + unlocking_chunks_count: u32, +) -> Vec { + let mut encoded = Vec::with_capacity(58); + encoded.push(STAKING_SCORE_PALLET); // 0x50 + encoded.push(RECEIVE_STAKING_DETAILS_CALL); // 0x01 + encoded.extend_from_slice(account_id); // 32 bytes + encoded.extend_from_slice(&staked_amount.to_le_bytes()); // 16 bytes + encoded.extend_from_slice(&nominations_count.to_le_bytes()); // 4 bytes + encoded.extend_from_slice(&unlocking_chunks_count.to_le_bytes()); // 4 bytes + encoded +} + +/// Build XCM V3 message: UnpaidExecution + Transact +fn build_xcm_values(encoded_call: &[u8]) -> (Value, Value) { + let dest = Value::unnamed_variant( + "V3", + vec![Value::named_composite([ + ("parents", Value::u128(0)), + ( + "interior", + Value::unnamed_variant( + "X1", + vec![Value::unnamed_variant( + "Teyrchain", + vec![Value::u128(PEOPLE_CHAIN_PARA_ID as u128)], + )], + ), + ), + ])], + ); + + let message = Value::unnamed_variant( + "V3", + vec![Value::unnamed_composite(vec![ + Value::named_variant( + "UnpaidExecution", + [ + ("weight_limit", Value::unnamed_variant("Unlimited", vec![])), + ("check_origin", Value::unnamed_variant("None", vec![])), + ], + ), + Value::named_variant( + "Transact", + [ + ("origin_kind", Value::unnamed_variant("Superuser", vec![])), + ( + "require_weight_at_most", + Value::named_composite([ + ("ref_time", Value::u128(10_000_000_000u128)), + ("proof_size", Value::u128(1_000_000u128)), + ]), + ), + ("call", Value::from_bytes(encoded_call)), + ], + ), + ])], + ); + + (dest, message) +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("=== SEND STAKING DETAILS TO PEOPLE CHAIN ===\n"); + + let relay_url = + std::env::var("RPC_URL").unwrap_or_else(|_| "ws://217.77.6.126:9944".to_string()); + let skip: usize = std::env::var("SKIP") + .unwrap_or_else(|_| "0".to_string()) + .parse() + .unwrap_or(0); + + let vals = validators(); + println!("Relay RPC: {}", relay_url); + println!("People Chain Para ID: {}", PEOPLE_CHAIN_PARA_ID); + println!("Validators: {} (skip {})\n", vals.len(), skip); + + // Connect to relay chain + let api = OnlineClient::::from_insecure_url(&relay_url).await?; + println!( + "Connected! specVersion: {}\n", + api.runtime_version().spec_version + ); + + // Load sudo keypair + let mnemonic_str = + std::env::var("SUDO_MNEMONIC").expect("SUDO_MNEMONIC environment variable required"); + let mnemonic = Mnemonic::from_str(&mnemonic_str)?; + let sudo_keypair = Keypair::from_phrase(&mnemonic, None)?; + println!("Sudo: {}\n", sudo_keypair.public_key().to_account_id()); + + let mut success_count = 0; + let mut fail_count = 0; + + for (i, v) in vals.iter().enumerate().skip(skip) { + let staked_planck = v.staked_hez as u128 * PLANCK_PER_HEZ; + println!("--- [{}/{}] {} ---", i + 1, vals.len(), v.name); + println!(" Address: {}", v.ss58); + println!(" Staked: {} HEZ ({} planck)", v.staked_hez, staked_planck); + + let account: AccountId32 = match v.ss58.parse() { + Ok(a) => a, + Err(e) => { + println!(" ERROR: Invalid SS58: {}", e); + fail_count += 1; + continue; + }, + }; + + // Encode receive_staking_details call + // nominations_count = 0 (validators don't nominate, they validate) + // unlocking_chunks_count = 0 (no pending unstakes) + let call = encode_receive_staking_details(&account.0, staked_planck, 0, 0); + println!( + " Call: {} bytes (0x{}...)", + call.len(), + hex::encode(&call[..6]) + ); + + // Build XCM message + let (dest, message) = build_xcm_values(&call); + + // Wrap: xcmPallet.send(dest, message) → sudo.sudo_unchecked_weight(...) + let xcm_send = pezkuwi_subxt::dynamic::tx("XcmPallet", "send", vec![dest, message]); + + let sudo_call = pezkuwi_subxt::dynamic::tx( + "Sudo", + "sudo_unchecked_weight", + vec![ + xcm_send.into_value(), + Value::named_composite([ + ("ref_time", Value::u128(1u128)), + ("proof_size", Value::u128(1u128)), + ]), + ], + ); + + // Submit with retries + use pezkuwi_subxt::tx::TxStatus; + let mut tx_progress_opt = None; + for attempt in 0..3 { + match api + .tx() + .sign_and_submit_then_watch_default(&sudo_call, &sudo_keypair) + .await + { + Ok(p) => { + tx_progress_opt = Some(p); + break; + }, + Err(e) => { + println!(" SUBMIT ERROR (attempt {}): {}", attempt + 1, e); + tokio::time::sleep(std::time::Duration::from_secs(12)).await; + }, + } + } + + let tx_progress = match tx_progress_opt { + Some(p) => p, + None => { + println!(" FAILED after 3 attempts"); + fail_count += 1; + continue; + }, + }; + + println!( + " TX: 0x{}", + hex::encode(tx_progress.extrinsic_hash().as_ref()) + ); + + let mut progress = tx_progress; + let mut tx_ok = false; + loop { + let status = progress.next().await; + match status { + Some(Ok(TxStatus::InBestBlock(details))) => { + match details.wait_for_success().await { + Ok(events) => { + let mut has_sudid = false; + let mut has_sent = false; + for event in events.iter() { + if let Ok(ev) = event { + if ev.pallet_name() == "Sudo" && ev.variant_name() == "Sudid" { + has_sudid = true; + } + if ev.pallet_name() == "XcmPallet" + && ev.variant_name() == "Sent" + { + has_sent = true; + } + } + } + if has_sudid && has_sent { + println!(" SUCCESS (Sudo::Sudid + XcmPallet::Sent)"); + tx_ok = true; + } else { + println!(" WARNING: Events:"); + for event in events.iter() { + if let Ok(ev) = event { + println!( + " {}::{}", + ev.pallet_name(), + ev.variant_name() + ); + } + } + } + }, + Err(e) => println!(" DISPATCH ERROR: {}", e), + } + break; + }, + Some(Ok(TxStatus::Error { message })) => { + println!(" TX ERROR: {}", message); + break; + }, + Some(Ok(TxStatus::Invalid { message })) => { + println!(" TX INVALID: {}", message); + break; + }, + Some(Ok(TxStatus::Dropped { message })) => { + println!(" TX DROPPED: {}", message); + break; + }, + Some(Err(e)) => { + println!(" STREAM ERROR: {}", e); + break; + }, + None => { + println!(" STREAM ENDED"); + break; + }, + _ => {}, + } + } + + if tx_ok { + success_count += 1; + } else { + fail_count += 1; + } + + // Wait between XCM sends + if i + 1 < vals.len() { + tokio::time::sleep(std::time::Duration::from_secs(12)).await; + } + } + + println!("\n=== RESULTS ==="); + println!("Success: {}/{}", success_count, vals.len()); + println!("Failed: {}/{}", fail_count, vals.len()); + println!("\nVerify on People Chain (port 41944):"); + println!(" - CachedStakingDetails[validator] should have staked_amount set"); + println!(" - Validators can now call start_score_tracking()"); + + Ok(()) +} diff --git a/vendor/pezkuwi-subxt/subxt/examples/set_pool_metadata.rs b/vendor/pezkuwi-subxt/subxt/examples/set_pool_metadata.rs new file mode 100644 index 00000000..c2265c7a --- /dev/null +++ b/vendor/pezkuwi-subxt/subxt/examples/set_pool_metadata.rs @@ -0,0 +1,191 @@ +//! Set metadata (names) for existing nomination pools on Asset Hub +//! +//! Environment variables: +//! WALLETS_FILE - JSON file with wallet list (required) +//! ASSET_HUB_RPC - Asset Hub RPC endpoint (default: ws://217.77.6.126:40944) +//! START_ID - First pool ID to set metadata for (default: 1) +//! +//! Wallets JSON format: +//! [ +//! { "name": "Pool Name", "mnemonic": "word1 word2 ...", "ss58": "5..." }, +//! ... +//! ] +//! +//! Run with: +//! WALLETS_FILE="wallets.json" \ +//! cargo run --release -p pezkuwi-subxt --example set_pool_metadata + +#![allow(missing_docs)] +use pezkuwi_subxt::dynamic::Value; +use pezkuwi_subxt::{OnlineClient, PezkuwiConfig}; +use pezkuwi_subxt_signer::bip39::Mnemonic; +use pezkuwi_subxt_signer::sr25519::Keypair; +use std::str::FromStr; + +const DEFAULT_ASSET_HUB_RPC: &str = "ws://217.77.6.126:40944"; + +#[derive(serde::Deserialize)] +struct WalletInfo { + name: String, + mnemonic: String, + #[allow(dead_code)] + ss58: String, +} + +fn load_wallets() -> Vec { + let path = std::env::var("WALLETS_FILE").expect( + "WALLETS_FILE environment variable required. \ + Point it to a JSON file with wallet entries: \ + [{\"name\": \"...\", \"mnemonic\": \"...\", \"ss58\": \"5...\"}]", + ); + let data = std::fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("Failed to read wallets file '{}': {}", path, e)); + serde_json::from_str(&data) + .unwrap_or_else(|e| panic!("Failed to parse wallets file '{}': {}", path, e)) +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("=== SET POOL METADATA ===\n"); + + let start_id: u32 = std::env::var("START_ID") + .unwrap_or_else(|_| "1".to_string()) + .parse()?; + + let rpc = + std::env::var("ASSET_HUB_RPC").unwrap_or_else(|_| DEFAULT_ASSET_HUB_RPC.to_string()); + + let api = OnlineClient::::from_insecure_url(&rpc).await?; + println!("Connected to Asset Hub!\n"); + + // First, query LastPoolId to confirm + let last_pool_query = + pezkuwi_subxt::dynamic::storage::<(), Value>("NominationPools", "LastPoolId"); + let storage = api.storage().at_latest().await?; + let last_pool = storage.entry(last_pool_query)?.try_fetch(()).await?; + if let Some(val) = last_pool { + let decoded = val.decode()?; + println!("LastPoolId raw value: {:?}", decoded); + println!( + "LastPoolId as_u128: {:?}", + decoded.as_u128() + ); + } + + let wallets = load_wallets(); + + for (i, wallet) in wallets.iter().enumerate() { + let pool_id = start_id + i as u32; + println!( + "--- [{}/{}] Pool {} -> '{}' ---", + i + 1, + wallets.len(), + pool_id, + wallet.name + ); + + let mnemonic = Mnemonic::from_str(&wallet.mnemonic)?; + let keypair = Keypair::from_phrase(&mnemonic, None)?; + println!(" Signer: {}", keypair.public_key().to_account_id()); + + let name_bytes = wallet.name.as_bytes().to_vec(); + let metadata_tx = pezkuwi_subxt::dynamic::tx( + "NominationPools", + "set_metadata", + vec![Value::u128(pool_id as u128), Value::from_bytes(&name_bytes)], + ); + + let mut ok = false; + for attempt in 0..3 { + if attempt > 0 { + println!(" Retry attempt {}...", attempt + 1); + tokio::time::sleep(std::time::Duration::from_secs(18)).await; + } + + let tx_progress = match api + .tx() + .sign_and_submit_then_watch_default(&metadata_tx, &keypair) + .await + { + Ok(p) => p, + Err(e) => { + println!(" SUBMIT ERROR (attempt {}): {}", attempt + 1, e); + continue; + }, + }; + + println!( + " TX: 0x{}", + hex::encode(tx_progress.extrinsic_hash().as_ref()) + ); + + use pezkuwi_subxt::tx::TxStatus; + let mut progress = tx_progress; + loop { + let status = progress.next().await; + match status { + Some(Ok(TxStatus::InBestBlock(details))) => { + match details.wait_for_success().await { + Ok(events) => { + println!(" SUCCESS!"); + for event in events.iter() { + if let Ok(ev) = event { + println!( + " {}::{}", + ev.pallet_name(), + ev.variant_name() + ); + } + } + ok = true; + }, + Err(e) => { + println!(" DISPATCH ERROR: {}", e); + }, + } + break; + }, + Some(Ok(TxStatus::Error { message })) => { + println!(" TX ERROR: {}", message); + break; + }, + Some(Ok(TxStatus::Invalid { message })) => { + println!(" TX INVALID: {}", message); + break; + }, + Some(Ok(TxStatus::Dropped { message })) => { + println!(" TX DROPPED: {}", message); + break; + }, + Some(Err(e)) => { + println!(" STREAM ERROR: {}", e); + break; + }, + None => { + println!(" STREAM ENDED"); + break; + }, + _ => {}, + } + } + + if ok { + break; + } + } + + if ok { + println!(" Pool {} named '{}'\n", pool_id, wallet.name); + } else { + println!(" FAILED to name pool {}\n", pool_id); + } + + // Wait between txs + if i + 1 < wallets.len() { + tokio::time::sleep(std::time::Duration::from_secs(18)).await; + } + } + + println!("\n=== DONE ==="); + Ok(()) +} diff --git a/vendor/pezkuwi-subxt/subxt/examples/transfer_to_validators.rs b/vendor/pezkuwi-subxt/subxt/examples/transfer_to_validators.rs new file mode 100644 index 00000000..c10715e4 --- /dev/null +++ b/vendor/pezkuwi-subxt/subxt/examples/transfer_to_validators.rs @@ -0,0 +1,209 @@ +//! Transfer HEZ from Founder (SQM) to all 21 validators +//! +//! Run with: +//! SUDO_MNEMONIC="..." RPC_URL="ws://217.77.6.126:9944" \ +//! AMOUNT_HEZ=500000 \ +//! cargo run --release --example transfer_to_validators + +#![allow(missing_docs)] +use pezkuwi_subxt::dynamic::Value; +use pezkuwi_subxt::utils::AccountId32; +use pezkuwi_subxt::{OnlineClient, PezkuwiConfig}; +use pezkuwi_subxt_signer::bip39::Mnemonic; +use pezkuwi_subxt_signer::sr25519::Keypair; +use std::str::FromStr; + +const PLANCKS_PER_HEZ: u128 = 1_000_000_000_000; + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("=== TRANSFER HEZ TO VALIDATORS ===\n"); + + let validators: Vec<(&str, &str)> = vec![ + ("Çiyager (Cihat Türkan)", "5GipBJs2uNWTCazyZQ2vG3DEqLz4tXNmNZtBAT1Mtm1orZ5i"), + ("Mehmet Tunç", "5HWFZbhkZuTUySXu6ZXYKrTHBnWXHvWRKLozE22zhnwXGGxk"), + ("Nagihan Akarsel", "5CrB5BWJfLNWEZAsAXDKXdJUGzFMXKvYnwRX4DVMcgBwxSdx"), + ("Sait Çürükkaya (Doktor Süleyman)", "5ELgySrX5ZyK7EWXjj6bAedyTCcTNWDANbiiipsT5gnpoCEp"), + ("Evdile Koçer", "5GCZQNjRdHofEHPvVq4ePrfDYcjRzQ1HQ2awHMX6AawpRYuM"), + ("Mam Zeki", "5H8jTzi4Gm4rbFtXw6h5enhLhgsuhNAqR5K2itmPiz83ymWy"), + ("Kakaî Falah", "5Fs3P5tHuL9cvwPQojsheViRRAjFkMMFa32jAkDSwW9mbTfU"), + ("Feryad Fazil Ömer", "5DXgq7uDXog6zcubT3wgtaYosoibjudz4w5ScPW2phLuAy3V"), + ("Mevlud Afand", "5FyFwbGLgPXun3azh6Gx83wCuUt5FTavb2WAVDYrjziVB9rN"), + ("Şêrko Fatih Şivandî", "5HEcuuypLDeJaSj6ZgH57aXhuviyeLNdw9QrCDJ8u6gsnjnL"), + ("Ramin Hüseyin Penahi", "5EpmpTXbMXpz6ixy3WhutdzcexzPbvybNKv4eiiN1kvTnQH5"), + ("Zanyar Moradi", "5DFsm3BBEgHmSEZkvwGKB7c7tiH2avhfuQE1SEjfMDGuczsW"), + ("Heidar Ghorbani", "5HePVUXjGSM2hVZ1YMz2V3KoX6EdQNEmmzUnUvpfGV95ofUR"), + ("Farhad Salimi", "5GP4nAcwtETTg1oAHQNvevmmhG8GEstGQeCirKEhaDTwpFgx"), + ("Vafa Azarbar", "5FYoCM3oeEGeoFY94EgXBhmABkRCabvPp72ur5bJNG3cK619"), + ("Dr. Aziz Mihemed", "5GspwkKF6aYzFkmAyBBQg7coSCSgDCore79fbW8uxJNAH347"), + ("Arîn Mîrkan", "5GmuX11pN2fC4Fyq1V7MuiYt3aevZcVQs3HZWKyzmap9bKfe"), + ("Ebu Leyla", "5FQptVCtM1qsxkLbQkATkw4Kio4M9LxWvM6TwgEo3QjmTXF3"), + ("Rêvan Kobanê", "5E7VD2qmso1yRfyq3t9u2qhauAgtmjZTybVsCARF5Zz9bXy6"), + ("Amanj Babani", "5Ccz5W7Q21g4UPCytzHxD3VSMLJ1BbbWSkJKFwsNtYRk3HkX"), + ("Xosrow Gulan", "5D7WPmK1SAJyYDdCtgqEzGJpWXQe3Lj9FqWL8z9waLTkUNv3"), + ]; + + let url = std::env::var("RPC_URL").unwrap_or_else(|_| "ws://217.77.6.126:9944".to_string()); + let amount_hez: u128 = std::env::var("AMOUNT_HEZ") + .unwrap_or_else(|_| "500000".to_string()) + .parse()?; + let amount_planck = amount_hez * PLANCKS_PER_HEZ; + let skip: usize = std::env::var("SKIP") + .unwrap_or_else(|_| "0".to_string()) + .parse() + .unwrap_or(0); + + println!("RPC: {}", url); + println!("Amount per validator: {} HEZ ({} TYR)", amount_hez, amount_planck); + println!( + "Total: {} HEZ to {} validators", + amount_hez * (validators.len() - skip) as u128, + validators.len() - skip + ); + + let api = OnlineClient::::from_insecure_url(&url).await?; + println!("Connected!"); + + let mnemonic_str = + std::env::var("SUDO_MNEMONIC").expect("SUDO_MNEMONIC environment variable required"); + let mnemonic = Mnemonic::from_str(&mnemonic_str)?; + let keypair = Keypair::from_phrase(&mnemonic, None)?; + println!("Sender: {}\n", keypair.public_key().to_account_id()); + + let mut success_count = 0; + let mut fail_count = 0; + + for (i, (name, ss58)) in validators.iter().enumerate().skip(skip) { + println!("--- [{}/{}] {} ---", i + 1, validators.len(), name); + + let dest: AccountId32 = match ss58.parse() { + Ok(a) => a, + Err(e) => { + println!(" ERROR: Invalid address: {}", e); + fail_count += 1; + continue; + }, + }; + + // Balances::transfer_keep_alive(dest, value) + let transfer_tx = pezkuwi_subxt::dynamic::tx( + "Balances", + "transfer_keep_alive", + vec![ + Value::unnamed_variant("Id", vec![Value::from_bytes(&dest.0)]), + Value::u128(amount_planck), + ], + ); + + // Retry up to 3 times + use pezkuwi_subxt::tx::TxStatus; + let mut tx_ok = false; + + for attempt in 0..3 { + let tx_progress = match api + .tx() + .sign_and_submit_then_watch_default(&transfer_tx, &keypair) + .await + { + Ok(p) => p, + Err(e) => { + println!(" SUBMIT ERROR (attempt {}): {}", attempt + 1, e); + tokio::time::sleep(std::time::Duration::from_secs(12)).await; + continue; + }, + }; + + println!( + " TX: 0x{}", + hex::encode(tx_progress.extrinsic_hash().as_ref()) + ); + + let mut progress = tx_progress; + loop { + let status = progress.next().await; + match status { + Some(Ok(TxStatus::InBestBlock(details))) => { + match details.wait_for_success().await { + Ok(events) => { + for event in events.iter() { + if let Ok(ev) = event { + if ev.pallet_name() == "Balances" + && ev.variant_name() == "Transfer" + { + println!( + " SUCCESS: {} HEZ transferred", + amount_hez + ); + tx_ok = true; + } + } + } + if !tx_ok { + println!(" WARNING: No Transfer event found"); + for event in events.iter() { + if let Ok(ev) = event { + println!( + " {}::{}", + ev.pallet_name(), + ev.variant_name() + ); + } + } + } + }, + Err(e) => println!(" DISPATCH ERROR: {}", e), + } + break; + }, + Some(Ok(TxStatus::Error { message })) => { + println!(" TX ERROR: {}", message); + break; + }, + Some(Ok(TxStatus::Invalid { message })) => { + println!(" TX INVALID: {}", message); + break; + }, + Some(Ok(TxStatus::Dropped { message })) => { + println!(" TX DROPPED: {}", message); + break; + }, + Some(Err(e)) => { + println!(" STREAM ERROR: {}", e); + break; + }, + None => { + println!(" STREAM ENDED"); + break; + }, + _ => {}, + } + } + + if tx_ok { + break; + } + tokio::time::sleep(std::time::Duration::from_secs(12)).await; + } + + if tx_ok { + success_count += 1; + } else { + fail_count += 1; + } + + // Wait for block inclusion before next TX + if i + 1 < validators.len() { + tokio::time::sleep(std::time::Duration::from_secs(12)).await; + } + } + + println!("\n=== RESULTS ==="); + println!("Success: {}/{}", success_count, validators.len() - skip); + println!("Failed: {}/{}", fail_count, validators.len() - skip); + println!( + "Total transferred: {} HEZ", + amount_hez * success_count as u128 + ); + + Ok(()) +} diff --git a/vendor/pezkuwi-subxt/subxt/examples/validator_welati_batch.rs b/vendor/pezkuwi-subxt/subxt/examples/validator_welati_batch.rs new file mode 100644 index 00000000..9cba0b82 --- /dev/null +++ b/vendor/pezkuwi-subxt/subxt/examples/validator_welati_batch.rs @@ -0,0 +1,402 @@ +//! Make 21 validators Welati citizens via XCM Transact batch +//! +//! People Chain has no Sudo, so we send from relay chain: +//! sudo(xcmPallet.send(Parachain(1004), Transact( +//! utility.batch_all([ +//! system.set_storage([KycStatuses, CitizenReferrers, IdentityHashes]), +//! tiki.force_mint_citizen_nft(validator) +//! ]) +//! ))) +//! +//! This sets all IdentityKyc storage AND mints Welati NFT in a single atomic batch. +//! +//! Run with: +//! SUDO_MNEMONIC="..." cargo run --release -p pezkuwi-subxt --example validator_welati_batch +//! SUDO_MNEMONIC="..." SKIP=5 cargo run --release -p pezkuwi-subxt --example validator_welati_batch + +#![allow(missing_docs)] +use pezkuwi_subxt::dynamic::Value; +use pezkuwi_subxt::utils::AccountId32; +use pezkuwi_subxt::{OnlineClient, PezkuwiConfig}; +use pezkuwi_subxt_signer::bip39::Mnemonic; +use pezkuwi_subxt_signer::sr25519::Keypair; +use std::str::FromStr; + +const PEOPLE_CHAIN_PARA_ID: u32 = 1004; + +// Founder account (referrer for all validators) +const FOUNDER_SS58: &str = "5CyuFfbF95rzBxru7c9yEsX4XmQXUxpLUcbj9RLg9K1cGiiF"; + +// People Chain pallet indices +const SYSTEM_PALLET: u8 = 0; +const UTILITY_PALLET: u8 = 40; // 0x28 +const TIKI_PALLET: u8 = 61; // 0x3d + +// Call indices +const SET_STORAGE_CALL: u8 = 4; +const BATCH_ALL_CALL: u8 = 2; +const FORCE_MINT_CALL: u8 = 2; + +struct ValidatorInfo { + name: &'static str, + ss58: &'static str, +} + +fn validators() -> Vec { + vec![ + ValidatorInfo { name: "Çiyager (Cihat Türkan)", ss58: "5GipBJs2uNWTCazyZQ2vG3DEqLz4tXNmNZtBAT1Mtm1orZ5i" }, + ValidatorInfo { name: "Mehmet Tunç", ss58: "5HWFZbhkZuTUySXu6ZXYKrTHBnWXHvWRKLozE22zhnwXGGxk" }, + ValidatorInfo { name: "Nagihan Akarsel", ss58: "5CrB5BWJfLNWEZAsAXDKXdJUGzFMXKvYnwRX4DVMcgBwxSdx" }, + ValidatorInfo { name: "Sait Çürükkaya (Doktor Süleyman)", ss58: "5ELgySrX5ZyK7EWXjj6bAedyTCcTNWDANbiiipsT5gnpoCEp" }, + ValidatorInfo { name: "Evdile Koçer", ss58: "5GCZQNjRdHofEHPvVq4ePrfDYcjRzQ1HQ2awHMX6AawpRYuM" }, + ValidatorInfo { name: "Mam Zeki", ss58: "5H8jTzi4Gm4rbFtXw6h5enhLhgsuhNAqR5K2itmPiz83ymWy" }, + ValidatorInfo { name: "Kakaî Falah", ss58: "5Fs3P5tHuL9cvwPQojsheViRRAjFkMMFa32jAkDSwW9mbTfU" }, + ValidatorInfo { name: "Feryad Fazil Ömer", ss58: "5DXgq7uDXog6zcubT3wgtaYosoibjudz4w5ScPW2phLuAy3V" }, + ValidatorInfo { name: "Mevlud Afand", ss58: "5FyFwbGLgPXun3azh6Gx83wCuUt5FTavb2WAVDYrjziVB9rN" }, + ValidatorInfo { name: "Şêrko Fatih Şivandî", ss58: "5HEcuuypLDeJaSj6ZgH57aXhuviyeLNdw9QrCDJ8u6gsnjnL" }, + ValidatorInfo { name: "Ramin Hüseyin Penahi", ss58: "5EpmpTXbMXpz6ixy3WhutdzcexzPbvybNKv4eiiN1kvTnQH5" }, + ValidatorInfo { name: "Zanyar Moradi", ss58: "5DFsm3BBEgHmSEZkvwGKB7c7tiH2avhfuQE1SEjfMDGuczsW" }, + ValidatorInfo { name: "Heidar Ghorbani", ss58: "5HePVUXjGSM2hVZ1YMz2V3KoX6EdQNEmmzUnUvpfGV95ofUR" }, + ValidatorInfo { name: "Farhad Salimi", ss58: "5GP4nAcwtETTg1oAHQNvevmmhG8GEstGQeCirKEhaDTwpFgx" }, + ValidatorInfo { name: "Vafa Azarbar", ss58: "5FYoCM3oeEGeoFY94EgXBhmABkRCabvPp72ur5bJNG3cK619" }, + ValidatorInfo { name: "Dr. Aziz Mihemed", ss58: "5GspwkKF6aYzFkmAyBBQg7coSCSgDCore79fbW8uxJNAH347" }, + ValidatorInfo { name: "Arîn Mîrkan", ss58: "5GmuX11pN2fC4Fyq1V7MuiYt3aevZcVQs3HZWKyzmap9bKfe" }, + ValidatorInfo { name: "Ebu Leyla", ss58: "5FQptVCtM1qsxkLbQkATkw4Kio4M9LxWvM6TwgEo3QjmTXF3" }, + ValidatorInfo { name: "Rêvan Kobanê", ss58: "5E7VD2qmso1yRfyq3t9u2qhauAgtmjZTybVsCARF5Zz9bXy6" }, + ValidatorInfo { name: "Amanj Babani", ss58: "5Ccz5W7Q21g4UPCytzHxD3VSMLJ1BbbWSkJKFwsNtYRk3HkX" }, + ValidatorInfo { name: "Xosrow Gulan", ss58: "5D7WPmK1SAJyYDdCtgqEzGJpWXQe3Lj9FqWL8z9waLTkUNv3" }, + ] +} + +// ====== SCALE & Storage Key Helpers ====== + +/// Compute StorageMap key with Blake2_128Concat hasher +/// key = twox128(pallet) + twox128(storage) + blake2_128(map_key) + map_key +fn storage_map_key(pallet: &str, storage: &str, map_key: &[u8]) -> Vec { + let mut key = Vec::with_capacity(16 + 16 + 16 + map_key.len()); + key.extend_from_slice(&pezsp_crypto_hashing::twox_128(pallet.as_bytes())); + key.extend_from_slice(&pezsp_crypto_hashing::twox_128(storage.as_bytes())); + key.extend_from_slice(&pezsp_crypto_hashing::blake2_128(map_key)); + key.extend_from_slice(map_key); + key +} + +/// SCALE compact encoding for small numbers (< 16384) +fn encode_compact(value: usize) -> Vec { + if value < 64 { + vec![(value as u8) << 2] + } else if value < 16384 { + let v = ((value as u16) << 2) | 0x01; + v.to_le_bytes().to_vec() + } else { + panic!("Value too large for compact encoding: {}", value); + } +} + +/// Encode system.set_storage(items: Vec<(Vec, Vec)>) +fn encode_set_storage(items: &[(Vec, Vec)]) -> Vec { + let mut encoded = vec![SYSTEM_PALLET, SET_STORAGE_CALL]; // 0x00, 0x04 + encoded.extend(encode_compact(items.len())); + for (key, value) in items { + encoded.extend(encode_compact(key.len())); + encoded.extend(key); + encoded.extend(encode_compact(value.len())); + encoded.extend(value); + } + encoded +} + +/// Encode tiki.force_mint_citizen_nft(dest: MultiAddress::Id(AccountId32)) +fn encode_force_mint(account_id: &[u8; 32]) -> Vec { + let mut encoded = Vec::with_capacity(35); + encoded.push(TIKI_PALLET); // 0x3d + encoded.push(FORCE_MINT_CALL); // 0x02 + encoded.push(0x00); // MultiAddress::Id variant + encoded.extend_from_slice(account_id); + encoded +} + +/// Encode utility.batch_all(calls: Vec) +fn encode_batch_all(calls: Vec>) -> Vec { + let mut encoded = vec![UTILITY_PALLET, BATCH_ALL_CALL]; // 0x28, 0x02 + encoded.extend(encode_compact(calls.len())); + for call in calls { + encoded.extend(call); + } + encoded +} + +const REFERRAL_PALLET: u8 = 52; // 0x34 +const FORCE_CONFIRM_REFERRAL_CALL: u8 = 1; + +/// Encode Referral.force_confirm_referral(referrer, referred) +/// call_index=1, both params are AccountId32 (raw 32 bytes, no MultiAddress) +fn encode_force_confirm_referral(referrer_id: &[u8; 32], referred_id: &[u8; 32]) -> Vec { + let mut encoded = Vec::with_capacity(66); + encoded.push(REFERRAL_PALLET); // 0x34 + encoded.push(FORCE_CONFIRM_REFERRAL_CALL); // 0x01 + encoded.extend_from_slice(referrer_id); + encoded.extend_from_slice(referred_id); + encoded +} + +/// Build encoded call: Referral.force_confirm_referral(founder, validator) +fn build_validator_batch_call( + validator_id: &[u8; 32], + founder_id: &[u8; 32], + _name: &str, +) -> Vec { + // KycStatuses, CitizenReferrers, IdentityHashes already written via set_storage. + // NFTs already minted via mint_welati_tiki.rs. + // Now just confirm referral to update ReferralCount, Referrals, ReferrerStats. + encode_force_confirm_referral(founder_id, validator_id) +} + +/// Build XCM V3 message: UnpaidExecution + Transact +fn build_xcm_values(encoded_call: &[u8]) -> (Value, Value) { + let dest = Value::unnamed_variant( + "V3", + vec![Value::named_composite([ + ("parents", Value::u128(0)), + ( + "interior", + Value::unnamed_variant( + "X1", + vec![Value::unnamed_variant( + "Teyrchain", + vec![Value::u128(PEOPLE_CHAIN_PARA_ID as u128)], + )], + ), + ), + ])], + ); + + let message = Value::unnamed_variant( + "V3", + vec![Value::unnamed_composite(vec![ + Value::named_variant( + "UnpaidExecution", + [ + ("weight_limit", Value::unnamed_variant("Unlimited", vec![])), + ("check_origin", Value::unnamed_variant("None", vec![])), + ], + ), + Value::named_variant( + "Transact", + [ + ("origin_kind", Value::unnamed_variant("Superuser", vec![])), + ( + "require_weight_at_most", + Value::named_composite([ + ("ref_time", Value::u128(10_000_000_000u128)), + ("proof_size", Value::u128(1_000_000u128)), + ]), + ), + ("call", Value::from_bytes(encoded_call)), + ], + ), + ])], + ); + + (dest, message) +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("=== VALIDATOR WELATI BATCH (XCM Transact) ===\n"); + + let relay_url = + std::env::var("RPC_URL").unwrap_or_else(|_| "ws://217.77.6.126:9944".to_string()); + let skip: usize = std::env::var("SKIP") + .unwrap_or_else(|_| "0".to_string()) + .parse() + .unwrap_or(0); + + let vals = validators(); + println!("Relay RPC: {}", relay_url); + println!("People Chain Para ID: {}", PEOPLE_CHAIN_PARA_ID); + println!("Validators: {} (skip {})\n", vals.len(), skip); + + // Parse founder account + let founder_account: AccountId32 = FOUNDER_SS58.parse()?; + println!("Founder (referrer): {}\n", FOUNDER_SS58); + + // Connect to relay chain + let api = OnlineClient::::from_insecure_url(&relay_url).await?; + println!( + "Connected! specVersion: {}\n", + api.runtime_version().spec_version + ); + + // Load sudo keypair + let mnemonic_str = + std::env::var("SUDO_MNEMONIC").expect("SUDO_MNEMONIC environment variable required"); + let mnemonic = Mnemonic::from_str(&mnemonic_str)?; + let sudo_keypair = Keypair::from_phrase(&mnemonic, None)?; + println!("Sudo: {}\n", sudo_keypair.public_key().to_account_id()); + + let mut success_count = 0; + let mut fail_count = 0; + + for (i, v) in vals.iter().enumerate().skip(skip) { + println!("--- [{}/{}] {} ---", i + 1, vals.len(), v.name); + println!(" Address: {}", v.ss58); + + let account: AccountId32 = match v.ss58.parse() { + Ok(a) => a, + Err(e) => { + println!(" ERROR: Invalid SS58: {}", e); + fail_count += 1; + continue; + }, + }; + + // Build the batch call (setStorage + force_mint) + let batch_call = build_validator_batch_call(&account.0, &founder_account.0, v.name); + println!( + " Batch call: {} bytes (0x{}...)", + batch_call.len(), + hex::encode(&batch_call[..6]) + ); + + // Build XCM message + let (dest, message) = build_xcm_values(&batch_call); + + // Wrap: xcmPallet.send(dest, message) → sudo.sudo_unchecked_weight(...) + let xcm_send = pezkuwi_subxt::dynamic::tx("XcmPallet", "send", vec![dest, message]); + + let sudo_call = pezkuwi_subxt::dynamic::tx( + "Sudo", + "sudo_unchecked_weight", + vec![ + xcm_send.into_value(), + Value::named_composite([ + ("ref_time", Value::u128(1u128)), + ("proof_size", Value::u128(1u128)), + ]), + ], + ); + + // Submit with retries + use pezkuwi_subxt::tx::TxStatus; + let mut tx_progress_opt = None; + for attempt in 0..3 { + match api + .tx() + .sign_and_submit_then_watch_default(&sudo_call, &sudo_keypair) + .await + { + Ok(p) => { + tx_progress_opt = Some(p); + break; + }, + Err(e) => { + println!(" SUBMIT ERROR (attempt {}): {}", attempt + 1, e); + tokio::time::sleep(std::time::Duration::from_secs(12)).await; + }, + } + } + + let tx_progress = match tx_progress_opt { + Some(p) => p, + None => { + println!(" FAILED after 3 attempts"); + fail_count += 1; + continue; + }, + }; + + println!( + " TX: 0x{}", + hex::encode(tx_progress.extrinsic_hash().as_ref()) + ); + + let mut progress = tx_progress; + let mut tx_ok = false; + loop { + let status = progress.next().await; + match status { + Some(Ok(TxStatus::InBestBlock(details))) => { + match details.wait_for_success().await { + Ok(events) => { + let mut has_sudid = false; + let mut has_sent = false; + for event in events.iter() { + if let Ok(ev) = event { + if ev.pallet_name() == "Sudo" && ev.variant_name() == "Sudid" { + has_sudid = true; + } + if ev.pallet_name() == "XcmPallet" + && ev.variant_name() == "Sent" + { + has_sent = true; + } + } + } + if has_sudid && has_sent { + println!(" SUCCESS (Sudo::Sudid + XcmPallet::Sent)"); + tx_ok = true; + } else { + println!(" WARNING: Events:"); + for event in events.iter() { + if let Ok(ev) = event { + println!( + " {}::{}", + ev.pallet_name(), + ev.variant_name() + ); + } + } + } + }, + Err(e) => println!(" DISPATCH ERROR: {}", e), + } + break; + }, + Some(Ok(TxStatus::Error { message })) => { + println!(" TX ERROR: {}", message); + break; + }, + Some(Ok(TxStatus::Invalid { message })) => { + println!(" TX INVALID: {}", message); + break; + }, + Some(Ok(TxStatus::Dropped { message })) => { + println!(" TX DROPPED: {}", message); + break; + }, + Some(Err(e)) => { + println!(" STREAM ERROR: {}", e); + break; + }, + None => { + println!(" STREAM ENDED"); + break; + }, + _ => {}, + } + } + + if tx_ok { + success_count += 1; + } else { + fail_count += 1; + } + + // Wait between XCM sends + if i + 1 < vals.len() { + tokio::time::sleep(std::time::Duration::from_secs(12)).await; + } + } + + println!("\n=== RESULTS ==="); + println!("Success: {}/{}", success_count, vals.len()); + println!("Failed: {}/{}", fail_count, vals.len()); + println!("\nVerify on People Chain (port 41944):"); + println!(" - KycStatuses[validator] = Approved"); + println!(" - CitizenReferrers[validator] = founder"); + println!(" - IdentityHashes[validator] = blake2_256(name)"); + println!(" - CitizenNft[validator] exists (Welati NFT minted)"); + + Ok(()) +} diff --git a/vendor/pezkuwi-subxt/subxt/examples/welati_citizenship.rs b/vendor/pezkuwi-subxt/subxt/examples/welati_citizenship.rs new file mode 100644 index 00000000..c76dc992 --- /dev/null +++ b/vendor/pezkuwi-subxt/subxt/examples/welati_citizenship.rs @@ -0,0 +1,316 @@ +//! Welati Citizenship: Transfer to People Chain + Apply + Approve + Confirm +//! +//! Steps: +//! 1. Transfer 10 HEZ from founder to each wallet on People Chain +//! 2. Each wallet applies for citizenship (IdentityKyc.apply_for_citizenship) +//! 3. Founder approves each referral (IdentityKyc.approve_referral) +//! 4. Each wallet confirms citizenship (IdentityKyc.confirm_citizenship) → Welati NFT minted +//! +//! Environment variables: +//! FOUNDER_MNEMONIC - Founder wallet mnemonic (required) +//! WALLETS_FILE - JSON file with wallet list (required) +//! PEOPLE_RPC - People Chain RPC endpoint (default: ws://217.77.6.126:41944) +//! SKIP - Number of wallets to skip (default: 0) +//! +//! Wallets JSON format: +//! [ +//! { "name": "Pool Name", "mnemonic": "word1 word2 ...", "ss58": "5..." }, +//! ... +//! ] +//! +//! Run with: +//! FOUNDER_MNEMONIC="..." WALLETS_FILE="wallets.json" \ +//! cargo run --release -p pezkuwi-subxt --example welati_citizenship +//! +//! # Or run a specific phase: +//! FOUNDER_MNEMONIC="..." WALLETS_FILE="wallets.json" \ +//! cargo run --release -p pezkuwi-subxt --example welati_citizenship -- transfer + +#![allow(missing_docs)] +use pezkuwi_subxt::dynamic::Value; +use pezkuwi_subxt::utils::AccountId32; +use pezkuwi_subxt::{OnlineClient, PezkuwiConfig}; +use pezkuwi_subxt_signer::bip39::Mnemonic; +use pezkuwi_subxt_signer::sr25519::Keypair; +use std::str::FromStr; + +const PLANCKS_PER_HEZ: u128 = 1_000_000_000_000; +const DEFAULT_PEOPLE_RPC: &str = "ws://217.77.6.126:41944"; + +#[derive(serde::Deserialize)] +struct WalletInfo { + name: String, + mnemonic: String, + ss58: String, +} + +fn load_wallets() -> Vec { + let path = std::env::var("WALLETS_FILE").expect( + "WALLETS_FILE environment variable required. \ + Point it to a JSON file with wallet entries: \ + [{\"name\": \"...\", \"mnemonic\": \"...\", \"ss58\": \"5...\"}]", + ); + let data = std::fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("Failed to read wallets file '{}': {}", path, e)); + serde_json::from_str(&data) + .unwrap_or_else(|e| panic!("Failed to parse wallets file '{}': {}", path, e)) +} + +fn founder_mnemonic() -> String { + std::env::var("FOUNDER_MNEMONIC").expect("FOUNDER_MNEMONIC environment variable required") +} + +async fn submit_and_watch( + api: &OnlineClient, + tx: pezkuwi_subxt::tx::DynamicPayload, + signer: &Keypair, + label: &str, +) -> Result> { + use pezkuwi_subxt::tx::TxStatus; + + for attempt in 0..3 { + if attempt > 0 { + println!(" Retry {}...", attempt + 1); + tokio::time::sleep(std::time::Duration::from_secs(18)).await; + } + + let tx_progress = match api + .tx() + .sign_and_submit_then_watch_default(&tx, signer) + .await + { + Ok(p) => p, + Err(e) => { + println!(" SUBMIT ERROR (attempt {}): {}", attempt + 1, e); + continue; + }, + }; + + println!( + " TX: 0x{}", + hex::encode(tx_progress.extrinsic_hash().as_ref()) + ); + + let mut progress = tx_progress; + loop { + let status = progress.next().await; + match status { + Some(Ok(TxStatus::InBestBlock(details))) => { + match details.wait_for_success().await { + Ok(events) => { + println!(" {} SUCCESS!", label); + for event in events.iter() { + if let Ok(ev) = event { + println!( + " {}::{}", + ev.pallet_name(), + ev.variant_name() + ); + } + } + return Ok(true); + }, + Err(e) => { + println!(" {} DISPATCH ERROR: {}", label, e); + return Ok(false); + }, + } + }, + Some(Ok(TxStatus::Error { message })) => { + println!(" {} TX ERROR: {}", label, message); + break; + }, + Some(Ok(TxStatus::Invalid { message })) => { + println!(" {} TX INVALID: {}", label, message); + break; + }, + Some(Ok(TxStatus::Dropped { message })) => { + println!(" {} TX DROPPED: {}", label, message); + break; + }, + Some(Err(e)) => { + println!(" {} STREAM ERROR: {}", label, e); + return Err(e.into()); + }, + None => { + println!(" {} STREAM ENDED", label); + break; + }, + _ => {}, + } + } + } + Ok(false) +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let ws = load_wallets(); + let args: Vec = std::env::args().collect(); + let phase = args.get(1).map(|s| s.as_str()).unwrap_or("all"); + let skip: usize = std::env::var("SKIP") + .unwrap_or_else(|_| "0".to_string()) + .parse() + .unwrap_or(0); + let rpc = std::env::var("PEOPLE_RPC").unwrap_or_else(|_| DEFAULT_PEOPLE_RPC.to_string()); + + println!("=== WELATI CITIZENSHIP WORKFLOW ==="); + println!("People Chain RPC: {}", rpc); + println!("Phase: {}", phase); + println!("Wallets loaded: {}", ws.len()); + println!("Skip: {}\n", skip); + + let api = OnlineClient::::from_insecure_url(&rpc).await?; + println!("Connected to People Chain!\n"); + + // ========== PHASE 1: TRANSFERS ========== + if phase == "all" || phase == "transfer" { + println!("========== PHASE 1: TRANSFER 10 HEZ TO PEOPLE CHAIN ==========\n"); + + let mnemonic = Mnemonic::from_str(&founder_mnemonic())?; + let founder_keypair = Keypair::from_phrase(&mnemonic, None)?; + println!("Founder: {}\n", founder_keypair.public_key().to_account_id()); + + for (i, w) in ws.iter().enumerate().skip(skip) { + println!("--- [{}/{}] {} ({}) ---", i + 1, ws.len(), w.name, w.ss58); + + let dest: AccountId32 = w.ss58.parse()?; + let amount = 10 * PLANCKS_PER_HEZ; + + let tx = pezkuwi_subxt::dynamic::tx( + "Balances", + "transfer_keep_alive", + vec![ + Value::unnamed_variant("Id", vec![Value::from_bytes(&dest.0)]), + Value::u128(amount), + ], + ); + + let ok = submit_and_watch(&api, tx, &founder_keypair, "TRANSFER").await?; + if !ok { + println!(" FAILED! Stopping."); + return Ok(()); + } + + if i + 1 < ws.len() { + tokio::time::sleep(std::time::Duration::from_secs(18)).await; + } + } + println!("\n========== ALL TRANSFERS DONE ==========\n"); + + if phase == "transfer" { + return Ok(()); + } + tokio::time::sleep(std::time::Duration::from_secs(18)).await; + } + + // ========== PHASE 2: APPLY FOR CITIZENSHIP ========== + if phase == "all" || phase == "apply" { + println!("========== PHASE 2: APPLY FOR CITIZENSHIP ==========\n"); + + for (i, w) in ws.iter().enumerate().skip(skip) { + println!("--- [{}/{}] {} applying ---", i + 1, ws.len(), w.name); + + let mnemonic = Mnemonic::from_str(&w.mnemonic)?; + let keypair = Keypair::from_phrase(&mnemonic, None)?; + + // Generate identity hash: H256(name) + let identity_hash = pezsp_crypto_hashing::blake2_256(w.name.as_bytes()); + + // IdentityKyc.apply_for_citizenship(identity_hash, referrer=None) + // referrer=None will default to founder + let tx = pezkuwi_subxt::dynamic::tx( + "IdentityKyc", + "apply_for_citizenship", + vec![ + Value::from_bytes(&identity_hash), + Value::unnamed_variant("None", vec![]), + ], + ); + + let ok = submit_and_watch(&api, tx, &keypair, "APPLY").await?; + if !ok { + println!(" FAILED! Continuing..."); + } + + if i + 1 < ws.len() { + tokio::time::sleep(std::time::Duration::from_secs(18)).await; + } + } + println!("\n========== ALL APPLICATIONS SUBMITTED ==========\n"); + + if phase == "apply" { + return Ok(()); + } + tokio::time::sleep(std::time::Duration::from_secs(18)).await; + } + + // ========== PHASE 3: FOUNDER APPROVES REFERRALS ========== + if phase == "all" || phase == "approve" { + println!("========== PHASE 3: FOUNDER APPROVES REFERRALS ==========\n"); + + let mnemonic = Mnemonic::from_str(&founder_mnemonic())?; + let founder_keypair = Keypair::from_phrase(&mnemonic, None)?; + println!("Founder: {}\n", founder_keypair.public_key().to_account_id()); + + for (i, w) in ws.iter().enumerate().skip(skip) { + println!("--- [{}/{}] Approving {} ---", i + 1, ws.len(), w.name); + + let applicant: AccountId32 = w.ss58.parse()?; + + // IdentityKyc.approve_referral(applicant) + let tx = pezkuwi_subxt::dynamic::tx( + "IdentityKyc", + "approve_referral", + vec![Value::from_bytes(&applicant.0)], + ); + + let ok = submit_and_watch(&api, tx, &founder_keypair, "APPROVE").await?; + if !ok { + println!(" FAILED! Continuing..."); + } + + if i + 1 < ws.len() { + tokio::time::sleep(std::time::Duration::from_secs(18)).await; + } + } + println!("\n========== ALL REFERRALS APPROVED ==========\n"); + + if phase == "approve" { + return Ok(()); + } + tokio::time::sleep(std::time::Duration::from_secs(18)).await; + } + + // ========== PHASE 4: CONFIRM CITIZENSHIP (MINT WELATI) ========== + if phase == "all" || phase == "confirm" { + println!("========== PHASE 4: CONFIRM CITIZENSHIP ==========\n"); + + for (i, w) in ws.iter().enumerate().skip(skip) { + println!("--- [{}/{}] {} confirming ---", i + 1, ws.len(), w.name); + + let mnemonic = Mnemonic::from_str(&w.mnemonic)?; + let keypair = Keypair::from_phrase(&mnemonic, None)?; + + // IdentityKyc.confirm_citizenship() + let tx = pezkuwi_subxt::dynamic::tx( + "IdentityKyc", + "confirm_citizenship", + Vec::::new(), + ); + + let ok = submit_and_watch(&api, tx, &keypair, "CONFIRM").await?; + if !ok { + println!(" FAILED! Continuing..."); + } + + if i + 1 < ws.len() { + tokio::time::sleep(std::time::Duration::from_secs(18)).await; + } + } + println!("\n========== ALL CITIZENSHIPS CONFIRMED ==========\n"); + } + + println!("=== DONE ==="); + Ok(()) +} diff --git a/vendor/pezkuwi-subxt/subxt/examples/zagros_deregister.rs b/vendor/pezkuwi-subxt/subxt/examples/zagros_deregister.rs new file mode 100644 index 00000000..b3c463fd --- /dev/null +++ b/vendor/pezkuwi-subxt/subxt/examples/zagros_deregister.rs @@ -0,0 +1,287 @@ +//! Zagros: List validators and optionally deregister via ValidatorManager +//! +//! Step 1 (DRY RUN by default): List all validators, show which to keep/remove +//! Step 2 (with EXECUTE=1): Actually submit the deregister tx +//! +//! Run with: +//! RPC_URL="ws://217.77.6.126:9948" \ +//! cargo run --release --example zagros_deregister -p pezkuwi-subxt +//! +//! To actually execute: +//! SUDO_MNEMONIC="******" EXECUTE=1 KEEP=2 \ +//! RPC_URL="ws://217.77.6.126:9948" \ +//! cargo run --release --example zagros_deregister -p pezkuwi-subxt + +#![allow(missing_docs)] +use pezkuwi_subxt::dynamic::Value; +use pezkuwi_subxt::{OnlineClient, PezkuwiConfig}; +use pezkuwi_subxt_signer::bip39::Mnemonic; +use pezkuwi_subxt_signer::sr25519::Keypair; +use std::str::FromStr; + +/// Decode SCALE compact length prefix +fn decode_compact(data: &[u8]) -> (usize, usize) { + let first = data[0]; + match first & 0x03 { + 0 => ((first >> 2) as usize, 1), + 1 => { + let val = (((data[1] as u16) << 8 | first as u16) >> 2) as usize; + (val, 2) + }, + 2 => { + let val = (((data[3] as u32) << 24) + | ((data[2] as u32) << 16) + | ((data[1] as u32) << 8) + | (first as u32)) + >> 2; + (val as usize, 4) + }, + _ => panic!("Big integer compact encoding not supported"), + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("=== ZAGROS: VALIDATOR DEREGISTRATION ===\n"); + + let url = std::env::var("RPC_URL").unwrap_or_else(|_| "ws://217.77.6.126:9948".to_string()); + let keep: usize = std::env::var("KEEP") + .unwrap_or_else(|_| "2".to_string()) + .parse()?; + let execute = std::env::var("EXECUTE").unwrap_or_default() == "1"; + + println!("RPC: {}", url); + println!("Keep: {} validators", keep); + println!("Mode: {}\n", if execute { "EXECUTE" } else { "DRY RUN" }); + + let api = OnlineClient::::from_insecure_url(&url).await?; + println!("Connected! specVersion: {}\n", api.runtime_version().spec_version); + + // Query QueuedKeys via raw storage — we know this key works and returns 21 entries + // QueuedKeys = Vec<(ValidatorId, Keys)> + // Storage key: twox128("Session") + twox128("QueuedKeys") + let queued_keys_key = + hex::decode("cec5070d609dd3497f72bde07fc96ba088dcde934c658227ee1dfafcd6e16903") + .unwrap(); + + let raw_data = api + .storage() + .at_latest() + .await? + .fetch_raw(queued_keys_key) + .await?; + + if raw_data.is_empty() { + println!("ERROR: QueuedKeys storage is empty!"); + return Ok(()); + } + + // Decode Vec length + let (count, mut offset) = decode_compact(&raw_data); + println!("QueuedKeys entries: {}", count); + + // Each entry: AccountId32 (32 bytes) + SessionKeys + // SessionKeys for relay chain: + // grandpa: 32 bytes + // babe: 32 bytes + // im_online: 32 bytes (ImOnlineId) + // para_validator: 32 bytes + // para_assignment: 32 bytes + // authority_discovery: 32 bytes + // beefy: 33 bytes (ECDSA compressed) + // Total SessionKeys = 32*6 + 33 = 225 bytes + // Each entry = 32 (AccountId) + 225 (SessionKeys) = 257 bytes + + // But we need to verify this. Let's compute expected total size: + let expected_entry_size = 32 + (32 * 6 + 33); // 257 + let expected_total = 1 + (count * expected_entry_size); // 1 byte compact + entries + println!( + "Expected data size: {} bytes, actual: {} bytes", + expected_total, + raw_data.len() + ); + + if raw_data.len() < offset + count * expected_entry_size { + // Try without beefy (older runtime might not have it) + let entry_no_beefy = 32 + (32 * 6); // 224 + let expected_no_beefy = 1 + (count * entry_no_beefy); + println!( + "Without beefy: expected {} bytes", + expected_no_beefy + ); + + if raw_data.len() >= offset + count * entry_no_beefy { + println!("Using SessionKeys without Beefy (6 keys x 32 bytes)"); + extract_and_process( + &raw_data, + offset, + count, + entry_no_beefy, + keep, + execute, + &api, + ) + .await?; + } else { + // Auto-detect entry size + let remaining = raw_data.len() - offset; + let entry_size = remaining / count; + println!( + "Auto-detected entry size: {} bytes (remaining={}, count={})", + entry_size, remaining, count + ); + extract_and_process(&raw_data, offset, count, entry_size, keep, execute, &api) + .await?; + } + } else { + println!("Using SessionKeys with Beefy (6 keys x 32 + 33 beefy)"); + extract_and_process( + &raw_data, + offset, + count, + expected_entry_size, + keep, + execute, + &api, + ) + .await?; + } + + Ok(()) +} + +async fn extract_and_process( + raw_data: &[u8], + mut offset: usize, + count: usize, + entry_size: usize, + keep: usize, + execute: bool, + api: &OnlineClient, +) -> Result<(), Box> { + let mut all_validators: Vec> = Vec::new(); + + println!("\nValidators:\n"); + for i in 0..count { + let account = raw_data[offset..offset + 32].to_vec(); + let label = if i < keep { "KEEP " } else { "REMOVE" }; + println!(" [{:2}] [{}] 0x{}", i + 1, label, hex::encode(&account)); + all_validators.push(account); + offset += entry_size; + } + + if count <= keep { + println!("\nAlready at {} validators, nothing to remove.", count); + return Ok(()); + } + + let to_remove = &all_validators[keep..]; + println!("\n--- Summary ---"); + println!("Total: {}", count); + println!("Keep: {}", keep); + println!("Remove: {}", to_remove.len()); + + if !execute { + println!("\nDRY RUN complete. Set EXECUTE=1 and SUDO_MNEMONIC to submit."); + return Ok(()); + } + + // Load sudo key + let mnemonic_str = + std::env::var("SUDO_MNEMONIC").expect("SUDO_MNEMONIC environment variable required"); + let mnemonic = Mnemonic::from_str(&mnemonic_str)?; + let sudo_keypair = Keypair::from_phrase(&mnemonic, None)?; + println!("\nSudo account: {}", sudo_keypair.public_key().to_account_id()); + + // Build validators list for deregister call + let validators_value: Vec = to_remove.iter().map(|v| Value::from_bytes(v)).collect(); + + let deregister_call = pezkuwi_subxt::dynamic::tx( + "ValidatorManager", + "deregister_validators", + vec![Value::unnamed_composite(validators_value)], + ); + + let sudo_call = + pezkuwi_subxt::dynamic::tx("Sudo", "sudo", vec![deregister_call.into_value()]); + + println!("Submitting sudo(validatorManager.deregister_validators)...\n"); + + use pezkuwi_subxt::tx::TxStatus; + + let tx_progress = api + .tx() + .sign_and_submit_then_watch_default(&sudo_call, &sudo_keypair) + .await?; + + println!( + " TX: 0x{}", + hex::encode(tx_progress.extrinsic_hash().as_ref()) + ); + + let mut progress = tx_progress; + let mut success = false; + loop { + let status = progress.next().await; + match status { + Some(Ok(TxStatus::InBestBlock(details))) => { + match details.wait_for_success().await { + Ok(events) => { + println!(" In best block! Events:"); + for event in events.iter() { + if let Ok(ev) = event { + println!(" {}::{}", ev.pallet_name(), ev.variant_name()); + if ev.pallet_name() == "Sudo" && ev.variant_name() == "Sudid" { + success = true; + } + if ev.pallet_name() == "ValidatorManager" + && ev.variant_name() == "ValidatorsDeregistered" + { + println!( + " >>> ValidatorsDeregistered event confirmed!" + ); + } + } + } + }, + Err(e) => println!(" DISPATCH ERROR: {}", e), + } + break; + }, + Some(Ok(TxStatus::Error { message })) => { + println!(" TX ERROR: {}", message); + break; + }, + Some(Ok(TxStatus::Invalid { message })) => { + println!(" TX INVALID: {}", message); + break; + }, + Some(Ok(TxStatus::Dropped { message })) => { + println!(" TX DROPPED: {}", message); + break; + }, + Some(Err(e)) => { + println!(" STREAM ERROR: {}", e); + break; + }, + None => { + println!(" STREAM ENDED"); + break; + }, + _ => {}, + } + } + + if success { + println!( + "\nSUCCESS! {} validators queued for deregistration.", + to_remove.len() + ); + println!("The change will take effect at current_session + 2."); + println!("Monitor GRANDPA authorities to confirm."); + } else { + println!("\nFAILED!"); + } + + Ok(()) +} diff --git a/vendor/pezkuwi-subxt/subxt/examples/zagros_diagnose.rs b/vendor/pezkuwi-subxt/subxt/examples/zagros_diagnose.rs new file mode 100644 index 00000000..75a3dcb1 --- /dev/null +++ b/vendor/pezkuwi-subxt/subxt/examples/zagros_diagnose.rs @@ -0,0 +1,253 @@ +//! Zagros: Diagnose ValidatorsToRetire storage +//! +//! This script: +//! 1. Reads QueuedKeys to get validator #5 (the first one to remove if keeping 4) +//! 2. Deregisters just that ONE validator via ValidatorManager +//! 3. Immediately reads ValidatorsToRetire to verify it was populated +//! +//! Run with: +//! SUDO_MNEMONIC="..." RPC_URL="ws://217.77.6.126:9948" \ +//! cargo run --release --example zagros_diagnose -p pezkuwi-subxt + +#![allow(missing_docs)] +use pezkuwi_subxt::dynamic::Value; +use pezkuwi_subxt::{OnlineClient, PezkuwiConfig}; +use pezkuwi_subxt_signer::bip39::Mnemonic; +use pezkuwi_subxt_signer::sr25519::Keypair; +use std::str::FromStr; + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("=== ZAGROS DEREGISTER DIAGNOSTIC ===\n"); + + let url = std::env::var("RPC_URL").unwrap_or_else(|_| "ws://217.77.6.126:9948".to_string()); + let api = OnlineClient::::from_insecure_url(&url).await?; + println!("Connected! specVersion: {}\n", api.runtime_version().spec_version); + + // Storage keys + let queued_keys_key = + hex::decode("cec5070d609dd3497f72bde07fc96ba088dcde934c658227ee1dfafcd6e16903") + .unwrap(); + let validators_to_retire_key = + hex::decode("084e7f70a295a190e2e33fd3f8cdfcc2b664fa73499821e43a617aa0e82b17b1") + .unwrap(); + + // Step 1: Check ValidatorsToRetire BEFORE + println!("=== STEP 1: Check ValidatorsToRetire BEFORE deregister ==="); + let retire_before = api + .storage() + .at_latest() + .await? + .fetch_raw(validators_to_retire_key.clone()) + .await?; + if retire_before.is_empty() { + println!(" ValidatorsToRetire: EMPTY (as expected)\n"); + } else { + println!( + " ValidatorsToRetire: {} bytes (already has data!)\n", + retire_before.len() + ); + } + + // Step 2: Get validator #5 from QueuedKeys + println!("=== STEP 2: Get test validator from QueuedKeys ==="); + let raw_data = api + .storage() + .at_latest() + .await? + .fetch_raw(queued_keys_key) + .await?; + let count = (raw_data[0] >> 2) as usize; + let remaining = raw_data.len() - 1; + let entry_size = remaining / count; + println!(" QueuedKeys: {} entries, {} bytes/entry", count, entry_size); + + if count <= 4 { + println!(" Only {} validators, nothing to deregister", count); + return Ok(()); + } + + // Get validator #5 (index 4, the first one to remove) + let test_offset = 1 + 4 * entry_size; + let test_validator = raw_data[test_offset..test_offset + 32].to_vec(); + println!( + " Test validator (index 5): 0x{}\n", + hex::encode(&test_validator) + ); + + // Step 3: Load sudo key and submit deregister for ONE validator + println!("=== STEP 3: Submit deregister for ONE validator ==="); + let mnemonic_str = + std::env::var("SUDO_MNEMONIC").expect("SUDO_MNEMONIC environment variable required"); + let mnemonic = Mnemonic::from_str(&mnemonic_str)?; + let sudo_keypair = Keypair::from_phrase(&mnemonic, None)?; + println!( + " Sudo account: {}", + sudo_keypair.public_key().to_account_id() + ); + + // Try TWO different encoding approaches + + // Approach A: Value::from_bytes (what we used before) + println!("\n --- Approach A: Value::from_bytes ---"); + let val_a = Value::from_bytes(&test_validator); + println!(" Value type: {:?}", val_a); + + // Approach B: Value::unnamed_composite with raw bytes + println!("\n --- Approach B: Try AccountId32 from subxt ---"); + // In subxt, AccountId32 can be created from [u8; 32] + let mut arr = [0u8; 32]; + arr.copy_from_slice(&test_validator); + + // Use approach A (same as before) to see if storage gets populated + let validators_value = vec![Value::from_bytes(&test_validator)]; + let deregister_call = pezkuwi_subxt::dynamic::tx( + "ValidatorManager", + "deregister_validators", + vec![Value::unnamed_composite(validators_value)], + ); + + // Print the encoded call data to debug + println!("\n Deregister call value: {:?}", deregister_call.call_data()); + + let sudo_call = + pezkuwi_subxt::dynamic::tx("Sudo", "sudo", vec![deregister_call.into_value()]); + + println!("\n Submitting sudo(validatorManager.deregister_validators([1 validator]))..."); + + use pezkuwi_subxt::tx::TxStatus; + + let tx_progress = api + .tx() + .sign_and_submit_then_watch_default(&sudo_call, &sudo_keypair) + .await?; + + println!( + " TX: 0x{}", + hex::encode(tx_progress.extrinsic_hash().as_ref()) + ); + + let mut progress = tx_progress; + let mut success = false; + loop { + let status = progress.next().await; + match status { + Some(Ok(TxStatus::InBestBlock(details))) => { + match details.wait_for_success().await { + Ok(events) => { + println!(" In best block! Events:"); + for event in events.iter() { + if let Ok(ev) = event { + println!(" {}::{}", ev.pallet_name(), ev.variant_name()); + if ev.pallet_name() == "Sudo" && ev.variant_name() == "Sudid" { + success = true; + } + if ev.pallet_name() == "ValidatorManager" + && ev.variant_name() == "ValidatorsDeregistered" + { + // Try to decode the event data + println!( + " >>> ValidatorsDeregistered event!" + ); + let bytes = ev.field_bytes(); + println!(" >>> Event field bytes ({} bytes): 0x{}", bytes.len(), hex::encode(&bytes[..std::cmp::min(bytes.len(), 128)])); + } + } + } + }, + Err(e) => println!(" DISPATCH ERROR: {}", e), + } + break; + }, + Some(Ok(TxStatus::Error { message })) => { + println!(" TX ERROR: {}", message); + break; + }, + Some(Ok(TxStatus::Invalid { message })) => { + println!(" TX INVALID: {}", message); + break; + }, + Some(Ok(TxStatus::Dropped { message })) => { + println!(" TX DROPPED: {}", message); + break; + }, + Some(Err(e)) => { + println!(" STREAM ERROR: {}", e); + break; + }, + None => { + println!(" STREAM ENDED"); + break; + }, + _ => {}, + } + } + + if !success { + println!("\n TX FAILED!"); + return Ok(()); + } + + // Step 4: IMMEDIATELY check ValidatorsToRetire AFTER + println!("\n=== STEP 4: Check ValidatorsToRetire AFTER deregister ==="); + + // Small delay to ensure state is updated + tokio::time::sleep(std::time::Duration::from_secs(2)).await; + + let retire_after = api + .storage() + .at_latest() + .await? + .fetch_raw(validators_to_retire_key.clone()) + .await?; + if retire_after.is_empty() { + println!(" ValidatorsToRetire: EMPTY !!! (deregister didn't populate storage!)"); + println!(" THIS IS THE BUG!"); + } else { + println!( + " ValidatorsToRetire: {} bytes", + retire_after.len() + ); + println!(" Raw hex: 0x{}", hex::encode(&retire_after)); + + // Decode it + let count = (retire_after[0] >> 2) as usize; + println!(" Decoded count: {}", count); + let mut offset = 1; + for i in 0..count { + if offset + 32 <= retire_after.len() { + let account = &retire_after[offset..offset + 32]; + println!(" [{}] 0x{}", i + 1, hex::encode(account)); + offset += 32; + } + } + + // Check if the stored AccountId matches what we sent + if count > 0 && retire_after.len() >= 33 { + let stored = &retire_after[1..33]; + if stored == test_validator.as_slice() { + println!("\n MATCH! Stored AccountId matches sent AccountId."); + } else { + println!("\n MISMATCH! Stored AccountId does NOT match!"); + println!(" Sent: 0x{}", hex::encode(&test_validator)); + println!(" Stored: 0x{}", hex::encode(stored)); + } + } + } + + // Step 5: Re-read raw storage one more time to triple-check + println!("\n=== STEP 5: Final raw storage check ==="); + let retire_final = api + .storage() + .at_latest() + .await? + .fetch_raw(validators_to_retire_key.clone()) + .await?; + println!(" ValidatorsToRetire final: {} bytes", retire_final.len()); + if !retire_final.is_empty() { + println!(" Raw: 0x{}", hex::encode(&retire_final)); + } + + println!("\n=== DIAGNOSTIC COMPLETE ==="); + Ok(()) +} diff --git a/vendor/pezkuwi-subxt/subxt/examples/zagros_force_new_era.rs b/vendor/pezkuwi-subxt/subxt/examples/zagros_force_new_era.rs new file mode 100644 index 00000000..83640085 --- /dev/null +++ b/vendor/pezkuwi-subxt/subxt/examples/zagros_force_new_era.rs @@ -0,0 +1,43 @@ +//! Zagros Testnet: Force new era via sudo +//! +//! Run with: +//! SUDO_MNEMONIC="******" \ +//! RPC_URL="ws://217.77.6.126:9948" \ +//! cargo run --release --example zagros_force_new_era + +#![allow(missing_docs)] +use pezkuwi_subxt::dynamic::Value; +use pezkuwi_subxt::{OnlineClient, PezkuwiConfig}; +use pezkuwi_subxt_signer::bip39::Mnemonic; +use pezkuwi_subxt_signer::sr25519::Keypair; +use std::str::FromStr; + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("=== ZAGROS FORCE NEW ERA ===\n"); + + let url = std::env::var("RPC_URL").unwrap_or_else(|_| "ws://217.77.6.126:9948".to_string()); + + let api = OnlineClient::::from_insecure_url(&url).await?; + println!("Connected to {}", url); + + let mnemonic_str = + std::env::var("SUDO_MNEMONIC").expect("SUDO_MNEMONIC environment variable required"); + let mnemonic = Mnemonic::from_str(&mnemonic_str)?; + let sudo_keypair = Keypair::from_phrase(&mnemonic, None)?; + println!("Sudo account: {}\n", sudo_keypair.public_key().to_account_id()); + + println!("Submitting sudo(staking.forceNewEra())..."); + + let force_era_call = + pezkuwi_subxt::dynamic::tx("Staking", "force_new_era", Vec::::new()); + + let sudo_tx = + pezkuwi_subxt::dynamic::tx("Sudo", "sudo", vec![force_era_call.into_value()]); + + let tx_hash = api.tx().sign_and_submit_default(&sudo_tx, &sudo_keypair).await?; + println!("Submitted! TX hash: 0x{}", hex::encode(tx_hash.as_ref())); + + println!("\nDone. ForceNewEra triggered."); + Ok(()) +} diff --git a/vendor/pezkuwi-subxt/subxt/examples/zagros_reduce_validators.rs b/vendor/pezkuwi-subxt/subxt/examples/zagros_reduce_validators.rs new file mode 100644 index 00000000..19dddcd3 --- /dev/null +++ b/vendor/pezkuwi-subxt/subxt/examples/zagros_reduce_validators.rs @@ -0,0 +1,84 @@ +//! Zagros Testnet: Reduce validator count from 21 to 4 via sudo +//! +//! Sends two sudo calls: +//! 1. sudo(staking.setValidatorCount(4)) +//! 2. sudo(staking.forceNewEra()) +//! +//! Run with: +//! SUDO_MNEMONIC="******" \ +//! RPC_URL="ws://217.77.6.126:9948" \ +//! cargo run --release --example zagros_reduce_validators + +#![allow(missing_docs)] +use pezkuwi_subxt::dynamic::Value; +use pezkuwi_subxt::{OnlineClient, PezkuwiConfig}; +use pezkuwi_subxt_signer::bip39::Mnemonic; +use pezkuwi_subxt_signer::sr25519::Keypair; +use std::str::FromStr; + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("=== ZAGROS VALIDATOR COUNT REDUCTION ===\n"); + + let url = std::env::var("RPC_URL").unwrap_or_else(|_| "ws://217.77.6.126:9948".to_string()); + let new_count: u32 = std::env::var("VALIDATOR_COUNT") + .unwrap_or_else(|_| "4".to_string()) + .parse()?; + + println!("RPC: {}", url); + println!("Target validator count: {}", new_count); + + // Connect (insecure ws:// allowed for local/VPS connections) + let api = OnlineClient::::from_insecure_url(&url).await?; + println!("Connected!"); + + // Load sudo key + let mnemonic_str = + std::env::var("SUDO_MNEMONIC").expect("SUDO_MNEMONIC environment variable required"); + let mnemonic = Mnemonic::from_str(&mnemonic_str)?; + let sudo_keypair = Keypair::from_phrase(&mnemonic, None)?; + let sudo_address = sudo_keypair.public_key().to_account_id(); + println!("Sudo account: {}\n", sudo_address); + + // Step 1: sudo(staking.setValidatorCount(new_count)) + println!("[1/2] Setting validator count to {}...", new_count); + + let set_count_call = pezkuwi_subxt::dynamic::tx("Staking", "set_validator_count", vec![ + Value::u128(new_count as u128), + ]); + + let sudo_tx_1 = + pezkuwi_subxt::dynamic::tx("Sudo", "sudo", vec![set_count_call.into_value()]); + + // Use sign_and_submit_default (does NOT wait for finalization) + let tx_hash_1 = api.tx().sign_and_submit_default(&sudo_tx_1, &sudo_keypair).await?; + println!(" Submitted! TX hash: 0x{}", hex::encode(tx_hash_1.as_ref())); + + // Wait a bit for the tx to be included in a block + println!(" Waiting 12 seconds for block inclusion..."); + tokio::time::sleep(std::time::Duration::from_secs(12)).await; + + // Step 2: sudo(staking.forceNewEra()) + println!("\n[2/2] Forcing new era..."); + + let force_era_call = + pezkuwi_subxt::dynamic::tx("Staking", "force_new_era", Vec::::new()); + + let sudo_tx_2 = + pezkuwi_subxt::dynamic::tx("Sudo", "sudo", vec![force_era_call.into_value()]); + + let tx_hash_2 = api.tx().sign_and_submit_default(&sudo_tx_2, &sudo_keypair).await?; + println!(" Submitted! TX hash: 0x{}", hex::encode(tx_hash_2.as_ref())); + + println!("\n=== DONE ==="); + println!("Both sudo calls submitted successfully."); + println!("Validator count: 21 -> {}", new_count); + println!("ForceNewEra triggered."); + println!(); + println!("Next steps:"); + println!(" - Wait for next era boundary (session change)"); + println!(" - GRANDPA should start finalizing with {} validators", new_count); + println!(" - Monitor: curl -s -d '{{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"chain_getFinalizedHead\",\"params\":[]}}' -H 'Content-Type: application/json' http://217.77.6.126:9948"); + + Ok(()) +} diff --git a/vendor/pezkuwi-subxt/subxt/examples/zagros_set_retire.rs b/vendor/pezkuwi-subxt/subxt/examples/zagros_set_retire.rs new file mode 100644 index 00000000..42a35f71 --- /dev/null +++ b/vendor/pezkuwi-subxt/subxt/examples/zagros_set_retire.rs @@ -0,0 +1,275 @@ +//! Zagros: Directly write ValidatorsToRetire via sudo(system.setStorage) +//! +//! This bypasses subxt's dynamic encoding by manually SCALE-encoding the data. +//! +//! Run with: +//! SUDO_MNEMONIC="..." KEEP=4 RPC_URL="ws://217.77.6.126:9948" \ +//! cargo run --release --example zagros_set_retire -p pezkuwi-subxt + +#![allow(missing_docs)] +use pezkuwi_subxt::dynamic::Value; +use pezkuwi_subxt::{OnlineClient, PezkuwiConfig}; +use pezkuwi_subxt_signer::bip39::Mnemonic; +use pezkuwi_subxt_signer::sr25519::Keypair; +use std::str::FromStr; + +/// SCALE encode a compact unsigned integer +fn encode_compact(value: usize) -> Vec { + if value < 64 { + vec![(value as u8) << 2] + } else if value < 16384 { + let v = ((value as u16) << 2) | 0x01; + v.to_le_bytes().to_vec() + } else { + panic!("Value too large for compact encoding: {}", value); + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("=== ZAGROS: SET ValidatorsToRetire via setStorage ===\n"); + + let url = std::env::var("RPC_URL").unwrap_or_else(|_| "ws://217.77.6.126:9948".to_string()); + let keep: usize = std::env::var("KEEP") + .unwrap_or_else(|_| "4".to_string()) + .parse()?; + + println!("RPC: {}", url); + println!("Keep: {} validators\n", keep); + + let api = OnlineClient::::from_insecure_url(&url).await?; + println!( + "Connected! specVersion: {}\n", + api.runtime_version().spec_version + ); + + // Verify genesis hash (Zagros = 0xbb4a61ab...) + let genesis = format!( + "0x{}", + hex::encode(api.genesis_hash().as_ref()) + ); + println!("Genesis: {}", genesis); + if !genesis.starts_with("0xbb4a61ab") { + println!("ERROR: This is NOT Zagros! Aborting."); + return Ok(()); + } + println!("Confirmed: This is Zagros testnet.\n"); + + // Read QueuedKeys to get all validator AccountIds + let queued_keys_key = + hex::decode("cec5070d609dd3497f72bde07fc96ba088dcde934c658227ee1dfafcd6e16903") + .unwrap(); + + let raw_data = api + .storage() + .at_latest() + .await? + .fetch_raw(queued_keys_key) + .await?; + + let count = (raw_data[0] >> 2) as usize; + let remaining = raw_data.len() - 1; + let entry_size = remaining / count; + println!("QueuedKeys: {} entries, {} bytes/entry", count, entry_size); + + if count <= keep { + println!("Only {} validators, nothing to remove.", count); + return Ok(()); + } + + // Extract all validator AccountIds + let mut all_validators: Vec> = Vec::new(); + for i in 0..count { + let offset = 1 + i * entry_size; + let account = raw_data[offset..offset + 32].to_vec(); + all_validators.push(account); + } + + let to_remove = &all_validators[keep..]; + println!("\nValidators to KEEP:"); + for (i, v) in all_validators[..keep].iter().enumerate() { + println!(" [{:2}] KEEP 0x{}", i + 1, hex::encode(v)); + } + println!("\nValidators to REMOVE:"); + for (i, v) in to_remove.iter().enumerate() { + println!(" [{:2}] REMOVE 0x{}", keep + i + 1, hex::encode(v)); + } + + // SCALE-encode Vec manually + // Format: compact_length ++ (32 bytes × N) + let mut encoded_retire = encode_compact(to_remove.len()); + for v in to_remove { + encoded_retire.extend_from_slice(v); + } + println!( + "\nSCALE-encoded ValidatorsToRetire: {} bytes", + encoded_retire.len() + ); + println!( + " compact_length: 0x{} (count={})", + hex::encode(&encode_compact(to_remove.len())), + to_remove.len() + ); + + // Storage key for ValidatorsToRetire + let validators_to_retire_key = + hex::decode("084e7f70a295a190e2e33fd3f8cdfcc2b664fa73499821e43a617aa0e82b17b1") + .unwrap(); + + println!( + "\nStorage key: 0x{}", + hex::encode(&validators_to_retire_key) + ); + println!( + "Storage value: 0x{}...({} bytes)", + hex::encode(&encoded_retire[..std::cmp::min(encoded_retire.len(), 40)]), + encoded_retire.len() + ); + + // Load sudo key + let mnemonic_str = + std::env::var("SUDO_MNEMONIC").expect("SUDO_MNEMONIC environment variable required"); + let mnemonic = Mnemonic::from_str(&mnemonic_str)?; + let sudo_keypair = Keypair::from_phrase(&mnemonic, None)?; + println!( + "\nSudo account: {}", + sudo_keypair.public_key().to_account_id() + ); + + // Build sudo(system.setStorage(items)) + let set_storage_tx = pezkuwi_subxt::dynamic::tx( + "System", + "set_storage", + vec![Value::unnamed_composite(vec![Value::unnamed_composite(vec![ + Value::from_bytes(&validators_to_retire_key), + Value::from_bytes(&encoded_retire), + ])])], + ); + + let sudo_call = pezkuwi_subxt::dynamic::tx("Sudo", "sudo", vec![set_storage_tx.into_value()]); + + println!("\nSubmitting sudo(system.setStorage) to write ValidatorsToRetire...\n"); + + use pezkuwi_subxt::tx::TxStatus; + + let tx_progress = api + .tx() + .sign_and_submit_then_watch_default(&sudo_call, &sudo_keypair) + .await?; + + println!( + " TX: 0x{}", + hex::encode(tx_progress.extrinsic_hash().as_ref()) + ); + + let mut progress = tx_progress; + let mut success = false; + loop { + let status = progress.next().await; + match status { + Some(Ok(TxStatus::InBestBlock(details))) => { + match details.wait_for_success().await { + Ok(events) => { + println!(" In best block! Events:"); + for event in events.iter() { + if let Ok(ev) = event { + println!(" {}::{}", ev.pallet_name(), ev.variant_name()); + if ev.pallet_name() == "Sudo" && ev.variant_name() == "Sudid" { + success = true; + } + } + } + }, + Err(e) => println!(" DISPATCH ERROR: {}", e), + } + break; + }, + Some(Ok(TxStatus::Error { message })) => { + println!(" TX ERROR: {}", message); + break; + }, + Some(Ok(TxStatus::Invalid { message })) => { + println!(" TX INVALID: {}", message); + break; + }, + Some(Ok(TxStatus::Dropped { message })) => { + println!(" TX DROPPED: {}", message); + break; + }, + Some(Err(e)) => { + println!(" STREAM ERROR: {}", e); + break; + }, + None => { + println!(" STREAM ENDED"); + break; + }, + _ => {}, + } + } + + if !success { + println!("\nFAILED!"); + return Ok(()); + } + + // Verify by reading back the storage + println!("\n=== VERIFICATION ==="); + tokio::time::sleep(std::time::Duration::from_secs(3)).await; + + let api2 = OnlineClient::::from_insecure_url(&url).await?; + match api2 + .storage() + .at_latest() + .await? + .fetch_raw( + hex::decode("084e7f70a295a190e2e33fd3f8cdfcc2b664fa73499821e43a617aa0e82b17b1") + .unwrap(), + ) + .await + { + Ok(data) => { + let stored_count = (data[0] >> 2) as usize; + println!( + "ValidatorsToRetire: {} entries ({} bytes)", + stored_count, + data.len() + ); + if stored_count == to_remove.len() { + println!("COUNT MATCHES! Storage write successful."); + } else { + println!( + "COUNT MISMATCH! Expected {}, got {}", + to_remove.len(), + stored_count + ); + } + // Show first few + let mut off = 1; + for i in 0..std::cmp::min(stored_count, 3) { + if off + 32 <= data.len() { + println!(" [{}] 0x{}", i + 1, hex::encode(&data[off..off + 32])); + off += 32; + } + } + if stored_count > 3 { + println!(" ... ({} more)", stored_count - 3); + } + }, + Err(e) => { + println!("ValidatorsToRetire: ERROR reading back: {}", e); + println!("Storage might not have been written!"); + }, + } + + println!("\n=== DONE ==="); + println!("ValidatorsToRetire is now set with {} validators to remove.", to_remove.len()); + println!("At next session change, new_session() will take() these and remove them."); + println!("Then at session+1 after that, GRANDPA authorities should change."); + + // Show timing info + println!("\nSession = 600 slots × 6 sec = 60 min"); + println!("Expected GRANDPA change: ~60-120 minutes from now."); + + Ok(()) +} diff --git a/vendor/pezkuwi-subxt/subxt/examples/zagros_sudo.rs b/vendor/pezkuwi-subxt/subxt/examples/zagros_sudo.rs new file mode 100644 index 00000000..55730d11 --- /dev/null +++ b/vendor/pezkuwi-subxt/subxt/examples/zagros_sudo.rs @@ -0,0 +1,184 @@ +//! Zagros Testnet: Generic sudo call sender +//! +//! Run with: +//! SUDO_MNEMONIC="..." RPC_URL="ws://..." CALL=setValidatorCount|forceNewEra \ +//! cargo run --release --example zagros_sudo + +#![allow(missing_docs)] +use pezkuwi_subxt::dynamic::Value; +use pezkuwi_subxt::{OnlineClient, PezkuwiConfig}; +use pezkuwi_subxt_signer::bip39::Mnemonic; +use pezkuwi_subxt_signer::sr25519::Keypair; +use std::str::FromStr; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let url = std::env::var("RPC_URL").unwrap_or_else(|_| "ws://217.77.6.126:9948".to_string()); + let call_name = std::env::var("CALL").unwrap_or_else(|_| "setValidatorCount".to_string()); + + println!("RPC: {}", url); + println!("Call: {}", call_name); + + let api = OnlineClient::::from_insecure_url(&url).await?; + println!("Connected!"); + + let mnemonic_str = + std::env::var("SUDO_MNEMONIC").expect("SUDO_MNEMONIC environment variable required"); + let mnemonic = Mnemonic::from_str(&mnemonic_str)?; + let sudo_keypair = Keypair::from_phrase(&mnemonic, None)?; + println!("Sudo: {}", sudo_keypair.public_key().to_account_id()); + + let inner_call = match call_name.as_str() { + "setValidatorCount" => { + let count: u32 = std::env::var("COUNT") + .unwrap_or_else(|_| "4".to_string()) + .parse()?; + println!("Setting validator count to {}", count); + pezkuwi_subxt::dynamic::tx("Staking", "set_validator_count", vec![Value::u128( + count as u128, + )]) + }, + "forceNewEra" => { + println!("Forcing new era"); + pezkuwi_subxt::dynamic::tx("Staking", "force_new_era", Vec::::new()) + }, + "forceNewEraAlways" => { + println!("Forcing new era always"); + pezkuwi_subxt::dynamic::tx("Staking", "force_new_era_always", Vec::::new()) + }, + "setStakingConfigs" => { + // Set min_validator_count to 1 via set_staking_configs + let min_count: u32 = std::env::var("MIN_COUNT") + .unwrap_or_else(|_| "1".to_string()) + .parse().unwrap(); + println!("Setting staking configs: min_nominator_bond=Noop, min_validator_bond=Noop, max_nominator_count=Noop, max_validator_count=Noop, chill_threshold=Noop, min_commission=Noop"); + // Actually we need to set min_validator_count directly + // Let's use a different approach - call set_staking_configs with all Noop except what we need + // ConfigOp enum: 0=Noop, 1=Set(value), 2=Remove + println!("Using setMinValidatorCount instead..."); + // Fallthrough to unknown + eprintln!("Use setMinValidatorCount instead"); + std::process::exit(1); + }, + "setMinValidatorCount" => { + let min_count: u32 = std::env::var("MIN_COUNT") + .unwrap_or_else(|_| "1".to_string()) + .parse().unwrap(); + println!("Setting minimum validator count to {}", min_count); + // Staking::set_staking_configs sets all params at once + // Instead we should check if there's a direct setter + // In substrate, there's no direct set_minimum_validator_count + // We need to use set_staking_configs with ConfigOp + // ConfigOp: Noop=unnamed_variant("Noop",[]), Set=unnamed_variant("Set",[Value::u128(x)]) + let noop = Value::unnamed_variant("Noop", Vec::::new()); + let set_val = Value::unnamed_variant("Set", vec![Value::u128(min_count as u128)]); + pezkuwi_subxt::dynamic::tx("Staking", "set_staking_configs", vec![ + noop.clone(), // min_nominator_bond + noop.clone(), // min_validator_bond + noop.clone(), // max_nominator_count + noop.clone(), // max_validator_count + noop.clone(), // chill_threshold + noop.clone(), // min_commission + noop.clone(), // max_staked_rewards (if exists) + ]) + }, + "setStorage" => { + // Set arbitrary storage via sudo(system.setStorage) + let key_hex = + std::env::var("STORAGE_KEY").expect("STORAGE_KEY env var required"); + let value_hex = + std::env::var("STORAGE_VALUE").expect("STORAGE_VALUE env var required"); + println!("Setting storage key={} value={}", key_hex, value_hex); + + let key_bytes = hex::decode(key_hex.trim_start_matches("0x")).unwrap(); + let value_bytes = hex::decode(value_hex.trim_start_matches("0x")).unwrap(); + + // system.setStorage takes Vec<(Key, Value)> + let item = Value::unnamed_composite([ + Value::from_bytes(&key_bytes), + Value::from_bytes(&value_bytes), + ]); + pezkuwi_subxt::dynamic::tx("System", "set_storage", vec![ + Value::unnamed_composite([item]), + ]) + }, + _ => { + eprintln!("Unknown call: {}", call_name); + std::process::exit(1); + }, + }; + + let sudo_tx = pezkuwi_subxt::dynamic::tx("Sudo", "sudo", vec![inner_call.into_value()]); + + println!("\nSubmitting..."); + + // Use sign_and_submit_then_watch to see TX lifecycle + let tx_progress = api + .tx() + .sign_and_submit_then_watch_default(&sudo_tx, &sudo_keypair) + .await?; + + println!("TX hash: 0x{}", hex::encode(tx_progress.extrinsic_hash().as_ref())); + println!("Watching TX status (Ctrl+C to abort)..."); + + // Don't wait for finalization - just wait for in_block + use pezkuwi_subxt::tx::TxStatus; + + let mut progress = tx_progress; + loop { + let status = progress.next().await; + match status { + Some(Ok(TxStatus::Validated)) => println!(" Status: Validated (in tx pool)"), + Some(Ok(TxStatus::Broadcasted)) => println!(" Status: Broadcasted"), + Some(Ok(TxStatus::InBestBlock(details))) => { + println!(" Status: InBestBlock {:?}", details.block_hash()); + match details.wait_for_success().await { + Ok(events) => { + println!(" TX SUCCESS!"); + for event in events.iter() { + if let Ok(ev) = event { + println!( + " Event: {}::{}", + ev.pallet_name(), + ev.variant_name() + ); + } + } + }, + Err(e) => println!(" TX dispatch error: {}", e), + } + break; + }, + Some(Ok(TxStatus::InFinalizedBlock(details))) => { + println!(" Status: Finalized {:?}", details.block_hash()); + break; + }, + Some(Ok(TxStatus::Error { message })) => { + println!(" Status: ERROR - {}", message); + break; + }, + Some(Ok(TxStatus::Invalid { message })) => { + println!(" Status: INVALID - {}", message); + break; + }, + Some(Ok(TxStatus::Dropped { message })) => { + println!(" Status: DROPPED - {}", message); + break; + }, + Some(Ok(TxStatus::NoLongerInBestBlock)) => { + println!(" Status: No longer in best block"); + }, + Some(Err(e)) => { + println!(" Stream error: {}", e); + break; + }, + None => { + println!(" Stream ended"); + break; + }, + } + } + + println!("\nDone."); + Ok(()) +} diff --git a/vendor/pezkuwi-subxt/subxt/examples/zagros_upgrade.rs b/vendor/pezkuwi-subxt/subxt/examples/zagros_upgrade.rs new file mode 100644 index 00000000..077acfa2 --- /dev/null +++ b/vendor/pezkuwi-subxt/subxt/examples/zagros_upgrade.rs @@ -0,0 +1,293 @@ +//! Zagros Testnet: Runtime upgrade + ValidatorCount fix +//! +//! Step 1: Deploy new WASM via sudo(sudoUncheckedWeight(system.setCodeWithoutChecks)) +//! Step 2: Set ValidatorCount=2 and ForceEra=ForceNew via sudo(system.setStorage) +//! +//! Run with: +//! SUDO_MNEMONIC="******" \ +//! WASM_FILE="/home/mamostehp/pezkuwi-sdk/target/release/wbuild/pezkuwichain-runtime/pezkuwichain_runtime.compact.compressed.wasm" \ +//! RPC_URL="ws://217.77.6.126:9948" \ +//! cargo run --release --example zagros_upgrade -p pezkuwi-subxt + +#![allow(missing_docs)] +use pezkuwi_subxt::dynamic::Value; +use pezkuwi_subxt::{OnlineClient, PezkuwiConfig}; +use pezkuwi_subxt_signer::bip39::Mnemonic; +use pezkuwi_subxt_signer::sr25519::Keypair; +use std::str::FromStr; + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("=== ZAGROS RUNTIME UPGRADE + VALIDATOR FIX ===\n"); + + let url = std::env::var("RPC_URL").unwrap_or_else(|_| "ws://217.77.6.126:9948".to_string()); + let wasm_path = std::env::var("WASM_FILE").expect("WASM_FILE environment variable required"); + let new_validator_count: u32 = std::env::var("VALIDATOR_COUNT") + .unwrap_or_else(|_| "2".to_string()) + .parse()?; + + println!("RPC: {}", url); + println!("WASM: {}", wasm_path); + println!("Target validator count: {}", new_validator_count); + + // Load WASM + let wasm_data = std::fs::read(&wasm_path)?; + println!("WASM size: {} bytes ({:.2} MB)", wasm_data.len(), wasm_data.len() as f64 / 1_048_576.0); + + // Connect + let api = OnlineClient::::from_insecure_url(&url).await?; + let rv = api.runtime_version(); + println!("Current on-chain specVersion: {}", rv.spec_version); + + // Load sudo key + let mnemonic_str = + std::env::var("SUDO_MNEMONIC").expect("SUDO_MNEMONIC environment variable required"); + let mnemonic = Mnemonic::from_str(&mnemonic_str)?; + let sudo_keypair = Keypair::from_phrase(&mnemonic, None)?; + println!("Sudo account: {}\n", sudo_keypair.public_key().to_account_id()); + + // ========================================== + // STEP 1: Runtime upgrade (deploy WASM) + // ========================================== + println!("=== STEP 1: RUNTIME UPGRADE ==="); + println!("Deploying WASM via sudo(sudoUncheckedWeight(system.setCodeWithoutChecks))..."); + + let set_code = pezkuwi_subxt::dynamic::tx( + "System", + "set_code_without_checks", + vec![Value::from_bytes(&wasm_data)], + ); + + let sudo_upgrade = pezkuwi_subxt::dynamic::tx("Sudo", "sudo_unchecked_weight", vec![ + set_code.into_value(), + Value::named_composite([ + ("ref_time", Value::u128(1u128)), + ("proof_size", Value::u128(1u128)), + ]), + ]); + + use pezkuwi_subxt::tx::TxStatus; + + let tx_progress = api + .tx() + .sign_and_submit_then_watch_default(&sudo_upgrade, &sudo_keypair) + .await?; + + println!( + " TX submitted: 0x{}", + hex::encode(tx_progress.extrinsic_hash().as_ref()) + ); + + let mut progress = tx_progress; + let mut upgrade_ok = false; + loop { + let status = progress.next().await; + match status { + Some(Ok(TxStatus::InBestBlock(details))) => { + match details.wait_for_success().await { + Ok(events) => { + println!(" In best block! Events:"); + for event in events.iter() { + if let Ok(ev) = event { + println!(" {}::{}", ev.pallet_name(), ev.variant_name()); + if ev.pallet_name() == "System" + && ev.variant_name() == "CodeUpdated" + { + upgrade_ok = true; + } + } + } + }, + Err(e) => println!(" DISPATCH ERROR: {}", e), + } + break; + }, + Some(Ok(TxStatus::Error { message })) => { + println!(" TX ERROR: {}", message); + break; + }, + Some(Ok(TxStatus::Invalid { message })) => { + println!(" TX INVALID: {}", message); + break; + }, + Some(Ok(TxStatus::Dropped { message })) => { + println!(" TX DROPPED: {}", message); + break; + }, + Some(Err(e)) => { + println!(" STREAM ERROR: {}", e); + break; + }, + None => { + println!(" STREAM ENDED"); + break; + }, + _ => {}, + } + } + + if !upgrade_ok { + println!("\n UPGRADE FAILED! Aborting."); + return Ok(()); + } + + println!(" UPGRADE SUCCESS!\n"); + + // Wait for next block to ensure new runtime is active + println!("Waiting 12 seconds for new runtime to activate..."); + tokio::time::sleep(std::time::Duration::from_secs(12)).await; + + // Reconnect with new runtime + let api2 = OnlineClient::::from_insecure_url(&url).await?; + let rv2 = api2.runtime_version(); + println!("New on-chain specVersion: {}\n", rv2.spec_version); + + // ========================================== + // STEP 2: Set ValidatorCount + ForceEra + // ========================================== + println!("=== STEP 2: SET VALIDATOR COUNT + FORCE ERA ==="); + + // Storage keys (verified): + // Staking::ValidatorCount: 0x5f3e4907f716ac89b6347d15ececedca138e71612491192d68deab7e6f563fe1 + // Staking::ForceEra: 0x5f3e4907f716ac89b6347d15ececedcaf7dad0317324aecae8744b87fc95f2f3 + + let validator_count_key = + hex::decode("5f3e4907f716ac89b6347d15ececedca138e71612491192d68deab7e6f563fe1") + .unwrap(); + let force_era_key = + hex::decode("5f3e4907f716ac89b6347d15ececedcaf7dad0317324aecae8744b87fc95f2f3") + .unwrap(); + + // ValidatorCount is u32 LE + let validator_count_value = new_validator_count.to_le_bytes().to_vec(); + // ForceEra::ForceNew = 0x01 + let force_era_value = vec![0x01u8]; + + println!("Setting ValidatorCount = {}", new_validator_count); + println!("Setting ForceEra = ForceNew (0x01)"); + + let set_storage_tx = pezkuwi_subxt::dynamic::tx("System", "set_storage", vec![ + Value::unnamed_composite(vec![ + Value::unnamed_composite(vec![ + Value::from_bytes(&validator_count_key), + Value::from_bytes(&validator_count_value), + ]), + Value::unnamed_composite(vec![ + Value::from_bytes(&force_era_key), + Value::from_bytes(&force_era_value), + ]), + ]), + ]); + + let sudo_storage = pezkuwi_subxt::dynamic::tx("Sudo", "sudo", vec![ + set_storage_tx.into_value(), + ]); + + let tx_progress2 = api2 + .tx() + .sign_and_submit_then_watch_default(&sudo_storage, &sudo_keypair) + .await?; + + println!( + " TX submitted: 0x{}", + hex::encode(tx_progress2.extrinsic_hash().as_ref()) + ); + + let mut progress2 = tx_progress2; + let mut storage_ok = false; + loop { + let status = progress2.next().await; + match status { + Some(Ok(TxStatus::InBestBlock(details))) => { + match details.wait_for_success().await { + Ok(events) => { + println!(" In best block! Events:"); + for event in events.iter() { + if let Ok(ev) = event { + println!(" {}::{}", ev.pallet_name(), ev.variant_name()); + if ev.pallet_name() == "Sudo" && ev.variant_name() == "Sudid" { + storage_ok = true; + } + } + } + }, + Err(e) => println!(" DISPATCH ERROR: {}", e), + } + break; + }, + Some(Ok(TxStatus::Error { message })) => { + println!(" TX ERROR: {}", message); + break; + }, + Some(Ok(TxStatus::Invalid { message })) => { + println!(" TX INVALID: {}", message); + break; + }, + Some(Ok(TxStatus::Dropped { message })) => { + println!(" TX DROPPED: {}", message); + break; + }, + Some(Err(e)) => { + println!(" STREAM ERROR: {}", e); + break; + }, + None => { + println!(" STREAM ENDED"); + break; + }, + _ => {}, + } + } + + if !storage_ok { + println!("\n STORAGE FIX FAILED!"); + } else { + println!(" STORAGE FIX SUCCESS!"); + } + + // ========================================== + // STEP 3: Verify + // ========================================== + println!("\n=== VERIFICATION ==="); + tokio::time::sleep(std::time::Duration::from_secs(6)).await; + + let api3 = OnlineClient::::from_insecure_url(&url).await?; + let rv3 = api3.runtime_version(); + println!("specVersion: {}", rv3.spec_version); + + // Read back storage to verify + let vc_bytes = api3 + .storage() + .at_latest() + .await? + .fetch_raw(validator_count_key) + .await?; + if vc_bytes.len() >= 4 { + let vc = u32::from_le_bytes([vc_bytes[0], vc_bytes[1], vc_bytes[2], vc_bytes[3]]); + println!("ValidatorCount: {}", vc); + } + + let fe_bytes = api3 + .storage() + .at_latest() + .await? + .fetch_raw(force_era_key) + .await?; + if !fe_bytes.is_empty() { + let fe_name = match fe_bytes[0] { + 0x00 => "NotForcing", + 0x01 => "ForceNew", + 0x02 => "ForceNone", + 0x03 => "ForceAlways", + _ => "Unknown", + }; + println!("ForceEra: {} (0x{:02x})", fe_name, fe_bytes[0]); + } + + println!("\n=== DONE ==="); + println!("Runtime upgraded and validator count set."); + println!("Next era should elect {} validators.", new_validator_count); + println!("Monitor: GRANDPA authorities should change within 1-2 sessions."); + + Ok(()) +}