use crate::{
chain::quantus_subxt,
cli::{common::submit_transaction, runtime::UpgradeTrack},
error::QuantusError,
log_error, log_print, log_success, log_verbose,
};
use clap::Subcommand;
use colored::Colorize;
use std::{
path::{Path, PathBuf},
str::FromStr,
};
#[derive(Subcommand, Debug)]
pub enum TechReferendaCommands {
#[command(arg_required_else_help = true)]
Submit {
#[arg(long, value_name = "HASH")]
preimage_hash: String,
#[arg(long, value_enum, default_value_t)]
track: UpgradeTrack,
#[arg(short, long, value_name = "WALLET")]
from: String,
#[arg(short, long, hide = true)]
password: Option<String>,
#[arg(long)]
password_file: Option<String>,
},
#[command(arg_required_else_help = true)]
SubmitWithPreimage {
#[arg(short, long, value_name = "PATH")]
wasm_file: PathBuf,
#[arg(long, value_enum, default_value_t)]
track: UpgradeTrack,
#[arg(short, long, value_name = "WALLET")]
from: String,
#[arg(short, long, hide = true)]
password: Option<String>,
#[arg(long)]
password_file: Option<String>,
},
List,
#[command(arg_required_else_help = true)]
Get {
#[arg(short, long, value_name = "REFERENDUM_INDEX")]
index: u32,
},
#[command(arg_required_else_help = true)]
Status {
#[arg(short, long, value_name = "REFERENDUM_INDEX")]
index: u32,
},
#[command(
arg_required_else_help = true,
after_help = "Example:\n quantus tech-referenda place-decision-deposit --index 0 --from alice"
)]
PlaceDecisionDeposit {
#[arg(short, long, value_name = "REFERENDUM_INDEX")]
index: u32,
#[arg(short, long, value_name = "WALLET")]
from: String,
#[arg(short, long, hide = true)]
password: Option<String>,
#[arg(long)]
password_file: Option<String>,
},
#[command(arg_required_else_help = true)]
RefundSubmissionDeposit {
#[arg(short, long, value_name = "REFERENDUM_INDEX")]
index: u32,
#[arg(short, long, value_name = "WALLET")]
from: String,
#[arg(short, long, hide = true)]
password: Option<String>,
#[arg(long)]
password_file: Option<String>,
},
#[command(arg_required_else_help = true)]
RefundDecisionDeposit {
#[arg(short, long, value_name = "REFERENDUM_INDEX")]
index: u32,
#[arg(short, long, value_name = "WALLET")]
from: String,
#[arg(short, long, hide = true)]
password: Option<String>,
#[arg(long)]
password_file: Option<String>,
},
Config,
}
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, track, from, password, password_file } =>
submit_runtime_upgrade(
&quantus_client,
&preimage_hash,
track,
&from,
password,
password_file,
execution_mode,
)
.await,
TechReferendaCommands::SubmitWithPreimage {
wasm_file,
track,
from,
password,
password_file,
} =>
submit_runtime_upgrade_with_preimage(
&quantus_client,
&wasm_file,
track,
&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::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,
}
}
async fn submit_runtime_upgrade(
quantus_client: &crate::chain::client::QuantusClient,
preimage_hash: &str,
track: UpgradeTrack,
from: &str,
password: Option<String>,
password_file: Option<String>,
execution_mode: crate::cli::common::ExecutionMode,
) -> crate::error::Result<()> {
log_print!("📝 Submitting Authorization Referendum on {}", track.label());
log_print!(" 🔗 Preimage hash: {}", preimage_hash.bright_cyan());
log_print!(" 🔑 Submitted by: {}", from.bright_yellow());
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()))?;
let signer = crate::wallet::load_signer_from_wallet(from, password, password_file)?;
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 = crate::cli::preimage::fetch_request_status(
quantus_client,
preimage_hash_parsed,
latest_block_hash,
)
.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 = preimage_status
.len()
.ok_or_else(|| QuantusError::Generic("Preimage length not available".to_string()))?;
let preimage = storage_at
.fetch(
&quantus_subxt::api::storage()
.preimage()
.preimage_for((preimage_hash_parsed, preimage_len)),
)
.await
.map_err(|e| QuantusError::Generic(format!("Failed to fetch preimage: {e:?}")))?
.ok_or_else(|| QuantusError::Generic("Preimage content not found on chain".to_string()))?;
let code_hash = crate::cli::runtime::validate_runtime_authorization_preimage(
&quantus_client.client().metadata(),
&preimage.0,
)?;
log_print!("✅ Authorization preimage found for runtime hash {:?}", code_hash);
let submit_call = crate::cli::runtime::build_authorization_referendum(
preimage_hash_parsed,
preimage_len,
track,
);
let tx_hash =
submit_transaction(quantus_client, &signer, submit_call, None, execution_mode).await?;
log_print!(
"✅ {} Runtime authorization referendum submitted! Hash: {:?}",
"SUCCESS".bright_green().bold(),
tx_hash
);
log_print!("💡 Use 'quantus tech-referenda list' to see active proposals");
Ok(())
}
async fn submit_runtime_upgrade_with_preimage(
quantus_client: &crate::chain::client::QuantusClient,
wasm_file: &Path,
track: UpgradeTrack,
from: &str,
password: Option<String>,
password_file: Option<String>,
execution_mode: crate::cli::common::ExecutionMode,
) -> crate::error::Result<()> {
log_print!("📝 Submitting Authorization Referendum on {}", track.label());
log_print!(" 📂 WASM file: {}", wasm_file.display().to_string().bright_cyan());
log_print!(" 🔑 Submitted by: {}", from.bright_yellow());
let wasm_code = crate::cli::runtime::read_wasm_file(wasm_file)?;
log_print!("📊 WASM file size: {} bytes", wasm_code.len());
let signer = crate::wallet::load_signer_from_wallet(from, password, password_file)?;
crate::cli::runtime::submit_runtime_authorization(
quantus_client,
&wasm_code,
&signer,
track,
execution_mode,
)
.await?;
log_print!("💡 Use 'quantus tech-referenda list' to see active proposals");
Ok(())
}
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();
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(())
}
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 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(&referendum_info_address(index)).await?;
if let Some(thunk) = info {
let value = thunk.to_value().map_err(|e| {
QuantusError::Generic(format!("Failed to decode referendum #{index}: {e:?}"))
})?;
log_print!("📋 Referendum Information (raw):");
log_print!("{value}");
} else {
log_print!("📭 Referendum #{} not found", index);
}
Ok(())
}
type TrackDetails =
quantus_subxt::api::runtime_types::pallet_referenda::types::TrackDetails<u128, u32, String>;
fn fetch_tracks(
quantus_client: &crate::chain::client::QuantusClient,
) -> crate::error::Result<Vec<(u16, TrackDetails)>> {
quantus_client
.client()
.constants()
.at(&quantus_subxt::api::constants().tech_referenda().tracks())
.map_err(|e| QuantusError::Generic(format!("Failed to decode Tracks constant: {e:?}")))
}
fn target_block_time_ms(
quantus_client: &crate::chain::client::QuantusClient,
) -> crate::error::Result<u64> {
quantus_client
.client()
.constants()
.at(&quantus_subxt::api::constants().q_po_w().target_block_time())
.map_err(|e| {
QuantusError::Generic(format!("Failed to read TargetBlockTime constant: {e:?}"))
})
}
fn track_display_name(name: &str) -> &str {
name.trim_matches(|c: char| c == '\0' || c.is_whitespace())
}
fn format_blocks_duration(blocks: u64, block_time_ms: u64) -> String {
let secs = blocks.saturating_mul(block_time_ms) / 1000;
if secs < 60 {
format!("{}s", secs)
} else if secs < 3600 {
format!("{:.0}min", secs as f64 / 60.0)
} else if secs < 172_800 {
format!("{:.1}h", secs as f64 / 3600.0)
} else {
format!("{:.1} days", secs as f64 / 86400.0)
}
}
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;
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() +
chrono::Duration::milliseconds(delta.saturating_mul(block_time_ms) as i64);
format!(
"block {} (in {} blocks / ~{}, ≈ {})",
target_block,
delta,
format_blocks_duration(delta, block_time_ms),
eta.format("%Y-%m-%d %H:%M UTC")
)
}
}
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())
}
async fn scheduled_enactment_block(
quantus_client: &crate::chain::client::QuantusClient,
index: u32,
at_block: subxt::utils::H256,
) -> crate::error::Result<Option<u32>> {
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"
))),
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum EnactmentMoment {
At(u32),
After(u32),
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct DecidingSnapshot {
since: u32,
confirming: Option<u32>,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct TallySnapshot {
pub(crate) ayes: u32,
pub(crate) nays: u32,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct OngoingSnapshot {
pub(crate) track: u16,
pub(crate) submitted: u32,
pub(crate) tally: TallySnapshot,
decision_deposit_placed: bool,
in_queue: bool,
deciding: Option<DecidingSnapshot>,
enactment: EnactmentMoment,
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum ReferendumSnapshot {
Ongoing(OngoingSnapshot),
Approved(u32),
Rejected(u32),
Cancelled(u32),
TimedOut(u32),
Killed(u32),
}
mod referendum_value {
use super::{
DecidingSnapshot, EnactmentMoment, OngoingSnapshot, ReferendumSnapshot, TallySnapshot,
};
use crate::{
cli::dynamic_decode::{
composite, field, missing, nth, option, u32_field, uint, variant, Val,
},
error::QuantusError,
};
pub fn decode(value: &Val) -> Result<ReferendumSnapshot, QuantusError> {
let (name, body) = variant(value).ok_or_else(|| missing("ReferendumInfo"))?;
let since = || -> Result<u32, QuantusError> {
let raw = nth(body, 0).and_then(uint).ok_or_else(|| missing("since"))?;
u32::try_from(raw).map_err(|_| missing("since"))
};
match name {
"Ongoing" => {
let status =
nth(body, 0).and_then(composite).ok_or_else(|| missing("ReferendumStatus"))?;
let track = u32_field(status, "track")?;
let track = u16::try_from(track).map_err(|_| missing("track"))?;
let tally =
field(status, "tally").and_then(composite).ok_or_else(|| missing("tally"))?;
let deciding = match field(status, "deciding").and_then(option) {
Some(Some(d)) => {
let d = composite(d).ok_or_else(|| missing("deciding"))?;
let confirming = match field(d, "confirming").and_then(option) {
Some(Some(c)) => Some(
u32::try_from(uint(c).ok_or_else(|| missing("confirming"))?)
.map_err(|_| missing("confirming"))?,
),
Some(None) => None,
None => return Err(missing("confirming")),
};
Some(DecidingSnapshot { since: u32_field(d, "since")?, confirming })
},
Some(None) => None,
None => return Err(missing("deciding")),
};
let enactment = {
let (kind, args) = field(status, "enactment")
.and_then(variant)
.ok_or_else(|| missing("enactment"))?;
let n = nth(args, 0).and_then(uint).ok_or_else(|| missing("enactment"))?;
let n = u32::try_from(n).map_err(|_| missing("enactment"))?;
match kind {
"At" => EnactmentMoment::At(n),
"After" => EnactmentMoment::After(n),
_ => return Err(missing("enactment")),
}
};
Ok(ReferendumSnapshot::Ongoing(OngoingSnapshot {
track,
submitted: u32_field(status, "submitted")?,
tally: TallySnapshot {
ayes: u32_field(tally, "ayes")?,
nays: u32_field(tally, "nays")?,
},
decision_deposit_placed: matches!(
field(status, "decision_deposit").and_then(option),
Some(Some(_))
),
in_queue: field(status, "in_queue")
.and_then(crate::cli::dynamic_decode::boolean)
.unwrap_or(false),
deciding,
enactment,
}))
},
"Approved" => Ok(ReferendumSnapshot::Approved(since()?)),
"Rejected" => Ok(ReferendumSnapshot::Rejected(since()?)),
"Cancelled" => Ok(ReferendumSnapshot::Cancelled(since()?)),
"TimedOut" => Ok(ReferendumSnapshot::TimedOut(since()?)),
"Killed" => Ok(ReferendumSnapshot::Killed(since()?)),
other => Err(QuantusError::Generic(format!(
"referendum decode: unknown ReferendumInfo variant `{other}`"
))),
}
}
}
fn referendum_info_address(
index: u32,
) -> subxt::storage::DynamicAddress<Vec<subxt::dynamic::Value>> {
subxt::dynamic::storage(
"TechReferenda",
"ReferendumInfoFor",
vec![subxt::dynamic::Value::u128(u128::from(index))],
)
}
pub(crate) async fn fetch_referendum(
quantus_client: &crate::chain::client::QuantusClient,
index: u32,
block_hash: subxt::utils::H256,
) -> crate::error::Result<Option<ReferendumSnapshot>> {
let storage_at = quantus_client.client().storage().at(block_hash);
let Some(thunk) = storage_at.fetch(&referendum_info_address(index)).await? else {
return Ok(None);
};
let value = thunk.to_value().map_err(|e| {
QuantusError::Generic(format!("Failed to decode referendum #{index}: {e:?}"))
})?;
referendum_value::decode(&value).map(Some)
}
fn enactment_block(
enactment: &EnactmentMoment,
approval_block: u32,
min_enactment_period: u32,
) -> u32 {
let desired = match enactment {
EnactmentMoment::At(block) => *block,
EnactmentMoment::After(offset) => approval_block.saturating_add(*offset),
};
desired.max(approval_block.saturating_add(min_enactment_period))
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum PreDecidingState {
Preparing { deposit_placed: bool },
WaitingForDecisionDeposit,
WaitingForTrackSlot,
Ready,
}
impl PreDecidingState {
fn enactment_estimate_reason(self) -> &'static str {
match self {
Self::Preparing { deposit_placed: false } =>
"until the prepare period ends, the decision deposit is placed, and Deciding starts",
Self::Preparing { deposit_placed: true } =>
"until the prepare period ends and Deciding starts",
Self::WaitingForDecisionDeposit =>
"until the decision deposit is placed and Deciding starts",
Self::WaitingForTrackSlot => "until a deciding slot opens and Deciding starts",
Self::Ready => "until Deciding starts",
}
}
}
fn pre_deciding_state(
prepare_end: u32,
current_block: u32,
deposit_placed: bool,
in_queue: bool,
) -> PreDecidingState {
if in_queue {
PreDecidingState::WaitingForTrackSlot
} else if current_block < prepare_end {
PreDecidingState::Preparing { deposit_placed }
} else if !deposit_placed {
PreDecidingState::WaitingForDecisionDeposit
} else {
PreDecidingState::Ready
}
}
async fn get_proposal_status(
quantus_client: &crate::chain::client::QuantusClient,
index: u32,
) -> crate::error::Result<()> {
log_verbose!("📊 Fetching status for Tech Referendum #{}...", index);
let latest_block_hash = quantus_client.get_latest_block().await?;
let info_res = fetch_referendum(quantus_client, index, latest_block_hash).await;
match info_res {
Ok(Some(info)) => {
log_print!("📊 Status for Referendum #{}", index.to_string().bright_yellow());
match info {
ReferendumSnapshot::Ongoing(status) => {
let current_block =
quantus_client.client().blocks().at(latest_block_hash).await?.number();
let block_time_ms = target_block_time_ms(quantus_client)?;
let tracks = fetch_tracks(quantus_client)?;
let (_, track) =
tracks.iter().find(|(id, _)| *id == status.track).ok_or_else(|| {
QuantusError::Generic(format!(
"Track {} not found in Tracks constant",
status.track
))
})?;
log_print!(" - Status: {}", "Ongoing".bright_green());
log_print!(
" - Track: {} ({})",
status.track,
track_display_name(&track.name).bright_cyan()
);
log_print!(" - Current block: {}", current_block);
log_print!(
" - Submitted at: {}",
format_block_eta(status.submitted, current_block, block_time_ms)
);
log_print!(
" - Tally: Ayes: {}, Nays: {}",
status.tally.ayes,
status.tally.nays
);
log_print!(
" - Decision deposit: {}",
if status.decision_deposit_placed {
"placed".green()
} else {
"not placed".yellow()
}
);
log_print!("");
log_print!(
" ⏱️ Track phase durations (target block time {}s):",
block_time_ms / 1000
);
log_print!(
" - Prepare: {} blocks (~{})",
track.prepare_period,
format_blocks_duration(track.prepare_period as u64, block_time_ms)
);
log_print!(
" - Decide: up to {} blocks (~{})",
track.decision_period,
format_blocks_duration(track.decision_period as u64, block_time_ms)
);
log_print!(
" - Confirm: {} blocks (~{})",
track.confirm_period,
format_blocks_duration(track.confirm_period as u64, block_time_ms)
);
log_print!(
" - Enactment delay after approval: ≥ {} blocks (~{})",
track.min_enactment_period,
format_blocks_duration(track.min_enactment_period as u64, block_time_ms)
);
log_print!("");
let prepare_end = status.submitted.saturating_add(track.prepare_period);
match &status.deciding {
None => {
let pre_deciding = pre_deciding_state(
prepare_end,
current_block,
status.decision_deposit_placed,
status.in_queue,
);
log_print!(
" 🧭 Phase: {} (1 of 3: Preparing → Deciding → Confirming)",
"Preparing".bright_yellow()
);
log_print!(
" - Prepare period ends at {}",
format_block_eta(prepare_end, current_block, block_time_ms)
);
log_print!("");
match pre_deciding {
PreDecidingState::Preparing { deposit_placed } => log_print!(
" ⏭️ Next phase: Deciding — earliest at {}{}",
format_block_eta(prepare_end, current_block, block_time_ms),
if deposit_placed {
""
} else {
" (requires the decision deposit to be placed)"
}
),
PreDecidingState::WaitingForDecisionDeposit => {
log_print!(
" ⏭️ Next phase: Deciding — prepare period complete, starts once the decision deposit is placed"
);
log_print!(
" 💡 Run: quantus tech-referenda place-decision-deposit --index {} --from <wallet>",
index
);
},
PreDecidingState::WaitingForTrackSlot => log_print!(
" ⏭️ Next phase: Deciding — waiting for a free deciding slot on this track"
),
PreDecidingState::Ready => log_print!(
" ⏭️ Next phase: Deciding — prepare period complete, waiting for the chain transition"
),
}
log_print!(
" 🏁 Enactment estimate: unavailable {}",
pre_deciding.enactment_estimate_reason()
);
if status.decision_deposit_placed && !status.in_queue {
let confirm_end = prepare_end.saturating_add(track.confirm_period);
let enacts = enactment_block(
&status.enactment,
confirm_end,
track.min_enactment_period,
);
log_print!(
" ⤷ projected, if the current tally still clears the \
threshold when Deciding opens:"
);
log_print!(
" - Confirm ends at {}",
format_block_eta(confirm_end, current_block, block_time_ms)
);
log_print!(
" - Enactment at {}",
format_block_eta(enacts, current_block, block_time_ms)
);
}
},
Some(deciding) => match deciding.confirming {
None => {
let decision_end =
deciding.since.saturating_add(track.decision_period);
log_print!(
" 🧭 Phase: {} (2 of 3: Preparing → Deciding → Confirming)",
"Deciding".bright_yellow()
);
log_print!(
" - Started at {}",
format_block_eta(deciding.since, current_block, block_time_ms)
);
log_print!(
" - Decision deadline: {} — must enter Confirming by then or it is rejected",
format_block_eta(decision_end, current_block, block_time_ms)
);
log_print!("");
log_print!(
" ⏭️ Next phase: Confirming — starts as soon as approval & support thresholds pass; lasts {} blocks (~{})",
track.confirm_period,
format_blocks_duration(
track.confirm_period as u64,
block_time_ms
)
);
let earliest_approval =
current_block.saturating_add(track.confirm_period);
let latest_approval =
decision_end.saturating_add(track.confirm_period);
log_print!(
" 🏁 Earliest enactment: {} (confirmation starts now)",
format_block_eta(
enactment_block(
&status.enactment,
earliest_approval,
track.min_enactment_period
),
current_block,
block_time_ms
)
);
log_print!(
" 🏁 Latest enactment: {} (confirmation only starts at the decision deadline)",
format_block_eta(
enactment_block(
&status.enactment,
latest_approval,
track.min_enactment_period
),
current_block,
block_time_ms
)
);
},
Some(confirm_end) => {
log_print!(
" 🧭 Phase: {} (3 of 3: Preparing → Deciding → Confirming)",
"Confirming".bright_green()
);
log_print!(
" - Confirmation started at {}",
format_block_eta(
confirm_end.saturating_sub(track.confirm_period),
current_block,
block_time_ms
)
);
log_print!(
" - Approval at {} (if support holds; otherwise it falls back to Deciding)",
format_block_eta(confirm_end, current_block, block_time_ms)
);
log_print!("");
log_print!(" ⏭️ Next phase: Approved → enactment is scheduled");
log_print!(
" 🏁 Enactment: {}",
format_block_eta(
enactment_block(
&status.enactment,
confirm_end,
track.min_enactment_period
),
current_block,
block_time_ms
)
);
},
},
}
log_verbose!(" - Full status: {:#?}", status);
},
ReferendumSnapshot::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: {}",
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"
),
}
},
ReferendumSnapshot::Rejected(since) => {
log_print!(" - Status: {}", "Rejected".red());
log_print!(" - Rejected at block: {}", since);
},
ReferendumSnapshot::Cancelled(since) => {
log_print!(" - Status: {}", "Cancelled".yellow());
log_print!(" - Cancelled at block: {}", since);
},
ReferendumSnapshot::TimedOut(since) => {
log_print!(" - Status: {}", "TimedOut".dimmed());
log_print!(" - Timed out at block: {}", since);
},
ReferendumSnapshot::Killed(since) => {
log_print!(" - Status: {}", "Killed".red().bold());
log_print!(" - Killed at block: {}", since);
},
}
},
Ok(None) => log_print!("📭 Referendum #{} not found", index),
Err(e) => log_error!("❌ Failed to fetch referendum #{}: {:?}", index, e),
}
Ok(())
}
async fn place_decision_deposit(
quantus_client: &crate::chain::client::QuantusClient,
index: u32,
from: &str,
password: Option<String>,
password_file: Option<String>,
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 signer = crate::wallet::load_signer_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, &signer, deposit_call, None, execution_mode).await?;
log_success!("✅ Decision deposit placed! Hash: {:?}", tx_hash.to_string().bright_yellow());
Ok(())
}
async fn get_config(
quantus_client: &crate::chain::client::QuantusClient,
) -> crate::error::Result<()> {
log_print!("⚙️ Tech Referenda Configuration");
log_print!("");
let tracks = fetch_tracks(quantus_client)?;
let block_time_ms = target_block_time_ms(quantus_client)?;
log_print!("{}", "📊 Track Configuration:".bold());
log_print!(" Target block time: {}s", block_time_ms / 1000);
for (id, info) in tracks.iter() {
log_print!(" ------------------------------------");
log_print!(
" • {} #{}: {}",
"Track".bold(),
id,
track_display_name(&info.name).bright_cyan()
);
log_print!(" • Max Deciding: {}", info.max_deciding);
log_print!(" • Decision Deposit: {}", info.decision_deposit);
log_print!(
" • Prepare Period: {} blocks (~{})",
info.prepare_period,
format_blocks_duration(info.prepare_period as u64, block_time_ms)
);
log_print!(
" • Decision Period: {} blocks (~{})",
info.decision_period,
format_blocks_duration(info.decision_period as u64, block_time_ms)
);
log_print!(
" • Confirm Period: {} blocks (~{})",
info.confirm_period,
format_blocks_duration(info.confirm_period as u64, block_time_ms)
);
log_print!(
" • Min Enactment Period: {} blocks (~{})",
info.min_enactment_period,
format_blocks_duration(info.min_enactment_period as u64, block_time_ms)
);
}
log_print!(" ------------------------------------");
Ok(())
}
async fn refund_submission_deposit(
quantus_client: &crate::chain::client::QuantusClient,
index: u32,
from: &str,
password: Option<String>,
password_file: Option<String>,
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());
let signer = crate::wallet::load_signer_from_wallet(from, password, password_file)?;
let refund_call = quantus_subxt::api::tx().tech_referenda().refund_submission_deposit(index);
let tx_hash =
submit_transaction(quantus_client, &signer, 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(())
}
async fn refund_decision_deposit(
quantus_client: &crate::chain::client::QuantusClient,
index: u32,
from: &str,
password: Option<String>,
password_file: Option<String>,
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());
let signer = crate::wallet::load_signer_from_wallet(from, password, password_file)?;
let refund_call = quantus_subxt::api::tx().tech_referenda().refund_decision_deposit(index);
let tx_hash =
submit_transaction(quantus_client, &signer, 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(())
}
#[cfg(test)]
mod tests {
use super::{enactment_task_name, format_block_eta, pre_deciding_state, PreDecidingState};
use codec::Encode;
#[test]
fn missing_deposit_blocks_pre_deciding_enactment_estimate() {
let state = pre_deciding_state(100, 100, false, false);
assert_eq!(state, PreDecidingState::WaitingForDecisionDeposit);
assert_eq!(
state.enactment_estimate_reason(),
"until the decision deposit is placed and Deciding starts"
);
}
#[test]
fn queued_referendum_blocks_pre_deciding_enactment_estimate() {
let state = pre_deciding_state(100, 200, true, true);
assert_eq!(state, PreDecidingState::WaitingForTrackSlot);
assert_eq!(
state.enactment_estimate_reason(),
"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}");
}
}