From c0220688eb2054645ee9460848eb4db8f78dd4dc Mon Sep 17 00:00:00 2001 From: Cezary Olborski Date: Tue, 3 Mar 2026 09:56:26 +0800 Subject: [PATCH] feat: Referenda commands restored --- src/cli/mod.rs | 22 +- src/cli/referenda.rs | 855 ++++++++++++++++++++++++++++++++++++ src/cli/referenda_decode.rs | 243 ++++++++++ src/cli/tech_referenda.rs | 771 ++++++++++++++++++++++++++++++++ src/config/mod.rs | 2 +- src/quantus_metadata.scale | Bin 163412 -> 163412 bytes 6 files changed, 1891 insertions(+), 2 deletions(-) create mode 100644 src/cli/referenda.rs create mode 100644 src/cli/referenda_decode.rs create mode 100644 src/cli/tech_referenda.rs diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 2d3e436..68bbdbb 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -14,6 +14,8 @@ pub mod multisend; pub mod multisig; pub mod preimage; pub mod recovery; +pub mod referenda; +pub mod referenda_decode; pub mod reversible; pub mod runtime; pub mod scheduler; @@ -21,6 +23,7 @@ pub mod send; pub mod storage; pub mod system; pub mod tech_collective; +pub mod tech_referenda; pub mod transfers; pub mod treasury; pub mod wallet; @@ -96,10 +99,18 @@ pub enum Commands { #[command(subcommand)] TechCollective(tech_collective::TechCollectiveCommands), - /// Tech Referenda management commands (for runtime upgrade proposals) + /// Preimage management commands #[command(subcommand)] Preimage(preimage::PreimageCommands), + /// Tech Referenda management commands (for runtime upgrade proposals) + #[command(subcommand)] + TechReferenda(tech_referenda::TechReferendaCommands), + + /// Standard Referenda management commands (public governance) + #[command(subcommand)] + Referenda(referenda::ReferendaCommands), + /// Treasury account info #[command(subcommand)] Treasury(treasury::TreasuryCommands), @@ -353,6 +364,15 @@ pub async fn execute_command( .await, Commands::Preimage(preimage_cmd) => preimage::handle_preimage_command(preimage_cmd, node_url, execution_mode).await, + Commands::TechReferenda(tech_referenda_cmd) => + tech_referenda::handle_tech_referenda_command( + tech_referenda_cmd, + node_url, + execution_mode, + ) + .await, + Commands::Referenda(referenda_cmd) => + referenda::handle_referenda_command(referenda_cmd, node_url, execution_mode).await, Commands::Treasury(treasury_cmd) => treasury::handle_treasury_command(treasury_cmd, node_url, execution_mode).await, Commands::Transfers(transfers_cmd) => diff --git a/src/cli/referenda.rs b/src/cli/referenda.rs new file mode 100644 index 0000000..04f4416 --- /dev/null +++ b/src/cli/referenda.rs @@ -0,0 +1,855 @@ +//! `quantus referenda` subcommand - manage standard Referenda proposals +use crate::{ + chain::quantus_subxt, cli::common::submit_transaction, error::QuantusError, log_error, + log_print, log_success, log_verbose, +}; +use clap::Subcommand; +use colored::Colorize; +use std::str::FromStr; + +/// Standard Referenda management commands +#[derive(Subcommand, Debug)] +pub enum ReferendaCommands { + /// Submit a simple proposal (System::remark) to test Referenda + SubmitRemark { + /// Message to include in the remark + #[arg(long)] + message: String, + + /// Wallet name to sign with + #[arg(short, long)] + from: String, + + /// Password for the wallet + #[arg(short, long)] + password: Option, + + /// Read password from file + #[arg(long)] + password_file: Option, + + /// Origin type: signed (default), none (for signaling track), root + #[arg(long, default_value = "signed")] + origin: String, + }, + + /// Submit a proposal using existing preimage hash + Submit { + /// Preimage hash (must already exist on chain) + #[arg(long)] + preimage_hash: String, + + /// Wallet name to sign with + #[arg(short, long)] + from: String, + + /// Password for the wallet + #[arg(short, long)] + password: Option, + + /// Read password from file + #[arg(long)] + password_file: Option, + + /// Origin type: signed (default), none (for signaling track), root + #[arg(long, default_value = "signed")] + origin: String, + }, + + /// List all active Referenda proposals + List, + + /// Get details of a specific Referendum + Get { + /// Referendum index + #[arg(short, long)] + index: u32, + + /// Decode and display the proposal call in human-readable format + #[arg(long)] + decode: bool, + }, + + /// Check the status of a Referendum + Status { + /// Referendum index + #[arg(short, long)] + index: u32, + }, + + /// Place a decision deposit for a Referendum + PlaceDecisionDeposit { + /// Referendum index + #[arg(short, long)] + index: u32, + + /// Wallet name to sign with + #[arg(short, long)] + from: String, + + /// Password for the wallet + #[arg(short, long)] + password: Option, + + /// Read password from file + #[arg(long)] + password_file: Option, + }, + + /// Vote on a Referendum (uses conviction voting) + Vote { + /// Referendum index + #[arg(short, long)] + index: u32, + + /// Vote aye (true) or nay (false) + #[arg(long)] + aye: bool, + + /// Conviction (0=None, 1=Locked1x, 2=Locked2x, up to 6=Locked6x) + #[arg(long, default_value = "0")] + conviction: u8, + + /// Amount to vote with + #[arg(long)] + amount: String, + + /// Wallet name to sign with + #[arg(short, long)] + from: String, + + /// Password for the wallet + #[arg(short, long)] + password: Option, + + /// Read password from file + #[arg(long)] + password_file: Option, + }, + + /// Refund submission deposit for a completed Referendum + RefundSubmissionDeposit { + /// Referendum index + #[arg(short, long)] + index: u32, + + /// Wallet name that submitted the referendum + #[arg(short, long)] + from: String, + + /// Password for the wallet + #[arg(short, long)] + password: Option, + + /// Read password from file + #[arg(long)] + password_file: Option, + }, + + /// Refund decision deposit for a completed Referendum + RefundDecisionDeposit { + /// Referendum index + #[arg(short, long)] + index: u32, + + /// Wallet name that placed the decision deposit + #[arg(short, long)] + from: String, + + /// Password for the wallet + #[arg(short, long)] + password: Option, + + /// Read password from file + #[arg(long)] + password_file: Option, + }, + + /// Get Referenda configuration + Config, +} + +/// Handle referenda commands +pub async fn handle_referenda_command( + command: ReferendaCommands, + node_url: &str, + execution_mode: crate::cli::common::ExecutionMode, +) -> crate::error::Result<()> { + let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?; + + match command { + ReferendaCommands::SubmitRemark { message, from, password, password_file, origin } => + submit_remark_proposal( + &quantus_client, + &message, + &from, + password, + password_file, + &origin, + execution_mode, + ) + .await, + ReferendaCommands::Submit { preimage_hash, from, password, password_file, origin } => + submit_proposal( + &quantus_client, + &preimage_hash, + &from, + password, + password_file, + &origin, + execution_mode, + ) + .await, + ReferendaCommands::List => list_proposals(&quantus_client).await, + ReferendaCommands::Get { index, decode } => + get_proposal_details(&quantus_client, index, decode).await, + ReferendaCommands::Status { index } => get_proposal_status(&quantus_client, index).await, + ReferendaCommands::PlaceDecisionDeposit { index, from, password, password_file } => + place_decision_deposit( + &quantus_client, + index, + &from, + password, + password_file, + execution_mode, + ) + .await, + ReferendaCommands::Vote { + index, + aye, + conviction, + amount, + from, + password, + password_file, + } => + vote_on_referendum( + &quantus_client, + index, + aye, + conviction, + &amount, + &from, + password, + password_file, + execution_mode, + ) + .await, + ReferendaCommands::RefundSubmissionDeposit { index, from, password, password_file } => + refund_submission_deposit( + &quantus_client, + index, + &from, + password, + password_file, + execution_mode, + ) + .await, + ReferendaCommands::RefundDecisionDeposit { index, from, password, password_file } => + refund_decision_deposit( + &quantus_client, + index, + &from, + password, + password_file, + execution_mode, + ) + .await, + ReferendaCommands::Config => get_config(&quantus_client).await, + } +} + +/// Submit a simple System::remark proposal +async fn submit_remark_proposal( + quantus_client: &crate::chain::client::QuantusClient, + message: &str, + from: &str, + password: Option, + password_file: Option, + origin_type: &str, + execution_mode: crate::cli::common::ExecutionMode, +) -> crate::error::Result<()> { + use qp_poseidon::PoseidonHasher; + + log_print!("📝 Submitting System::remark Proposal to Referenda"); + log_print!(" đŸ’Ŧ Message: {}", message.bright_cyan()); + log_print!(" 🔑 Submitted by: {}", from.bright_yellow()); + log_print!(" đŸŽ¯ Origin type: {}", origin_type.bright_magenta()); + + // Load wallet keypair + let keypair = crate::wallet::load_keypair_from_wallet(from, password, password_file)?; + + // Build System::remark call and encode it + let remark_bytes = message.as_bytes().to_vec(); + let remark_payload = quantus_subxt::api::tx().system().remark(remark_bytes.clone()); + let metadata = quantus_client.client().metadata(); + let encoded_call = <_ as subxt::tx::Payload>::encode_call_data(&remark_payload, &metadata) + .map_err(|e| QuantusError::Generic(format!("Failed to encode call data: {:?}", e)))?; + + log_verbose!("📝 Encoded call size: {} bytes", encoded_call.len()); + + // Compute preimage hash using Poseidon + let preimage_hash: sp_core::H256 = + ::hash(&encoded_call); + + log_print!("🔗 Preimage hash: {:?}", preimage_hash); + + // Submit Preimage::note_preimage + type PreimageBytes = quantus_subxt::api::preimage::calls::types::note_preimage::Bytes; + let bounded_bytes: PreimageBytes = encoded_call.clone(); + + log_print!("📝 Submitting preimage..."); + let note_preimage_tx = quantus_subxt::api::tx().preimage().note_preimage(bounded_bytes); + let preimage_tx_hash = + submit_transaction(quantus_client, &keypair, note_preimage_tx, None, execution_mode) + .await?; + log_print!("✅ Preimage transaction submitted: {:?}", preimage_tx_hash); + + // Wait for preimage transaction confirmation + log_print!("âŗ Waiting for preimage transaction confirmation..."); + + // Build Referenda::submit call using Lookup preimage reference + type ProposalBounded = + quantus_subxt::api::runtime_types::frame_support::traits::preimages::Bounded< + quantus_subxt::api::runtime_types::quantus_runtime::RuntimeCall, + quantus_subxt::api::runtime_types::qp_poseidon::PoseidonHasher, + >; + + let preimage_hash_subxt: subxt::utils::H256 = preimage_hash; + let proposal: ProposalBounded = + ProposalBounded::Lookup { hash: preimage_hash_subxt, len: encoded_call.len() as u32 }; + + // Create origin based on origin_type parameter + let account_id_sp = keypair.to_account_id_32(); + let account_id_subxt: subxt::ext::subxt_core::utils::AccountId32 = + subxt::ext::subxt_core::utils::AccountId32(*account_id_sp.as_ref()); + + let origin_caller = match origin_type.to_lowercase().as_str() { + "signed" => { + let raw_origin = + quantus_subxt::api::runtime_types::frame_support::dispatch::RawOrigin::Signed( + account_id_subxt, + ); + quantus_subxt::api::runtime_types::quantus_runtime::OriginCaller::system(raw_origin) + }, + "none" => { + let raw_origin = + quantus_subxt::api::runtime_types::frame_support::dispatch::RawOrigin::None; + quantus_subxt::api::runtime_types::quantus_runtime::OriginCaller::system(raw_origin) + }, + "root" => { + let raw_origin = + quantus_subxt::api::runtime_types::frame_support::dispatch::RawOrigin::Root; + quantus_subxt::api::runtime_types::quantus_runtime::OriginCaller::system(raw_origin) + }, + _ => + return Err(QuantusError::Generic(format!( + "Invalid origin type: {}. Must be 'signed', 'none', or 'root'", + origin_type + ))), + }; + + let enactment = + quantus_subxt::api::runtime_types::frame_support::traits::schedule::DispatchTime::After( + 10u32, // Execute 10 blocks after approval + ); + + log_print!("🔧 Creating Referenda::submit call..."); + let submit_call = + quantus_subxt::api::tx().referenda().submit(origin_caller, proposal, enactment); + + let tx_hash = + submit_transaction(quantus_client, &keypair, submit_call, None, execution_mode).await?; + log_print!( + "✅ {} Referendum proposal submitted! Hash: {:?}", + "SUCCESS".bright_green().bold(), + tx_hash + ); + + log_print!("💡 Use 'quantus referenda list' to see active proposals"); + Ok(()) +} + +/// Submit a proposal using existing preimage hash +async fn submit_proposal( + quantus_client: &crate::chain::client::QuantusClient, + preimage_hash: &str, + from: &str, + password: Option, + password_file: Option, + origin_type: &str, + execution_mode: crate::cli::common::ExecutionMode, +) -> crate::error::Result<()> { + log_print!("📝 Submitting Proposal to Referenda"); + log_print!(" 🔗 Preimage hash: {}", preimage_hash.bright_cyan()); + log_print!(" 🔑 Submitted by: {}", from.bright_yellow()); + log_print!(" đŸŽ¯ Origin type: {}", origin_type.bright_magenta()); + + // Parse preimage hash + let hash_str = preimage_hash.trim_start_matches("0x"); + let preimage_hash_parsed: sp_core::H256 = sp_core::H256::from_str(hash_str) + .map_err(|_| QuantusError::Generic("Invalid preimage hash format".to_string()))?; + + // Load wallet keypair + let keypair = crate::wallet::load_keypair_from_wallet(from, password, password_file)?; + + // Check if preimage exists and get its length + log_print!("🔍 Checking preimage status..."); + let latest_block_hash = quantus_client.get_latest_block().await?; + let storage_at = quantus_client.client().storage().at(latest_block_hash); + + let preimage_status = storage_at + .fetch( + &quantus_subxt::api::storage() + .preimage() + .request_status_for(preimage_hash_parsed), + ) + .await + .map_err(|e| QuantusError::Generic(format!("Failed to fetch preimage status: {:?}", e)))? + .ok_or_else(|| QuantusError::Generic("Preimage not found on chain".to_string()))?; + + let preimage_len = match preimage_status { + quantus_subxt::api::runtime_types::pallet_preimage::RequestStatus::Unrequested { + ticket: _, + len, + } => len, + quantus_subxt::api::runtime_types::pallet_preimage::RequestStatus::Requested { + maybe_ticket: _, + count: _, + maybe_len, + } => match maybe_len { + Some(len) => len, + None => return Err(QuantusError::Generic("Preimage length not available".to_string())), + }, + }; + + log_print!("✅ Preimage found! Length: {} bytes", preimage_len); + + // Build Referenda::submit call + type ProposalBounded = + quantus_subxt::api::runtime_types::frame_support::traits::preimages::Bounded< + quantus_subxt::api::runtime_types::quantus_runtime::RuntimeCall, + quantus_subxt::api::runtime_types::qp_poseidon::PoseidonHasher, + >; + + let preimage_hash_subxt: subxt::utils::H256 = preimage_hash_parsed; + let proposal: ProposalBounded = + ProposalBounded::Lookup { hash: preimage_hash_subxt, len: preimage_len }; + + // Create origin based on origin_type parameter + let account_id_sp = keypair.to_account_id_32(); + let account_id_subxt: subxt::ext::subxt_core::utils::AccountId32 = + subxt::ext::subxt_core::utils::AccountId32(*account_id_sp.as_ref()); + + let origin_caller = match origin_type.to_lowercase().as_str() { + "signed" => { + let raw_origin = + quantus_subxt::api::runtime_types::frame_support::dispatch::RawOrigin::Signed( + account_id_subxt, + ); + quantus_subxt::api::runtime_types::quantus_runtime::OriginCaller::system(raw_origin) + }, + "none" => { + let raw_origin = + quantus_subxt::api::runtime_types::frame_support::dispatch::RawOrigin::None; + quantus_subxt::api::runtime_types::quantus_runtime::OriginCaller::system(raw_origin) + }, + "root" => { + let raw_origin = + quantus_subxt::api::runtime_types::frame_support::dispatch::RawOrigin::Root; + quantus_subxt::api::runtime_types::quantus_runtime::OriginCaller::system(raw_origin) + }, + _ => + return Err(QuantusError::Generic(format!( + "Invalid origin type: {}. Must be 'signed', 'none', or 'root'", + origin_type + ))), + }; + + let enactment = + quantus_subxt::api::runtime_types::frame_support::traits::schedule::DispatchTime::After( + 10u32, + ); + + log_print!("🔧 Creating Referenda::submit call..."); + let submit_call = + quantus_subxt::api::tx().referenda().submit(origin_caller, proposal, enactment); + + let tx_hash = + submit_transaction(quantus_client, &keypair, submit_call, None, execution_mode).await?; + log_print!( + "✅ {} Referendum proposal submitted! Hash: {:?}", + "SUCCESS".bright_green().bold(), + tx_hash + ); + + log_print!("💡 Use 'quantus referenda list' to see active proposals"); + Ok(()) +} + +/// List recent Referenda proposals +async fn list_proposals( + quantus_client: &crate::chain::client::QuantusClient, +) -> crate::error::Result<()> { + log_print!("📜 Active Referenda Proposals"); + log_print!(""); + + let addr = quantus_subxt::api::storage().referenda().referendum_count(); + + let latest_block_hash = quantus_client.get_latest_block().await?; + let storage_at = quantus_client.client().storage().at(latest_block_hash); + + let count = storage_at.fetch(&addr).await?; + + if let Some(total) = count { + log_print!("📊 Total referenda created: {}", total); + if total == 0 { + log_print!("📭 No active proposals found"); + return Ok(()); + } + log_print!("🔍 Fetching recent referenda..."); + for i in (0..total).rev().take(10) { + get_proposal_status(quantus_client, i).await?; + log_print!("----------------------------------------"); + } + } else { + log_print!("📭 No referenda found - Referenda may be empty"); + } + + Ok(()) +} + +/// Get details of a specific Referendum +async fn get_proposal_details( + quantus_client: &crate::chain::client::QuantusClient, + index: u32, + decode: bool, +) -> crate::error::Result<()> { + use quantus_subxt::api::runtime_types::pallet_referenda::types::ReferendumInfo; + + log_print!("📄 Referendum #{} Details", index); + log_print!(""); + + let addr = quantus_subxt::api::storage().referenda().referendum_info_for(index); + + let latest_block_hash = quantus_client.get_latest_block().await?; + let storage_at = quantus_client.client().storage().at(latest_block_hash); + + let info = storage_at.fetch(&addr).await?; + + if let Some(referendum_info) = info { + if decode { + // Try to decode the proposal + match &referendum_info { + ReferendumInfo::Ongoing(status) => { + log_print!("📊 {} Referendum #{}", "Ongoing".bright_green(), index); + log_print!(" đŸ›¤ī¸ Track: {}", status.track); + log_print!(" 📅 Submitted: Block #{}", status.submitted); + log_print!( + " đŸ—ŗī¸ Tally: Ayes: {}, Nays: {}, Support: {}", + status.tally.ayes, + status.tally.nays, + status.tally.support + ); + log_print!(""); + + // Extract preimage hash and length from proposal + if let quantus_subxt::api::runtime_types::frame_support::traits::preimages::Bounded::Lookup { + hash, + len, + } = &status.proposal + { + log_print!("📝 Proposal Details:"); + log_print!(" 🔗 Preimage Hash: {:?}", hash); + log_print!(" 📏 Length: {} bytes", len); + log_print!(""); + + // Fetch and decode the preimage + match crate::cli::referenda_decode::decode_preimage(quantus_client, hash, *len).await { + Ok(decoded) => { + log_print!("✅ Decoded Proposal:"); + log_print!("{}", decoded); + }, + Err(e) => { + log_print!("âš ī¸ Could not decode proposal: {}", e); + log_print!(" Run 'quantus preimage get --hash {:?} --len {}' to see raw data", hash, len); + }, + } + } else { + log_print!("âš ī¸ Proposal is inline (not a preimage lookup)"); + } + }, + ReferendumInfo::Approved(..) => { + log_print!("📊 {} Referendum #{}", "Approved".green(), index); + log_print!( + " â„šī¸ Proposal details no longer available (referendum finalized)" + ); + }, + ReferendumInfo::Rejected(..) => { + log_print!("📊 {} Referendum #{}", "Rejected".red(), index); + log_print!( + " â„šī¸ Proposal details no longer available (referendum finalized)" + ); + }, + ReferendumInfo::Cancelled(..) => { + log_print!("📊 {} Referendum #{}", "Cancelled".yellow(), index); + log_print!( + " â„šī¸ Proposal details no longer available (referendum finalized)" + ); + }, + ReferendumInfo::TimedOut(..) => { + log_print!("📊 {} Referendum #{}", "TimedOut".dimmed(), index); + log_print!( + " â„šī¸ Proposal details no longer available (referendum finalized)" + ); + }, + ReferendumInfo::Killed(..) => { + log_print!("📊 {} Referendum #{}", "Killed".red().bold(), index); + log_print!(" â„šī¸ Proposal details no longer available (referendum killed)"); + }, + } + } else { + // Raw output (original behavior) + log_print!("📋 Referendum Information (raw):"); + log_print!("{:#?}", referendum_info); + } + } else { + log_print!("📭 Referendum #{} not found", index); + } + Ok(()) +} + +/// Get the status of a Referendum +async fn get_proposal_status( + quantus_client: &crate::chain::client::QuantusClient, + index: u32, +) -> crate::error::Result<()> { + use quantus_subxt::api::runtime_types::pallet_referenda::types::ReferendumInfo; + + log_verbose!("📊 Fetching status for Referendum #{}...", index); + + let addr = quantus_subxt::api::storage().referenda().referendum_info_for(index); + + let latest_block_hash = quantus_client.get_latest_block().await?; + let storage_at = quantus_client.client().storage().at(latest_block_hash); + + let info_res = storage_at.fetch(&addr).await; + + match info_res { + Ok(Some(info)) => { + log_print!("📊 Status for Referendum #{}", index.to_string().bright_yellow()); + match info { + ReferendumInfo::Ongoing(status) => { + log_print!(" - Status: {}", "Ongoing".bright_green()); + log_print!(" - Track: {}", status.track); + log_print!(" - Submitted at: block {}", status.submitted); + log_print!( + " - Tally: Ayes: {}, Nays: {}", + status.tally.ayes, + status.tally.nays + ); + log_verbose!(" - Full status: {:#?}", status); + }, + ReferendumInfo::Approved(submitted, ..) => { + log_print!(" - Status: {}", "Approved".green()); + log_print!(" - Submitted at block: {}", submitted); + }, + ReferendumInfo::Rejected(submitted, ..) => { + log_print!(" - Status: {}", "Rejected".red()); + log_print!(" - Submitted at block: {}", submitted); + }, + ReferendumInfo::Cancelled(submitted, ..) => { + log_print!(" - Status: {}", "Cancelled".yellow()); + log_print!(" - Submitted at block: {}", submitted); + }, + ReferendumInfo::TimedOut(submitted, ..) => { + log_print!(" - Status: {}", "TimedOut".dimmed()); + log_print!(" - Submitted at block: {}", submitted); + }, + ReferendumInfo::Killed(submitted) => { + log_print!(" - Status: {}", "Killed".red().bold()); + log_print!(" - Killed at block: {}", submitted); + }, + } + }, + Ok(None) => log_print!("📭 Referendum #{} not found", index), + Err(e) => log_error!("❌ Failed to fetch referendum #{}: {:?}", index, e), + } + + Ok(()) +} + +/// Place a decision deposit for a Referendum +async fn place_decision_deposit( + quantus_client: &crate::chain::client::QuantusClient, + index: u32, + from: &str, + password: Option, + password_file: Option, + execution_mode: crate::cli::common::ExecutionMode, +) -> crate::error::Result<()> { + log_print!("📋 Placing decision deposit for Referendum #{}", index); + log_print!(" 🔑 Placed by: {}", from.bright_yellow()); + + let keypair = crate::wallet::load_keypair_from_wallet(from, password, password_file)?; + + let deposit_call = quantus_subxt::api::tx().referenda().place_decision_deposit(index); + let tx_hash = + submit_transaction(quantus_client, &keypair, deposit_call, None, execution_mode).await?; + log_success!("✅ Decision deposit placed! Hash: {:?}", tx_hash.to_string().bright_yellow()); + Ok(()) +} + +/// Vote on a Referendum +async fn vote_on_referendum( + quantus_client: &crate::chain::client::QuantusClient, + index: u32, + aye: bool, + conviction: u8, + amount: &str, + from: &str, + password: Option, + password_file: Option, + execution_mode: crate::cli::common::ExecutionMode, +) -> crate::error::Result<()> { + log_print!("đŸ—ŗī¸ Voting on Referendum #{}", index); + log_print!(" 📊 Vote: {}", if aye { "AYE ✅".bright_green() } else { "NAY ❌".bright_red() }); + log_print!(" 💰 Amount: {}", amount.bright_cyan()); + log_print!(" 🔒 Conviction: {}", conviction); + log_print!(" 🔑 Signed by: {}", from.bright_yellow()); + + let keypair = crate::wallet::load_keypair_from_wallet(from, password, password_file)?; + + // Parse amount + let amount_value: u128 = (amount + .parse::() + .map_err(|_| QuantusError::Generic("Invalid amount format".to_string()))? + .max(0.0) * + 1_000_000_000_000_000_000.0) as u128; + + // Validate conviction + if conviction > 6 { + return Err(QuantusError::Generic("Invalid conviction (must be 0-6)".to_string())); + } + + // Build vote + let vote = + quantus_subxt::api::runtime_types::pallet_conviction_voting::vote::AccountVote::Standard { + vote: quantus_subxt::api::runtime_types::pallet_conviction_voting::vote::Vote( + if aye { 128 } else { 0 } | conviction, + ), + balance: amount_value, + }; + + let vote_call = quantus_subxt::api::tx().conviction_voting().vote(index, vote); + let tx_hash = + submit_transaction(quantus_client, &keypair, vote_call, None, execution_mode).await?; + + log_print!( + "✅ {} Vote transaction submitted! Hash: {:?}", + "SUCCESS".bright_green().bold(), + tx_hash + ); + + log_success!("🎉 {} Vote submitted!", "FINISHED".bright_green().bold()); + Ok(()) +} + +/// Get Referenda configuration +async fn get_config( + quantus_client: &crate::chain::client::QuantusClient, +) -> crate::error::Result<()> { + log_print!("âš™ī¸ Referenda Configuration"); + log_print!(""); + + let constants = quantus_client.client().constants(); + let tracks_addr = quantus_subxt::api::constants().referenda().tracks(); + + match constants.at(&tracks_addr) { + Ok(tracks) => { + log_print!("{}", "📊 Track Configuration:".bold()); + for (id, info) in tracks.iter() { + log_print!(" ------------------------------------"); + log_print!( + " â€ĸ {} #{}: {}", + "Track".bold(), + id, + info.name.to_string().bright_cyan() + ); + log_print!(" â€ĸ Max Deciding: {}", info.max_deciding); + log_print!(" â€ĸ Decision Deposit: {}", info.decision_deposit); + log_print!(" â€ĸ Prepare Period: {} blocks", info.prepare_period); + log_print!(" â€ĸ Decision Period: {} blocks", info.decision_period); + log_print!(" â€ĸ Confirm Period: {} blocks", info.confirm_period); + log_print!(" â€ĸ Min Enactment Period: {} blocks", info.min_enactment_period); + } + log_print!(" ------------------------------------"); + }, + Err(e) => { + log_error!("❌ Failed to decode Tracks constant: {:?}", e); + log_print!("💡 It's possible the Tracks constant is not in the expected format."); + }, + } + + Ok(()) +} + +/// Refund submission deposit for a completed Referendum +async fn refund_submission_deposit( + quantus_client: &crate::chain::client::QuantusClient, + index: u32, + from: &str, + password: Option, + password_file: Option, + execution_mode: crate::cli::common::ExecutionMode, +) -> crate::error::Result<()> { + log_print!("💰 Refunding submission deposit for Referendum #{}", index); + log_print!(" 🔑 Refund to: {}", from.bright_yellow()); + + // Load wallet keypair + let keypair = crate::wallet::load_keypair_from_wallet(from, password, password_file)?; + + // Create refund_submission_deposit call + let refund_call = quantus_subxt::api::tx().referenda().refund_submission_deposit(index); + + let tx_hash = + submit_transaction(quantus_client, &keypair, refund_call, None, execution_mode).await?; + log_print!( + "✅ {} Refund transaction submitted! Hash: {:?}", + "SUCCESS".bright_green().bold(), + tx_hash + ); + + log_print!("💡 Check your balance to confirm the refund"); + Ok(()) +} + +/// Refund decision deposit for a completed Referendum +async fn refund_decision_deposit( + quantus_client: &crate::chain::client::QuantusClient, + index: u32, + from: &str, + password: Option, + password_file: Option, + execution_mode: crate::cli::common::ExecutionMode, +) -> crate::error::Result<()> { + log_print!("💰 Refunding decision deposit for Referendum #{}", index); + log_print!(" 🔑 Refund to: {}", from.bright_yellow()); + + // Load wallet keypair + let keypair = crate::wallet::load_keypair_from_wallet(from, password, password_file)?; + + // Create refund_decision_deposit call + let refund_call = quantus_subxt::api::tx().referenda().refund_decision_deposit(index); + + let tx_hash = + submit_transaction(quantus_client, &keypair, refund_call, None, execution_mode).await?; + log_print!( + "✅ {} Refund transaction submitted! Hash: {:?}", + "SUCCESS".bright_green().bold(), + tx_hash + ); + + log_print!("💡 Check your balance to confirm the refund"); + Ok(()) +} diff --git a/src/cli/referenda_decode.rs b/src/cli/referenda_decode.rs new file mode 100644 index 0000000..7140fa7 --- /dev/null +++ b/src/cli/referenda_decode.rs @@ -0,0 +1,243 @@ +//! Decoding utilities for referendum proposals + +use crate::error::QuantusError; +use codec::Decode; +use colored::Colorize; + +/// Decode preimage call data into human-readable format +pub async fn decode_preimage( + quantus_client: &crate::chain::client::QuantusClient, + hash: &subxt::utils::H256, + len: u32, +) -> crate::error::Result { + // Fetch preimage from storage + let latest_block_hash = quantus_client.get_latest_block().await?; + let storage_at = quantus_client.client().storage().at(latest_block_hash); + + let preimage_addr = crate::chain::quantus_subxt::api::storage() + .preimage() + .preimage_for((*hash, len)); + + let preimage_result = storage_at.fetch(&preimage_addr).await; + + let content = match preimage_result { + Ok(Some(bounded_vec)) => bounded_vec.0, + Ok(None) => + return Err(QuantusError::Generic(format!("Preimage not found for hash {:?}", hash))), + Err(e) => return Err(QuantusError::Generic(format!("Error fetching preimage: {:?}", e))), + }; + + // Decode using direct Decode trait (RuntimeCall implements it via DecodeAsType derive) + decode_runtime_call_direct(&content) +} + +/// Decode RuntimeCall directly using Decode trait +fn decode_runtime_call_direct(data: &[u8]) -> crate::error::Result { + // First, let's try to understand the call structure by reading indices + if data.len() < 3 { + return Err(QuantusError::Generic("Call data too short".to_string())); + } + + let pallet_index = data[0]; + let inner_index = data[1]; + let call_index = data[2]; + + match (pallet_index, inner_index, call_index) { + // System pallet (0, 0, X) + // Special case: if call_index looks like Compact (high value like 0xe8), + // it might be remark (call 0) where the call index byte is omitted + (0, 0, idx) if idx > 100 => { + // Likely remark (call 0) with Compact-encoded Vec starting at byte 2 + decode_system_remark_no_index(&data[2..]) + }, + (0, 0, _) => decode_system_call(&data[2..]), + + // TreasuryPallet (18, 5, X) where X is any spend variant (11, 15, 19, etc.) + // Different indices represent different value ranges/encodings + (18, 5, _) => decode_treasury_spend_call(&data[3..]), + + // Unknown + _ => Ok(format!( + " {} {} {} {}\n {} {} bytes\n {}:\n {}", + "Call Indices:".dimmed(), + pallet_index, + inner_index, + call_index, + "Args:".dimmed(), + data.len() - 3, + "Raw Hex".dimmed(), + hex::encode(&data[3..]).bright_green() + )), + } +} + +/// Decode System::remark when call index byte is omitted (call 0) +fn decode_system_remark_no_index(args: &[u8]) -> crate::error::Result { + // args starts directly with Compact-encoded Vec + let mut cursor = args; + let remark_bytes: Vec = Vec::decode(&mut cursor) + .map_err(|e| QuantusError::Generic(format!("Failed to decode remark: {:?}", e)))?; + let remark_str = String::from_utf8_lossy(&remark_bytes); + + Ok(format!( + " {} {}\n {} {}\n {}:\n {} \"{}\"", + "Pallet:".dimmed(), + "System".bright_cyan(), + "Call:".dimmed(), + "remark".bright_yellow(), + "Parameters".dimmed(), + "message:".dimmed(), + remark_str.bright_green() + )) +} + +/// Decode System pallet calls +fn decode_system_call(data_from_call: &[u8]) -> crate::error::Result { + if data_from_call.is_empty() { + return Err(QuantusError::Generic("Empty system call data".to_string())); + } + + let call_index = data_from_call[0]; + let args = &data_from_call[1..]; + + match call_index { + 0 => { + // remark - standard Vec + let mut cursor = args; + let remark_bytes: Vec = Vec::decode(&mut cursor) + .map_err(|e| QuantusError::Generic(format!("Failed to decode remark: {:?}", e)))?; + let remark_str = String::from_utf8_lossy(&remark_bytes); + + Ok(format!( + " {} {}\n {} {}\n {}:\n {} \"{}\"", + "Pallet:".dimmed(), + "System".bright_cyan(), + "Call:".dimmed(), + "remark".bright_yellow(), + "Parameters".dimmed(), + "message:".dimmed(), + remark_str.bright_green() + )) + }, + 1 => { + // remark_with_event - has different encoding, try decoding from byte 1 + let remark_str = if args.len() > 1 { + String::from_utf8_lossy(&args[1..]) + } else { + String::from_utf8_lossy(args) + }; + + Ok(format!( + " {} {}\n {} {}\n {}:\n {} \"{}\"", + "Pallet:".dimmed(), + "System".bright_cyan(), + "Call:".dimmed(), + "remark_with_event".bright_yellow(), + "Parameters".dimmed(), + "message:".dimmed(), + remark_str.bright_green() + )) + }, + 7 => { + // set_code + Ok(format!( + " {} {}\n {} {} {}\n {} {}", + "Pallet:".dimmed(), + "System".bright_cyan(), + "Call:".dimmed(), + "set_code".bright_yellow(), + "(Runtime Upgrade)".dimmed(), + "Parameters:".dimmed(), + "".bright_green() + )) + }, + _ => Ok(format!( + " {} {}\n {} {} (index {})", + "Pallet:".dimmed(), + "System".bright_cyan(), + "Call:".dimmed(), + "unknown".yellow(), + call_index + )), + } +} + +/// Decode TreasuryPallet::spend call arguments +/// The amount is stored as variable-length u128 in little-endian +fn decode_treasury_spend_call(args: &[u8]) -> crate::error::Result { + use sp_core::crypto::Ss58Codec; + + crate::log_verbose!("Decoding treasury spend, args length: {} bytes", args.len()); + crate::log_verbose!("Args hex: {}", hex::encode(args)); + + if args.len() < 34 { + return Err(QuantusError::Generic(format!( + "Args too short for treasury spend: {} bytes (expected 40-42)", + args.len() + ))); + } + + // Structure (discovered through empirical analysis): + // - asset_kind: Box<()> = 0 bytes (unit type has no encoding) + // - amount: u128 = variable bytes (7-8 bytes typically) as little-endian + // - beneficiary: Box = 32 bytes (no variant byte!) + // - valid_from: Option = 1 byte (0x00 for None) + + // The amount length varies based on the value: + // - Small values (< 256TB): 7 bytes + // - Larger values: 8+ bytes + // Total length is typically 40 bytes (7+32+1) or 42 bytes (8+32+1) or similar + + // Calculate amount bytes length: total - 32 (beneficiary) - 1 (valid_from) + let amount_bytes_len = args.len() - 32 - 1; + if !(1..=16).contains(&amount_bytes_len) { + return Err(QuantusError::Generic(format!( + "Invalid amount bytes length: {}", + amount_bytes_len + ))); + } + + // Decode amount: first N bytes as little-endian u128 + let mut amount_bytes_extended = [0u8; 16]; + amount_bytes_extended[..amount_bytes_len].copy_from_slice(&args[..amount_bytes_len]); + let amount = u128::from_le_bytes(amount_bytes_extended); + + // Decode beneficiary: starts after amount bytes, 32 bytes + let beneficiary_start = amount_bytes_len; + let account_bytes: [u8; 32] = args[beneficiary_start..beneficiary_start + 32] + .try_into() + .map_err(|_| QuantusError::Generic("Failed to extract beneficiary bytes".to_string()))?; + let sp_account = sp_core::crypto::AccountId32::from(account_bytes); + let ss58 = sp_account.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(42)); + let beneficiary_str = format!("{} ({}...{})", ss58, &ss58[..8], &ss58[ss58.len() - 6..]); + + // Decode valid_from: last byte + let valid_from_byte = args[args.len() - 1]; + let valid_from_str = if valid_from_byte == 0 { + "None (immediate)".to_string() + } else { + format!("Some (byte: 0x{:02x})", valid_from_byte) + }; + + // Format amount in QUAN (1 QUAN = 10^12) + let quan = amount as f64 / 1_000_000_000_000.0; + + Ok(format!( + " {} {}\n {} {}\n {}:\n {} {} {} ({} raw)\n {} {}\n {} {}\n\n {} {}", + "Pallet:".dimmed(), + "TreasuryPallet".bright_cyan(), + "Call:".dimmed(), + "spend".bright_yellow(), + "Parameters".dimmed(), + "amount:".dimmed(), + quan.to_string().bright_green().bold(), + "QUAN".bright_green(), + amount, + "beneficiary:".dimmed(), + beneficiary_str.bright_green(), + "valid_from:".dimmed(), + valid_from_str.bright_green(), + "💡 Info:".cyan(), + "Vote YES if you approve this Treasury spend, NO to reject.".cyan() + )) +} diff --git a/src/cli/tech_referenda.rs b/src/cli/tech_referenda.rs new file mode 100644 index 0000000..6fc8860 --- /dev/null +++ b/src/cli/tech_referenda.rs @@ -0,0 +1,771 @@ +//! `quantus tech-referenda` subcommand - manage Tech Referenda proposals +use crate::{ + chain::quantus_subxt, cli::common::submit_transaction, error::QuantusError, log_error, + log_print, log_success, log_verbose, +}; +use clap::Subcommand; +use colored::Colorize; +use std::{path::PathBuf, str::FromStr}; + +/// Tech Referenda management commands +#[derive(Subcommand, Debug)] +pub enum TechReferendaCommands { + /// Submit a runtime upgrade proposal to Tech Referenda (requires existing preimage) + Submit { + /// Preimage hash (must already exist on chain) + #[arg(long)] + preimage_hash: String, + + /// Wallet name to sign with (must be a Tech Collective member or root) + #[arg(short, long)] + from: String, + + /// Password for the wallet + #[arg(short, long)] + password: Option, + + /// Read password from file + #[arg(long)] + password_file: Option, + }, + + /// Submit a runtime upgrade proposal to Tech Referenda (creates preimage first) + SubmitWithPreimage { + /// Path to the runtime WASM file + #[arg(short, long)] + wasm_file: PathBuf, + + /// Wallet name to sign with (must be a Tech Collective member or root) + #[arg(short, long)] + from: String, + + /// Password for the wallet + #[arg(short, long)] + password: Option, + + /// Read password from file + #[arg(long)] + password_file: Option, + }, + + /// List all active Tech Referenda proposals + List, + + /// Get details of a specific Tech Referendum + Get { + /// Referendum index + #[arg(short, long)] + index: u32, + }, + + /// Check the status of a Tech Referendum + Status { + /// Referendum index + #[arg(short, long)] + index: u32, + }, + + /// Place a decision deposit for a Tech Referendum + PlaceDecisionDeposit { + /// Referendum index + #[arg(short, long)] + index: u32, + + /// Wallet name to sign with + #[arg(short, long)] + from: String, + + /// Password for the wallet + #[arg(short, long)] + password: Option, + + /// Read password from file + #[arg(long)] + password_file: Option, + }, + + /// Cancel a Tech Referendum (requires root permissions) + Cancel { + /// Referendum index to cancel + #[arg(short, long)] + index: u32, + + /// Wallet name to sign with (must have root permissions) + #[arg(short, long)] + from: String, + + /// Password for the wallet + #[arg(short, long)] + password: Option, + + /// Read password from file + #[arg(long)] + password_file: Option, + }, + + /// Kill a Tech Referendum (requires root permissions) + Kill { + /// Referendum index to kill + #[arg(short, long)] + index: u32, + + /// Wallet name to sign with (must have root permissions) + #[arg(short, long)] + from: String, + + /// Password for the wallet + #[arg(short, long)] + password: Option, + + /// Read password from file + #[arg(long)] + password_file: Option, + }, + + /// Nudge a Tech Referendum to next phase (sudo origin) + Nudge { + /// Referendum index to nudge + #[arg(short, long)] + index: u32, + + /// Wallet name to sign with + #[arg(short, long)] + from: String, + + /// Password for the wallet + #[arg(short, long)] + password: Option, + + /// Read password from file + #[arg(long)] + password_file: Option, + }, + + /// Refund submission deposit for a completed Tech Referendum + RefundSubmissionDeposit { + /// Referendum index + #[arg(short, long)] + index: u32, + + /// Wallet name that submitted the referendum + #[arg(short, long)] + from: String, + + /// Password for the wallet + #[arg(short, long)] + password: Option, + + /// Read password from file + #[arg(long)] + password_file: Option, + }, + + /// Refund decision deposit for a completed Tech Referendum + RefundDecisionDeposit { + /// Referendum index + #[arg(short, long)] + index: u32, + + /// Wallet name that placed the decision deposit + #[arg(short, long)] + from: String, + + /// Password for the wallet + #[arg(short, long)] + password: Option, + + /// Read password from file + #[arg(long)] + password_file: Option, + }, + + /// Get Tech Referenda configuration + Config, +} + +/// Handle tech referenda commands +pub async fn handle_tech_referenda_command( + command: TechReferendaCommands, + node_url: &str, + execution_mode: crate::cli::common::ExecutionMode, +) -> crate::error::Result<()> { + let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?; + + match command { + TechReferendaCommands::Submit { preimage_hash, from, password, password_file } => + submit_runtime_upgrade( + &quantus_client, + &preimage_hash, + &from, + password, + password_file, + execution_mode, + ) + .await, + TechReferendaCommands::SubmitWithPreimage { wasm_file, from, password, password_file } => + submit_runtime_upgrade_with_preimage( + &quantus_client, + &wasm_file, + &from, + password, + password_file, + execution_mode, + ) + .await, + TechReferendaCommands::List => list_proposals(&quantus_client).await, + TechReferendaCommands::Get { index } => get_proposal_details(&quantus_client, index).await, + TechReferendaCommands::Status { index } => + get_proposal_status(&quantus_client, index).await, + TechReferendaCommands::PlaceDecisionDeposit { index, from, password, password_file } => + place_decision_deposit( + &quantus_client, + index, + &from, + password, + password_file, + execution_mode, + ) + .await, + TechReferendaCommands::Cancel { index, from, password, password_file } => + cancel_proposal(&quantus_client, index, &from, password, password_file, execution_mode) + .await, + TechReferendaCommands::Kill { index, from, password, password_file } => + kill_proposal(&quantus_client, index, &from, password, password_file, execution_mode) + .await, + TechReferendaCommands::Nudge { index, from, password, password_file } => + nudge_proposal(&quantus_client, index, &from, password, password_file, execution_mode) + .await, + TechReferendaCommands::RefundSubmissionDeposit { index, from, password, password_file } => + refund_submission_deposit( + &quantus_client, + index, + &from, + password, + password_file, + execution_mode, + ) + .await, + TechReferendaCommands::RefundDecisionDeposit { index, from, password, password_file } => + refund_decision_deposit( + &quantus_client, + index, + &from, + password, + password_file, + execution_mode, + ) + .await, + TechReferendaCommands::Config => get_config(&quantus_client).await, + } +} + +/// Submit a runtime upgrade proposal to Tech Referenda (uses existing preimage) +async fn submit_runtime_upgrade( + quantus_client: &crate::chain::client::QuantusClient, + preimage_hash: &str, + from: &str, + password: Option, + password_file: Option, + execution_mode: crate::cli::common::ExecutionMode, +) -> crate::error::Result<()> { + log_print!("📝 Submitting Runtime Upgrade Proposal to Tech Referenda"); + log_print!(" 🔗 Preimage hash: {}", preimage_hash.bright_cyan()); + log_print!(" 🔑 Submitted by: {}", from.bright_yellow()); + + // Parse preimage hash (trim 0x) + let hash_str = preimage_hash.trim_start_matches("0x"); + let preimage_hash_parsed: sp_core::H256 = sp_core::H256::from_str(hash_str) + .map_err(|_| QuantusError::Generic("Invalid preimage hash format".to_string()))?; + + // Load wallet keypair + let keypair = crate::wallet::load_keypair_from_wallet(from, password, password_file)?; + + // Check if preimage exists and get its length + log_print!("🔍 Checking preimage status..."); + let latest_block_hash = quantus_client.get_latest_block().await?; + let storage_at = quantus_client.client().storage().at(latest_block_hash); + + let preimage_status = storage_at + .fetch( + &quantus_subxt::api::storage() + .preimage() + .request_status_for(preimage_hash_parsed), + ) + .await + .map_err(|e| QuantusError::Generic(format!("Failed to fetch preimage status: {:?}", e)))? + .ok_or_else(|| QuantusError::Generic("Preimage not found on chain".to_string()))?; + + let preimage_len = match preimage_status { + quantus_subxt::api::runtime_types::pallet_preimage::RequestStatus::Unrequested { + ticket: _, + len, + } => len, + quantus_subxt::api::runtime_types::pallet_preimage::RequestStatus::Requested { + maybe_ticket: _, + count: _, + maybe_len, + } => match maybe_len { + Some(len) => len, + None => return Err(QuantusError::Generic("Preimage length not available".to_string())), + }, + }; + + log_print!("✅ Preimage found! Length: {} bytes", preimage_len); + + // Build TechReferenda::submit call using Lookup preimage reference + type ProposalBounded = + quantus_subxt::api::runtime_types::frame_support::traits::preimages::Bounded< + quantus_subxt::api::runtime_types::quantus_runtime::RuntimeCall, + quantus_subxt::api::runtime_types::qp_poseidon::PoseidonHasher, + >; + + let preimage_hash_subxt: subxt::utils::H256 = preimage_hash_parsed; + let proposal: ProposalBounded = + ProposalBounded::Lookup { hash: preimage_hash_subxt, len: preimage_len }; + + let raw_origin_root = + quantus_subxt::api::runtime_types::frame_support::dispatch::RawOrigin::Root; + let origin_caller = + quantus_subxt::api::runtime_types::quantus_runtime::OriginCaller::system(raw_origin_root); + + let enactment = + quantus_subxt::api::runtime_types::frame_support::traits::schedule::DispatchTime::After( + 0u32, + ); + + log_print!("🔧 Creating TechReferenda::submit call..."); + let submit_call = + quantus_subxt::api::tx() + .tech_referenda() + .submit(origin_caller, proposal, enactment); + + let tx_hash = + submit_transaction(quantus_client, &keypair, submit_call, None, execution_mode).await?; + log_print!( + "✅ {} Runtime upgrade proposal submitted! Hash: {:?}", + "SUCCESS".bright_green().bold(), + tx_hash + ); + + log_print!("💡 Use 'quantus tech-referenda list' to see active proposals"); + Ok(()) +} + +/// Submit a runtime upgrade proposal to Tech Referenda (creates preimage first) +async fn submit_runtime_upgrade_with_preimage( + quantus_client: &crate::chain::client::QuantusClient, + wasm_file: &PathBuf, + from: &str, + password: Option, + password_file: Option, + execution_mode: crate::cli::common::ExecutionMode, +) -> crate::error::Result<()> { + use qp_poseidon::PoseidonHasher; + + log_print!("📝 Submitting Runtime Upgrade Proposal to Tech Referenda"); + log_print!(" 📂 WASM file: {}", wasm_file.display().to_string().bright_cyan()); + log_print!(" 🔑 Submitted by: {}", from.bright_yellow()); + + if !wasm_file.exists() { + return Err(QuantusError::Generic(format!("WASM file not found: {}", wasm_file.display()))); + } + + if let Some(ext) = wasm_file.extension() { + if ext != "wasm" { + log_verbose!("âš ī¸ Warning: File doesn't have .wasm extension"); + } + } + + // Read WASM file + let wasm_code = std::fs::read(wasm_file) + .map_err(|e| QuantusError::Generic(format!("Failed to read WASM file: {}", e)))?; + + log_print!("📊 WASM file size: {} bytes", wasm_code.len()); + + // Load wallet keypair + let keypair = crate::wallet::load_keypair_from_wallet(from, password, password_file)?; + + // Build a static payload for System::set_code and encode full call data (pallet + call + args) + let set_code_payload = quantus_subxt::api::tx().system().set_code(wasm_code.clone()); + let metadata = quantus_client.client().metadata(); + let encoded_call = <_ as subxt::tx::Payload>::encode_call_data(&set_code_payload, &metadata) + .map_err(|e| QuantusError::Generic(format!("Failed to encode call data: {:?}", e)))?; + + log_verbose!("📝 Encoded call size: {} bytes", encoded_call.len()); + + // Compute preimage hash using Poseidon (runtime uses PoseidonHasher) + let preimage_hash: sp_core::H256 = + ::hash(&encoded_call); + + log_print!("🔗 Preimage hash: {:?}", preimage_hash); + + // Submit Preimage::note_preimage with bounded bytes + type PreimageBytes = quantus_subxt::api::preimage::calls::types::note_preimage::Bytes; + let bounded_bytes: PreimageBytes = encoded_call.clone(); + + log_print!("📝 Submitting preimage..."); + let note_preimage_tx = quantus_subxt::api::tx().preimage().note_preimage(bounded_bytes); + let preimage_tx_hash = + submit_transaction(quantus_client, &keypair, note_preimage_tx, None, execution_mode) + .await?; + log_print!("✅ Preimage transaction submitted: {:?}", preimage_tx_hash); + + // Wait for preimage transaction confirmation + log_print!("âŗ Waiting for preimage transaction confirmation..."); + + // Build TechReferenda::submit call using Lookup preimage reference + type ProposalBounded = + quantus_subxt::api::runtime_types::frame_support::traits::preimages::Bounded< + quantus_subxt::api::runtime_types::quantus_runtime::RuntimeCall, + quantus_subxt::api::runtime_types::qp_poseidon::PoseidonHasher, + >; + + let preimage_hash_subxt: subxt::utils::H256 = preimage_hash; + let proposal: ProposalBounded = + ProposalBounded::Lookup { hash: preimage_hash_subxt, len: encoded_call.len() as u32 }; + + let raw_origin_root = + quantus_subxt::api::runtime_types::frame_support::dispatch::RawOrigin::Root; + let origin_caller = + quantus_subxt::api::runtime_types::quantus_runtime::OriginCaller::system(raw_origin_root); + + let enactment = + quantus_subxt::api::runtime_types::frame_support::traits::schedule::DispatchTime::After( + 0u32, + ); + + log_print!("🔧 Creating TechReferenda::submit call..."); + let submit_call = + quantus_subxt::api::tx() + .tech_referenda() + .submit(origin_caller, proposal, enactment); + + let tx_hash = + submit_transaction(quantus_client, &keypair, submit_call, None, execution_mode).await?; + log_print!( + "✅ {} Runtime upgrade proposal submitted! Hash: {:?}", + "SUCCESS".bright_green().bold(), + tx_hash + ); + + log_print!("💡 Use 'quantus tech-referenda list' to see active proposals"); + Ok(()) +} + +/// List recent Tech Referenda proposals +async fn list_proposals( + quantus_client: &crate::chain::client::QuantusClient, +) -> crate::error::Result<()> { + log_print!("📜 Active Tech Referenda Proposals"); + log_print!(""); + + let addr = quantus_subxt::api::storage().tech_referenda().referendum_count(); + + // Get the latest block hash to read from the latest state (not finalized) + let latest_block_hash = quantus_client.get_latest_block().await?; + let storage_at = quantus_client.client().storage().at(latest_block_hash); + + let count = storage_at.fetch(&addr).await?; + + if let Some(total) = count { + log_print!("📊 Total referenda created: {}", total); + if total == 0 { + log_print!("📭 No active proposals found"); + return Ok(()); + } + log_print!("🔍 Fetching recent referenda..."); + for i in (0..total).rev().take(10) { + get_proposal_status(quantus_client, i).await?; + log_print!("----------------------------------------"); + } + } else { + log_print!("📭 No referenda found - Tech Referenda may be empty"); + } + + Ok(()) +} + +/// Get details of a specific Tech Referendum +async fn get_proposal_details( + quantus_client: &crate::chain::client::QuantusClient, + index: u32, +) -> crate::error::Result<()> { + log_print!("📄 Tech Referendum #{} Details", index); + log_print!(""); + + let addr = quantus_subxt::api::storage().tech_referenda().referendum_info_for(index); + + // Get the latest block hash to read from the latest state (not finalized) + let latest_block_hash = quantus_client.get_latest_block().await?; + let storage_at = quantus_client.client().storage().at(latest_block_hash); + + let info = storage_at.fetch(&addr).await?; + + if let Some(referendum_info) = info { + log_print!("📋 Referendum Information (raw):"); + log_print!("{:#?}", referendum_info); + } else { + log_print!("📭 Referendum #{} not found", index); + } + Ok(()) +} + +/// Get the status of a Tech Referendum +async fn get_proposal_status( + quantus_client: &crate::chain::client::QuantusClient, + index: u32, +) -> crate::error::Result<()> { + use quantus_subxt::api::runtime_types::pallet_referenda::types::ReferendumInfo; + + log_verbose!("📊 Fetching status for Tech Referendum #{}...", index); + + let addr = quantus_subxt::api::storage().tech_referenda().referendum_info_for(index); + + // Get the latest block hash to read from the latest state (not finalized) + let latest_block_hash = quantus_client.get_latest_block().await?; + let storage_at = quantus_client.client().storage().at(latest_block_hash); + + let info_res = storage_at.fetch(&addr).await; + + match info_res { + Ok(Some(info)) => { + log_print!("📊 Status for Referendum #{}", index.to_string().bright_yellow()); + match info { + ReferendumInfo::Ongoing(status) => { + log_print!(" - Status: {}", "Ongoing".bright_green()); + log_print!(" - Track: {}", status.track); + log_print!(" - Submitted at: block {}", status.submitted); + log_print!( + " - Tally: Ayes: {}, Nays: {}", + status.tally.ayes, + status.tally.nays + ); + log_verbose!(" - Full status: {:#?}", status); + }, + ReferendumInfo::Approved(submitted, ..) => { + log_print!(" - Status: {}", "Approved".green()); + log_print!(" - Submitted at block: {}", submitted); + }, + ReferendumInfo::Rejected(submitted, ..) => { + log_print!(" - Status: {}", "Rejected".red()); + log_print!(" - Submitted at block: {}", submitted); + }, + ReferendumInfo::Cancelled(submitted, ..) => { + log_print!(" - Status: {}", "Cancelled".yellow()); + log_print!(" - Submitted at block: {}", submitted); + }, + ReferendumInfo::TimedOut(submitted, ..) => { + log_print!(" - Status: {}", "TimedOut".dimmed()); + log_print!(" - Submitted at block: {}", submitted); + }, + ReferendumInfo::Killed(submitted) => { + log_print!(" - Status: {}", "Killed".red().bold()); + log_print!(" - Killed at block: {}", submitted); + }, + } + }, + Ok(None) => log_print!("📭 Referendum #{} not found", index), + Err(e) => log_error!("❌ Failed to fetch referendum #{}: {:?}", index, e), + } + + Ok(()) +} + +/// Place a decision deposit for a Tech Referendum +async fn place_decision_deposit( + quantus_client: &crate::chain::client::QuantusClient, + index: u32, + from: &str, + password: Option, + password_file: Option, + execution_mode: crate::cli::common::ExecutionMode, +) -> crate::error::Result<()> { + log_print!("📋 Placing decision deposit for Tech Referendum #{}", index); + log_print!(" 🔑 Placed by: {}", from.bright_yellow()); + + let keypair = crate::wallet::load_keypair_from_wallet(from, password, password_file)?; + + let deposit_call = quantus_subxt::api::tx().tech_referenda().place_decision_deposit(index); + let tx_hash = + submit_transaction(quantus_client, &keypair, deposit_call, None, execution_mode).await?; + log_success!("✅ Decision deposit placed! Hash: {:?}", tx_hash.to_string().bright_yellow()); + Ok(()) +} + +/// Cancel a Tech Referendum (sudo) +async fn cancel_proposal( + quantus_client: &crate::chain::client::QuantusClient, + index: u32, + from: &str, + password: Option, + password_file: Option, + execution_mode: crate::cli::common::ExecutionMode, +) -> crate::error::Result<()> { + log_print!("❌ Cancelling Tech Referendum #{}", index); + log_print!(" 🔑 Cancelled by: {}", from.bright_yellow()); + + let keypair = crate::wallet::load_keypair_from_wallet(from, password, password_file)?; + + let inner = + quantus_subxt::api::Call::TechReferenda(quantus_subxt::api::tech_referenda::Call::cancel { + index, + }); + let sudo_call = quantus_subxt::api::tx().sudo().sudo(inner); + + let tx_hash = + submit_transaction(quantus_client, &keypair, sudo_call, None, execution_mode).await?; + log_success!("✅ Referendum cancelled! Hash: {:?}", tx_hash.to_string().bright_yellow()); + Ok(()) +} + +/// Kill a Tech Referendum (sudo) +async fn kill_proposal( + quantus_client: &crate::chain::client::QuantusClient, + index: u32, + from: &str, + password: Option, + password_file: Option, + execution_mode: crate::cli::common::ExecutionMode, +) -> crate::error::Result<()> { + log_print!("💀 Killing Tech Referendum #{}", index); + log_print!(" 🔑 Killed by: {}", from.bright_yellow()); + + let keypair = crate::wallet::load_keypair_from_wallet(from, password, password_file)?; + + let inner = + quantus_subxt::api::Call::TechReferenda(quantus_subxt::api::tech_referenda::Call::kill { + index, + }); + let sudo_call = quantus_subxt::api::tx().sudo().sudo(inner); + + let tx_hash = + submit_transaction(quantus_client, &keypair, sudo_call, None, execution_mode).await?; + log_success!("✅ Referendum killed! Hash: {:?}", tx_hash.to_string().bright_yellow()); + Ok(()) +} + +/// Nudge a Tech Referendum to next phase (sudo) +async fn nudge_proposal( + quantus_client: &crate::chain::client::QuantusClient, + index: u32, + from: &str, + password: Option, + password_file: Option, + execution_mode: crate::cli::common::ExecutionMode, +) -> crate::error::Result<()> { + log_print!("🔄 Nudging Tech Referendum #{}", index); + log_print!(" 🔑 Nudged by: {}", from.bright_yellow()); + + let keypair = crate::wallet::load_keypair_from_wallet(from, password, password_file)?; + + let inner = quantus_subxt::api::Call::TechReferenda( + quantus_subxt::api::tech_referenda::Call::nudge_referendum { index }, + ); + let sudo_call = quantus_subxt::api::tx().sudo().sudo(inner); + + let tx_hash = + submit_transaction(quantus_client, &keypair, sudo_call, None, execution_mode).await?; + log_success!("✅ Referendum nudged! Hash: {:?}", tx_hash.to_string().bright_yellow()); + Ok(()) +} + +/// Get Tech Referenda configuration +async fn get_config( + quantus_client: &crate::chain::client::QuantusClient, +) -> crate::error::Result<()> { + log_print!("âš™ī¸ Tech Referenda Configuration"); + log_print!(""); + + let constants = quantus_client.client().constants(); + let tracks_addr = quantus_subxt::api::constants().tech_referenda().tracks(); + + match constants.at(&tracks_addr) { + Ok(tracks) => { + log_print!("{}", "📊 Track Configuration:".bold()); + for (id, info) in tracks.iter() { + log_print!(" ------------------------------------"); + log_print!( + " â€ĸ {} #{}: {}", + "Track".bold(), + id, + info.name.to_string().bright_cyan() + ); + log_print!(" â€ĸ Max Deciding: {}", info.max_deciding); + log_print!(" â€ĸ Decision Deposit: {}", info.decision_deposit); + log_print!(" â€ĸ Prepare Period: {} blocks", info.prepare_period); + log_print!(" â€ĸ Decision Period: {} blocks", info.decision_period); + log_print!(" â€ĸ Confirm Period: {} blocks", info.confirm_period); + log_print!(" â€ĸ Min Enactment Period: {} blocks", info.min_enactment_period); + } + log_print!(" ------------------------------------"); + }, + Err(e) => { + log_error!("❌ Failed to decode Tracks constant: {:?}", e); + log_print!("💡 It's possible the Tracks constant is not in the expected format."); + }, + } + + Ok(()) +} + +/// Refund submission deposit for a completed Tech Referendum +async fn refund_submission_deposit( + quantus_client: &crate::chain::client::QuantusClient, + index: u32, + from: &str, + password: Option, + password_file: Option, + execution_mode: crate::cli::common::ExecutionMode, +) -> crate::error::Result<()> { + log_print!("💰 Refunding submission deposit for Tech Referendum #{}", index); + log_print!(" 🔑 Refund to: {}", from.bright_yellow()); + + // Load wallet keypair + let keypair = crate::wallet::load_keypair_from_wallet(from, password, password_file)?; + + // Create refund_submission_deposit call for TechReferenda instance + let refund_call = quantus_subxt::api::tx().tech_referenda().refund_submission_deposit(index); + + let tx_hash = + submit_transaction(quantus_client, &keypair, refund_call, None, execution_mode).await?; + log_print!( + "✅ {} Refund transaction submitted! Hash: {:?}", + "SUCCESS".bright_green().bold(), + tx_hash + ); + + log_success!("🎉 {} Submission deposit refunded!", "FINISHED".bright_green().bold()); + log_print!("💡 Check your balance to confirm the refund"); + Ok(()) +} + +/// Refund decision deposit for a completed Tech Referendum +async fn refund_decision_deposit( + quantus_client: &crate::chain::client::QuantusClient, + index: u32, + from: &str, + password: Option, + password_file: Option, + execution_mode: crate::cli::common::ExecutionMode, +) -> crate::error::Result<()> { + log_print!("💰 Refunding decision deposit for Tech Referendum #{}", index); + log_print!(" 🔑 Refund to: {}", from.bright_yellow()); + + // Load wallet keypair + let keypair = crate::wallet::load_keypair_from_wallet(from, password, password_file)?; + + // Create refund_decision_deposit call for TechReferenda instance + let refund_call = quantus_subxt::api::tx().tech_referenda().refund_decision_deposit(index); + + let tx_hash = + submit_transaction(quantus_client, &keypair, refund_call, None, execution_mode).await?; + log_print!( + "✅ {} Refund transaction submitted! Hash: {:?}", + "SUCCESS".bright_green().bold(), + tx_hash + ); + + log_success!("🎉 {} Decision deposit refunded!", "FINISHED".bright_green().bold()); + log_print!("💡 Check your balance to confirm the refund"); + Ok(()) +} diff --git a/src/config/mod.rs b/src/config/mod.rs index c6d7ac8..a8f3074 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -3,7 +3,7 @@ //! This module handles runtime compatibility information. /// List of runtime spec versions that this CLI is compatible with -pub const COMPATIBLE_RUNTIME_VERSIONS: &[u32] = &[115, 116]; +pub const COMPATIBLE_RUNTIME_VERSIONS: &[u32] = &[118]; /// Check if a runtime version is compatible with this CLI pub fn is_runtime_compatible(spec_version: u32) -> bool { diff --git a/src/quantus_metadata.scale b/src/quantus_metadata.scale index a26ebd3d6596c6fd980bbc9313a32f6e0c8a4b30..d77a70b0b04abf18c938614aa8f726adf6c82203 100644 GIT binary patch delta 21 ccmccehx5uG&W0AoElf-tjAh%IIhX>=0A`2>dH?_b delta 21 ccmccehx5uG&W0AoElf-tjHTO|IhX>=0A_&)c>n+a