diff --git a/src/cli/tech_referenda.rs b/src/cli/tech_referenda.rs index a2f7d1b..0a85a2a 100644 --- a/src/cli/tech_referenda.rs +++ b/src/cli/tech_referenda.rs @@ -604,7 +604,14 @@ fn format_blocks_duration(blocks: u64, block_time_ms: u64) -> String { fn format_block_eta(target_block: u32, current_block: u32, block_time_ms: u64) -> String { if target_block <= current_block { let ago = (current_block - target_block) as u64; - format!("block {} (~{} ago)", target_block, format_blocks_duration(ago, block_time_ms)) + let at = chrono::Utc::now() - + chrono::Duration::milliseconds(ago.saturating_mul(block_time_ms) as i64); + format!( + "block {} (~{} ago, ≈ {})", + target_block, + format_blocks_duration(ago, block_time_ms), + at.format("%Y-%m-%d %H:%M UTC") + ) } else { let delta = (target_block - current_block) as u64; let eta = chrono::Utc::now() + @@ -619,6 +626,40 @@ fn format_block_eta(target_block: u32, current_block: u32, block_time_ms: u64) - } } +/// Scheduler task id for a referendum's enactment call (matches pallet-referenda). +fn enactment_task_name(index: u32) -> [u8; 32] { + use codec::Encode; + const ASSEMBLY_ID: [u8; 8] = *b"assembly"; + sp_core::hashing::blake2_256(&(ASSEMBLY_ID, "enactment", index).encode()) +} + +/// Resolve the scheduled enactment block from Scheduler::Lookup, if still pending. +async fn scheduled_enactment_block( + quantus_client: &crate::chain::client::QuantusClient, + index: u32, + at_block: subxt::utils::H256, +) -> crate::error::Result> { + use quantus_subxt::api::runtime_types::qp_scheduler::BlockNumberOrTimestamp; + + let addr = quantus_subxt::api::storage().scheduler().lookup(enactment_task_name(index)); + let storage_at = quantus_client.client().storage().at(at_block); + let Some((when, _)) = storage_at.fetch(&addr).await.map_err(|e| { + QuantusError::NetworkError(format!( + "Failed to fetch Scheduler::Lookup for referendum #{index}: {e:?}" + )) + })? + else { + return Ok(None); + }; + + match when { + BlockNumberOrTimestamp::BlockNumber(block) => Ok(Some(block)), + BlockNumberOrTimestamp::Timestamp(_) => Err(QuantusError::Generic(format!( + "Referendum #{index} enactment is scheduled by timestamp, not block number" + ))), + } +} + /// Compute the enactment block for a given approval block, honoring the track's minimum delay fn enactment_block( enactment: &quantus_subxt::api::runtime_types::frame_support::traits::schedule::DispatchTime< @@ -901,11 +942,25 @@ async fn get_proposal_status( log_verbose!(" - Full status: {:#?}", status); }, ReferendumInfo::Approved(since, ..) => { + let current_block = + quantus_client.client().blocks().at(latest_block_hash).await?.number(); + let block_time_ms = target_block_time_ms(quantus_client)?; log_print!(" - Status: {}", "Approved".green()); - log_print!(" - Approved at block: {}", since); log_print!( - " 💡 Enactment is scheduled; check the scheduler for the exact block" + " - Approved at: {}", + format_block_eta(since, current_block, block_time_ms) ); + match scheduled_enactment_block(quantus_client, index, latest_block_hash) + .await? + { + Some(when) => log_print!( + " 🏁 Enactment: {}", + format_block_eta(when, current_block, block_time_ms) + ), + None => log_print!( + " 🏁 Enactment: no longer scheduled; execution outcome unavailable" + ), + } }, ReferendumInfo::Rejected(since, ..) => { log_print!(" - Status: {}", "Rejected".red()); @@ -1065,7 +1120,8 @@ async fn refund_decision_deposit( #[cfg(test)] mod tests { - use super::{pre_deciding_state, PreDecidingState}; + use super::{enactment_task_name, format_block_eta, pre_deciding_state, PreDecidingState}; + use codec::Encode; #[test] fn missing_deposit_blocks_pre_deciding_enactment_estimate() { @@ -1088,4 +1144,25 @@ mod tests { "until a deciding slot opens and Deciding starts" ); } + + #[test] + fn enactment_task_name_matches_pallet_referenda_formula() { + const ASSEMBLY_ID: [u8; 8] = *b"assembly"; + let index = 4u32; + let expected = sp_core::hashing::blake2_256(&(ASSEMBLY_ID, "enactment", index).encode()); + assert_eq!(enactment_task_name(index), expected); + } + + #[test] + fn format_block_eta_includes_absolute_time_for_past_and_future() { + let past = format_block_eta(100, 200, 6_000); + assert!(past.contains("block 100"), "{past}"); + assert!(past.contains("ago"), "{past}"); + assert!(past.contains("UTC"), "{past}"); + + let future = format_block_eta(300, 200, 6_000); + assert!(future.contains("block 300"), "{future}"); + assert!(future.contains("in 100 blocks"), "{future}"); + assert!(future.contains("UTC"), "{future}"); + } }