From b9de826e91f3c3cb792f3dc7da9397d3b81b6b9d Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Fri, 12 Jun 2026 18:39:19 +0800 Subject: [PATCH] better info on referenda --- src/chain/client.rs | 24 +++++-- src/cli/common.rs | 134 ++++++++++++++++++++++++++++++++++++++ src/cli/tech_referenda.rs | 27 +++++++- 3 files changed, 176 insertions(+), 9 deletions(-) diff --git a/src/chain/client.rs b/src/chain/client.rs index 00f73da..62c6525 100644 --- a/src/chain/client.rs +++ b/src/chain/client.rs @@ -170,21 +170,31 @@ impl QuantusClient { Ok(account_info.nonce as u64) } - /// Get genesis hash using RPC call - pub async fn get_genesis_hash(&self) -> crate::error::Result { - log_verbose!("🔍 Fetching genesis hash via RPC..."); - + /// Get the hash of a block by number using RPC call. `None` if the block doesn't exist. + pub async fn get_block_hash( + &self, + number: u32, + ) -> crate::error::Result> { use jsonrpsee::core::client::ClientT; - let genesis_hash: subxt::utils::H256 = self + let hash: Option = self .rpc_client - .request::("chain_getBlockHash", [0u32]) + .request::, [u32; 1]>("chain_getBlockHash", [number]) .await .map_err(|e| { crate::error::QuantusError::NetworkError(format!( - "Failed to fetch genesis hash: {e:?}" + "Failed to fetch hash of block {number}: {e:?}" )) })?; + log_verbose!("📦 Block {} hash: {:?}", number, hash); + Ok(hash) + } + /// Get genesis hash using RPC call + pub async fn get_genesis_hash(&self) -> crate::error::Result { + log_verbose!("🔍 Fetching genesis hash via RPC..."); + let genesis_hash = self.get_block_hash(0).await?.ok_or_else(|| { + crate::error::QuantusError::NetworkError("Genesis block hash not found".to_string()) + })?; log_verbose!("🧬 Genesis hash: {:?}", genesis_hash); Ok(genesis_hash) } diff --git a/src/cli/common.rs b/src/cli/common.rs index 3f34ce9..91199d1 100644 --- a/src/cli/common.rs +++ b/src/cli/common.rs @@ -768,6 +768,140 @@ fn format_dispatch_error( } } +/// Outcome of an approved referendum's scheduled enactment call. +pub enum EnactmentStatus { + /// Still in the scheduler agenda, will dispatch at the given block. + Scheduled(u32), + /// Dispatched by the scheduler; `Err` carries the dispatch error description. + Executed { block: u32, result: std::result::Result<(), String> }, + /// Dropped: the preimage was unavailable at enactment time (never executed). + PreimageMissing(u32), + /// Dropped: call weight exceeded the scheduler's maximum (never executed). + Overweight(u32), + /// Could not be determined (e.g. node pruned the enactment block's state). + Unknown, +} + +/// Resolve whether the enactment call of an approved referendum was actually executed. +/// +/// `pallet_referenda` only records `Approved`; the scheduled `set_code`/call dispatch happens +/// later via `pallet_scheduler` under a named task id of `blake2_256(("assembly", "enactment", +/// index))`. We check the scheduler agenda first, then scan events in the enactment window. +pub async fn resolve_enactment_status( + quantus_client: &crate::chain::client::QuantusClient, + referendum_index: u32, + approved_at: u32, + min_enactment_period: u32, +) -> Result { + use crate::chain::quantus_subxt::api::{ + self, runtime_types::qp_scheduler::BlockNumberOrTimestamp, scheduler::events, + }; + use codec::Encode; + + let task_id = + sp_core::hashing::blake2_256(&(*b"assembly", "enactment", referendum_index).encode()); + + // Still scheduled for the future? + let latest_block_hash = quantus_client.get_latest_block().await?; + let lookup_addr = api::storage().scheduler().lookup(task_id); + if let Some((when, _agenda_index)) = + quantus_client.client().storage().at(latest_block_hash).fetch(&lookup_addr).await? + { + if let BlockNumberOrTimestamp::BlockNumber(n) = when { + return Ok(EnactmentStatus::Scheduled(n)); + } + return Ok(EnactmentStatus::Unknown); + } + + // Not scheduled anymore: the dispatch happened (or was dropped) no earlier than + // `approved_at + min_enactment_period`. Scan a small window to cover postponements. + let start = approved_at.saturating_add(min_enactment_period); + for block_number in start..start.saturating_add(12) { + let Some(block_hash) = quantus_client.get_block_hash(block_number).await? else { + return Ok(EnactmentStatus::Unknown); + }; + let block = match quantus_client.client().blocks().at(block_hash).await { + Ok(block) => block, + Err(_) => return Ok(EnactmentStatus::Unknown), + }; + let block_events = block.events().await.map_err(|e| { + crate::error::QuantusError::NetworkError(format!( + "Failed to fetch events of block {block_number}: {e:?}" + )) + })?; + + for event in block_events.find::() { + let event = event.map_err(|e| { + crate::error::QuantusError::NetworkError(format!( + "Failed to decode Scheduler::Dispatched event: {e:?}" + )) + })?; + if event.id == Some(task_id) { + let metadata = quantus_client.client().metadata(); + let result = event + .result + .map_err(|dispatch_error| format_dispatch_error(&dispatch_error, &metadata)); + return Ok(EnactmentStatus::Executed { block: block_number, result }); + } + } + for event in block_events.find::() { + if event.ok().and_then(|e| e.id) == Some(task_id) { + return Ok(EnactmentStatus::PreimageMissing(block_number)); + } + } + for event in block_events.find::() { + if event.ok().and_then(|e| e.id) == Some(task_id) { + return Ok(EnactmentStatus::Overweight(block_number)); + } + } + } + + Ok(EnactmentStatus::Unknown) +} + +/// Print a human-readable enactment line for an approved referendum. +pub fn print_enactment_status(status: &EnactmentStatus) { + match status { + EnactmentStatus::Scheduled(block) => { + crate::log_print!(" - Enactment: ⏳ scheduled at block {}", block); + }, + EnactmentStatus::Executed { block, result: Ok(()) } => { + crate::log_print!( + " - Enactment: {} at block {}", + "✅ executed successfully".bright_green(), + block + ); + }, + EnactmentStatus::Executed { block, result: Err(error) } => { + crate::log_print!( + " - Enactment: {} at block {}: {}", + "❌ execution FAILED".bright_red().bold(), + block, + error + ); + }, + EnactmentStatus::PreimageMissing(block) => { + crate::log_print!( + " - Enactment: {} - preimage unavailable at block {} (call never executed)", + "❌ DROPPED".bright_red().bold(), + block + ); + }, + EnactmentStatus::Overweight(block) => { + crate::log_print!( + " - Enactment: {} - permanently overweight at block {} (call never executed)", + "❌ DROPPED".bright_red().bold(), + block + ); + }, + EnactmentStatus::Unknown => { + crate::log_print!( + " - Enactment: ❓ unknown (enactment block not found or state pruned)" + ); + }, + } +} + /// Submit a preimage, treating AlreadyNoted as success (idempotent). /// Always waits for inclusion so subsequent txs from the same sender get a fresh nonce. pub async fn submit_preimage( diff --git a/src/cli/tech_referenda.rs b/src/cli/tech_referenda.rs index d3e172b..537004f 100644 --- a/src/cli/tech_referenda.rs +++ b/src/cli/tech_referenda.rs @@ -554,6 +554,20 @@ async fn get_proposal_details( Ok(()) } +/// Min enactment period (blocks) of the tech track, from the runtime's Tracks constant. +fn min_enactment_period( + quantus_client: &crate::chain::client::QuantusClient, +) -> crate::error::Result { + let tracks_addr = quantus_subxt::api::constants().tech_referenda().tracks(); + let tracks = quantus_client.client().constants().at(&tracks_addr).map_err(|e| { + QuantusError::NetworkError(format!("Failed to decode Tracks constant: {e:?}")) + })?; + tracks + .first() + .map(|(_, info)| info.min_enactment_period) + .ok_or_else(|| QuantusError::NetworkError("No tracks configured".to_string())) +} + /// Get the status of a Tech Referendum async fn get_proposal_status( quantus_client: &crate::chain::client::QuantusClient, @@ -586,9 +600,18 @@ async fn get_proposal_status( ); log_verbose!(" - Full status: {:#?}", status); }, - ReferendumInfo::Approved(submitted, ..) => { + ReferendumInfo::Approved(since, ..) => { log_print!(" - Status: {}", "Approved".green()); - log_print!(" - Submitted at block: {}", submitted); + log_print!(" - Approved at block: {}", since); + let period = min_enactment_period(quantus_client)?; + let enactment = crate::cli::common::resolve_enactment_status( + quantus_client, + index, + since, + period, + ) + .await?; + crate::cli::common::print_enactment_status(&enactment); }, ReferendumInfo::Rejected(submitted, ..) => { log_print!(" - Status: {}", "Rejected".red());