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
This commit is contained in:
2026-02-16 08:18:26 +03:00
parent d6444076c3
commit 0e809c3a74
22 changed files with 5451 additions and 4 deletions
@@ -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)
+3 -3
View File
@@ -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)
File diff suppressed because it is too large Load Diff
+159
View File
@@ -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);
});
@@ -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<u8> {
vec![0x00]
}
/// SCALE encode ConfigOp::Set(value) for u128 (Balance)
fn encode_set_u128(value: u128) -> Vec<u8> {
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<u8> {
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<Balance> = Set(min_join_bond)
encoded.extend_from_slice(&encode_set_u128(min_join_bond));
// min_create_bond: ConfigOp<Balance> = Set(min_create_bond)
encoded.extend_from_slice(&encode_set_u128(min_create_bond));
// max_pools: ConfigOp<u32> = Noop
encoded.extend_from_slice(&encode_noop());
// max_members: ConfigOp<u32> = Noop
encoded.extend_from_slice(&encode_noop());
// max_members_per_pool: ConfigOp<u32> = Noop
encoded.extend_from_slice(&encode_noop());
// global_max_commission: ConfigOp<Perbill> = Noop
encoded.extend_from_slice(&encode_noop());
encoded
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
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::<PezkuwiConfig>::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(())
}
@@ -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<dyn std::error::Error>> {
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::<PezkuwiConfig>::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(())
}
@@ -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<WalletInfo> {
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<WalletInfo>) -> Vec<PoolConfig> {
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<PezkuwiConfig, OnlineClient<PezkuwiConfig>>,
label: &str,
) -> Result<bool, Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
let wallets = load_wallets();
let pool_configs = build_pool_configs(wallets);
// Parse CLI args
let args: Vec<String> = 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::<PezkuwiConfig>::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(())
}
+131
View File
@@ -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<dyn std::error::Error>> {
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::<PezkuwiConfig>::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(&not_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(&not_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(())
}
+321
View File
@@ -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<u8> {
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<dyn std::error::Error>> {
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::<PezkuwiConfig>::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(())
}
+71
View File
@@ -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<dyn std::error::Error>> {
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::<PezkuwiConfig>::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(())
}
@@ -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<ValidatorInfo> {
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<u8> {
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<dyn std::error::Error>> {
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::<PezkuwiConfig>::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(())
}
+191
View File
@@ -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<WalletInfo> {
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<dyn std::error::Error>> {
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::<PezkuwiConfig>::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(())
}
@@ -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<dyn std::error::Error>> {
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::<PezkuwiConfig>::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(())
}
@@ -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<ValidatorInfo> {
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<u8> {
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<u8> {
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<u8>, Vec<u8>)>)
fn encode_set_storage(items: &[(Vec<u8>, Vec<u8>)]) -> Vec<u8> {
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<u8> {
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<RuntimeCall>)
fn encode_batch_all(calls: Vec<Vec<u8>>) -> Vec<u8> {
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<u8> {
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<u8> {
// 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<dyn std::error::Error>> {
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::<PezkuwiConfig>::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(())
}
@@ -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<WalletInfo> {
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<PezkuwiConfig>,
tx: pezkuwi_subxt::tx::DynamicPayload,
signer: &Keypair,
label: &str,
) -> Result<bool, Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
let ws = load_wallets();
let args: Vec<String> = 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::<PezkuwiConfig>::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::<Value>::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(())
}
+287
View File
@@ -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<dyn std::error::Error>> {
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::<PezkuwiConfig>::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<PezkuwiConfig>,
) -> Result<(), Box<dyn std::error::Error>> {
let mut all_validators: Vec<Vec<u8>> = 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<Value> = 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(())
}
+253
View File
@@ -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<dyn std::error::Error>> {
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::<PezkuwiConfig>::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(())
}
@@ -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<dyn std::error::Error>> {
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::<PezkuwiConfig>::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::<Value>::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(())
}
@@ -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<dyn std::error::Error>> {
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::<PezkuwiConfig>::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::<Value>::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(())
}
+275
View File
@@ -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<u8> {
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<dyn std::error::Error>> {
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::<PezkuwiConfig>::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<u8>> = 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<AccountId32> 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::<PezkuwiConfig>::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(())
}
+184
View File
@@ -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<dyn std::error::Error>> {
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::<PezkuwiConfig>::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::<Value>::new())
},
"forceNewEraAlways" => {
println!("Forcing new era always");
pezkuwi_subxt::dynamic::tx("Staking", "force_new_era_always", Vec::<Value>::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::<Value>::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(())
}
+293
View File
@@ -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<dyn std::error::Error>> {
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::<PezkuwiConfig>::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::<PezkuwiConfig>::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::<PezkuwiConfig>::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(())
}