Files
pezkuwi-sdk/vendor/pezkuwi-subxt/subxt/examples/create_nomination_pools.rs
T
pezkuwichain 0e809c3a74 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
2026-02-16 08:18:26 +03:00

433 lines
12 KiB
Rust

//! 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(())
}