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:
@@ -143,7 +143,7 @@ pub mod pezkuwichain {
|
|||||||
Extensions::new("pezkuwichain-mainnet".to_string(), 1004),
|
Extensions::new("pezkuwichain-mainnet".to_string(), 1004),
|
||||||
)
|
)
|
||||||
.with_name("Pezkuwichain People")
|
.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_chain_type(ChainType::Live)
|
||||||
.with_genesis_config_preset_name("genesis")
|
.with_genesis_config_preset_name("genesis")
|
||||||
.with_properties(properties)
|
.with_properties(properties)
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ echo -e "${GREEN} -> relay-plain.json created${NC}"
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
echo -e "${YELLOW}[2/6] Generating Asset Hub chain spec...${NC}"
|
echo -e "${YELLOW}[2/6] Generating Asset Hub chain spec...${NC}"
|
||||||
$TEYRCHAIN_BIN build-spec \
|
$TEYRCHAIN_BIN build-spec \
|
||||||
--chain asset-hub-pezkuwichain \
|
--chain asset-hub-pezkuwichain-genesis \
|
||||||
--disable-default-bootnode \
|
--disable-default-bootnode \
|
||||||
2>/dev/null > "$OUTPUT_DIR/asset-hub-plain.json"
|
2>/dev/null > "$OUTPUT_DIR/asset-hub-plain.json"
|
||||||
echo -e "${GREEN} -> asset-hub-plain.json created${NC}"
|
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}"
|
echo -e "${YELLOW}[3/6] Generating People Chain chain spec...${NC}"
|
||||||
$TEYRCHAIN_BIN build-spec \
|
$TEYRCHAIN_BIN build-spec \
|
||||||
--chain people-pezkuwichain \
|
--chain people-pezkuwichain-genesis \
|
||||||
--disable-default-bootnode \
|
--disable-default-bootnode \
|
||||||
2>/dev/null > "$OUTPUT_DIR/people-plain.json"
|
2>/dev/null > "$OUTPUT_DIR/people-plain.json"
|
||||||
echo -e "${GREEN} -> people-plain.json created${NC}"
|
echo -e "${GREEN} -> people-plain.json created${NC}"
|
||||||
@@ -221,7 +221,7 @@ def verify_spec(path, name, expected_id):
|
|||||||
ok = True
|
ok = True
|
||||||
ok = verify_spec("$OUTPUT_DIR/relay-raw.json", "Relay Chain", "pezkuwichain_mainnet") and ok
|
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/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:
|
if not ok:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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(())
|
||||||
|
}
|
||||||
@@ -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(¬_forcing_value)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Build: system.setStorage(items: Vec<(Key, Value)>)
|
||||||
|
let set_storage_call = pezkuwi_subxt::dynamic::tx(
|
||||||
|
"System",
|
||||||
|
"set_storage",
|
||||||
|
vec![Value::unnamed_composite(vec![Value::unnamed_composite(vec![
|
||||||
|
Value::from_bytes(&force_era_key),
|
||||||
|
Value::from_bytes(¬_forcing_value),
|
||||||
|
])])],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wrap in sudo
|
||||||
|
let sudo_call = pezkuwi_subxt::dynamic::tx(
|
||||||
|
"Sudo",
|
||||||
|
"sudo",
|
||||||
|
vec![set_storage_call.into_value()],
|
||||||
|
);
|
||||||
|
|
||||||
|
println!("\nSubmitting sudo(system.setStorage)...");
|
||||||
|
|
||||||
|
use pezkuwi_subxt::tx::TxStatus;
|
||||||
|
let tx_progress = api
|
||||||
|
.tx()
|
||||||
|
.sign_and_submit_then_watch_default(&sudo_call, &keypair)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"TX: 0x{}",
|
||||||
|
hex::encode(tx_progress.extrinsic_hash().as_ref())
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut progress = tx_progress;
|
||||||
|
loop {
|
||||||
|
let status = progress.next().await;
|
||||||
|
match status {
|
||||||
|
Some(Ok(TxStatus::InBestBlock(details))) => {
|
||||||
|
match details.wait_for_success().await {
|
||||||
|
Ok(events) => {
|
||||||
|
let mut sudid = false;
|
||||||
|
for event in events.iter() {
|
||||||
|
if let Ok(ev) = event {
|
||||||
|
println!(
|
||||||
|
" Event: {}::{}",
|
||||||
|
ev.pallet_name(),
|
||||||
|
ev.variant_name()
|
||||||
|
);
|
||||||
|
if ev.pallet_name() == "Sudo" && ev.variant_name() == "Sudid" {
|
||||||
|
sudid = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sudid {
|
||||||
|
println!("\nSUCCESS: ForceEra set to NotForcing");
|
||||||
|
} else {
|
||||||
|
println!("\nWARNING: Sudo::Sudid event not found");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => println!("DISPATCH ERROR: {}", e),
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
Some(Ok(TxStatus::Error { message })) => {
|
||||||
|
println!("TX ERROR: {}", message);
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
Some(Ok(TxStatus::Invalid { message })) => {
|
||||||
|
println!("TX INVALID: {}", message);
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
Some(Ok(TxStatus::Dropped { message })) => {
|
||||||
|
println!("TX DROPPED: {}", message);
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
Some(Err(e)) => {
|
||||||
|
println!("STREAM ERROR: {}", e);
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
println!("STREAM ENDED");
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -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(())
|
||||||
|
}
|
||||||
@@ -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(())
|
||||||
|
}
|
||||||
@@ -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(())
|
||||||
|
}
|
||||||
@@ -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(())
|
||||||
|
}
|
||||||
@@ -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(())
|
||||||
|
}
|
||||||
@@ -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
@@ -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(())
|
||||||
|
}
|
||||||
@@ -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(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user