synor/apps/cli/src/commands/governance.rs
Gulshan Yadav 5c643af64c fix: resolve all clippy warnings for CI
Fix all Rust clippy warnings that were causing CI failures when built
with RUSTFLAGS=-Dwarnings. Changes include:

- Replace derivable_impls with derive macros for BlockBody, Network, etc.
- Use div_ceil() instead of manual implementation
- Fix should_implement_trait by renaming from_str to parse
- Add type aliases for type_complexity warnings
- Use or_default(), is_some_and(), is_multiple_of() where appropriate
- Remove needless borrows and redundant closures
- Fix manual_strip with strip_prefix()
- Add allow attributes for intentional patterns (too_many_arguments,
  needless_range_loop in cryptographic code, assertions_on_constants)
- Remove unused imports, mut bindings, and dead code in tests
2026-01-08 05:58:22 +05:30

544 lines
17 KiB
Rust

//! Governance commands for DAO voting and treasury management.
use anyhow::Result;
use serde_json::json;
use crate::client::RpcClient;
use crate::output::{self, OutputFormat};
use crate::GovernanceCommands;
/// Handle governance commands.
pub async fn handle(
client: &RpcClient,
cmd: GovernanceCommands,
format: OutputFormat,
) -> Result<()> {
match cmd {
GovernanceCommands::Info => info(client, format).await,
GovernanceCommands::Stats => stats(client, format).await,
// Proposal commands
GovernanceCommands::Proposals { state } => {
proposals(client, state.as_deref(), format).await
}
GovernanceCommands::Proposal { id } => proposal(client, &id, format).await,
GovernanceCommands::CreateProposal {
proposer,
proposal_type,
title,
description,
recipient,
amount,
parameter,
old_value,
new_value,
} => {
create_proposal(
client,
&proposer,
&proposal_type,
&title,
&description,
recipient.as_deref(),
amount,
parameter.as_deref(),
old_value.as_deref(),
new_value.as_deref(),
format,
)
.await
}
GovernanceCommands::Vote {
proposal_id,
voter,
choice,
reason,
} => {
vote(
client,
&proposal_id,
&voter,
&choice,
reason.as_deref(),
format,
)
.await
}
GovernanceCommands::Execute {
proposal_id,
executor,
} => execute(client, &proposal_id, &executor, format).await,
// Treasury commands
GovernanceCommands::Treasury => treasury(client, format).await,
GovernanceCommands::TreasuryPool { id } => treasury_pool(client, &id, format).await,
}
}
/// Get governance info.
async fn info(client: &RpcClient, format: OutputFormat) -> Result<()> {
let info = client.get_governance_info().await?;
match format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&info)?);
}
OutputFormat::Text => {
output::print_header("Governance Info");
output::print_kv("Proposal Threshold", &format_synor(info.proposal_threshold));
output::print_kv("Quorum", &format!("{}%", info.quorum_bps as f64 / 100.0));
output::print_kv("Voting Period", &format_blocks(info.voting_period_blocks));
output::print_kv(
"Execution Delay",
&format_blocks(info.execution_delay_blocks),
);
println!();
output::print_kv("Total Proposals", &info.total_proposals.to_string());
output::print_kv("Active Proposals", &info.active_proposals.to_string());
output::print_kv(
"Treasury Balance",
&format_synor(info.total_treasury_balance),
);
}
}
Ok(())
}
/// Get DAO statistics.
async fn stats(client: &RpcClient, format: OutputFormat) -> Result<()> {
let stats = client.get_dao_stats().await?;
match format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&stats)?);
}
OutputFormat::Text => {
output::print_header("DAO Statistics");
output::print_kv("Total Proposals", &stats.total_proposals.to_string());
output::print_kv("Active Proposals", &stats.active_proposals.to_string());
output::print_kv("Passed Proposals", &stats.passed_proposals.to_string());
output::print_kv("Defeated Proposals", &stats.defeated_proposals.to_string());
output::print_kv("Executed Proposals", &stats.executed_proposals.to_string());
output::print_kv("Total Votes Cast", &stats.total_votes_cast.to_string());
output::print_kv("Council Members", &stats.council_members.to_string());
}
}
Ok(())
}
/// List proposals.
async fn proposals(client: &RpcClient, state: Option<&str>, format: OutputFormat) -> Result<()> {
let proposals = match state {
Some(s) => client.get_proposals_by_state(s).await?,
None => client.get_active_proposals().await?,
};
match format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&proposals)?);
}
OutputFormat::Text => {
let title = match state {
Some(s) => format!("{} Proposals", capitalize(s)),
None => "Active Proposals".to_string(),
};
output::print_header(&title);
if proposals.is_empty() {
output::print_info("No proposals found");
return Ok(());
}
for proposal in &proposals {
println!();
println!(
"#{} [{}] {}",
proposal.number,
state_emoji(&proposal.state),
proposal.title
);
println!(" ID: {}...", &proposal.id[..16]);
println!(" Proposer: {}", &proposal.proposer);
println!(
" Votes: {} Yes / {} No / {} Abstain ({:.1}% Yes)",
proposal.yes_votes,
proposal.no_votes,
proposal.abstain_votes,
proposal.yes_percentage
);
println!(
" Participation: {:.2}% | Quorum: {}",
proposal.participation_rate,
if proposal.has_quorum {
"Reached"
} else {
"Not reached"
}
);
if let Some(remaining) = proposal.time_remaining_blocks {
println!(" Time Remaining: {}", format_blocks(remaining));
}
}
}
}
Ok(())
}
/// Get proposal details.
async fn proposal(client: &RpcClient, id: &str, format: OutputFormat) -> Result<()> {
let proposal = client.get_proposal(id).await?;
if let Some(error) = &proposal.error {
output::print_error(&format!("Failed to get proposal: {}", error));
return Ok(());
}
match format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&proposal)?);
}
OutputFormat::Text => {
output::print_header(&format!(
"Proposal #{}: {}",
proposal.number, proposal.title
));
println!();
output::print_kv("ID", &proposal.id);
output::print_kv(
"State",
&format!("{} {}", state_emoji(&proposal.state), proposal.state),
);
output::print_kv("Type", &proposal.proposal_type);
output::print_kv("Proposer", &proposal.proposer);
println!();
output::print_kv("Description", "");
println!("{}", proposal.description);
if let Some(url) = &proposal.discussion_url {
output::print_kv("Discussion", url);
}
println!();
println!("Timeline:");
output::print_kv(" Created", &format!("Block {}", proposal.created_at_block));
output::print_kv(
" Voting Starts",
&format!("Block {}", proposal.voting_starts_block),
);
output::print_kv(
" Voting Ends",
&format!("Block {}", proposal.voting_ends_block),
);
output::print_kv(
" Execution Allowed",
&format!("Block {}", proposal.execution_allowed_block),
);
println!();
println!("Voting Results:");
let total = proposal.yes_votes + proposal.no_votes + proposal.abstain_votes;
let yes_pct = if total > 0 {
proposal.yes_votes as f64 / (proposal.yes_votes + proposal.no_votes) as f64 * 100.0
} else {
0.0
};
output::print_kv(
" Yes",
&format!("{} ({:.1}%)", format_synor(proposal.yes_votes), yes_pct),
);
output::print_kv(" No", &format_synor(proposal.no_votes));
output::print_kv(" Abstain", &format_synor(proposal.abstain_votes));
output::print_kv(" Total Voters", &proposal.votes.len().to_string());
if !proposal.votes.is_empty() {
println!();
println!("Recent Votes:");
for vote in proposal.votes.iter().take(5) {
let choice_emoji = match vote.choice.as_str() {
"Yes" => "",
"No" => "",
"Abstain" => "⏸️",
_ => "🔘",
};
println!(
" {} {} {} (weight: {})",
choice_emoji,
&vote.voter[..20],
vote.choice,
vote.weight
);
if let Some(reason) = &vote.reason {
println!(" \"{}\"", reason);
}
}
}
}
}
Ok(())
}
/// Create a proposal.
#[allow(clippy::too_many_arguments)]
async fn create_proposal(
client: &RpcClient,
proposer: &str,
proposal_type: &str,
title: &str,
description: &str,
recipient: Option<&str>,
amount: Option<u64>,
parameter: Option<&str>,
old_value: Option<&str>,
new_value: Option<&str>,
format: OutputFormat,
) -> Result<()> {
// Build proposal params based on type
let params = match proposal_type {
"treasury_spend" | "ecosystem_grant" => {
let recipient = recipient
.ok_or_else(|| anyhow::anyhow!("--recipient required for treasury proposals"))?;
let amount = amount
.ok_or_else(|| anyhow::anyhow!("--amount required for treasury proposals"))?;
json!({
"recipient": recipient,
"amount": amount
})
}
"parameter_change" => {
let param = parameter.ok_or_else(|| anyhow::anyhow!("--parameter required"))?;
let old = old_value.ok_or_else(|| anyhow::anyhow!("--old-value required"))?;
let new = new_value.ok_or_else(|| anyhow::anyhow!("--new-value required"))?;
json!({
"parameter": param,
"old_value": old,
"new_value": new
})
}
"signaling" => {
json!({})
}
_ => {
json!({})
}
};
let spinner = output::create_spinner("Creating proposal...");
let result = client
.create_proposal(proposer, proposal_type, title, description, params)
.await?;
spinner.finish_and_clear();
if let Some(error) = &result.error {
output::print_error(&format!("Failed to create proposal: {}", error));
return Ok(());
}
match format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&result)?);
}
OutputFormat::Text => {
output::print_success("Proposal created!");
output::print_kv("Proposal ID", &result.proposal_id);
output::print_kv("Number", &result.number.to_string());
println!();
output::print_info("Voting will begin after the voting delay period");
}
}
Ok(())
}
/// Cast a vote.
async fn vote(
client: &RpcClient,
proposal_id: &str,
voter: &str,
choice: &str,
reason: Option<&str>,
format: OutputFormat,
) -> Result<()> {
// Validate choice
let valid_choices = ["yes", "no", "abstain"];
if !valid_choices.contains(&choice.to_lowercase().as_str()) {
anyhow::bail!("Invalid choice. Use: yes, no, or abstain");
}
let spinner = output::create_spinner("Casting vote...");
let result = client.vote(proposal_id, voter, choice, reason).await?;
spinner.finish_and_clear();
if let Some(error) = &result.error {
output::print_error(&format!("Failed to vote: {}", error));
return Ok(());
}
match format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&result)?);
}
OutputFormat::Text => {
output::print_success("Vote cast!");
output::print_kv("Proposal", &result.proposal_id);
output::print_kv("Voter", &result.voter);
output::print_kv("Choice", &result.choice);
}
}
Ok(())
}
/// Execute a proposal.
async fn execute(
client: &RpcClient,
proposal_id: &str,
executor: &str,
format: OutputFormat,
) -> Result<()> {
let spinner = output::create_spinner("Executing proposal...");
let result = client.execute_proposal(proposal_id, executor).await?;
spinner.finish_and_clear();
if let Some(error) = &result.error {
output::print_error(&format!("Failed to execute proposal: {}", error));
return Ok(());
}
match format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&result)?);
}
OutputFormat::Text => {
output::print_success("Proposal executed!");
output::print_kv("Proposal", &result.proposal_id);
output::print_kv("Executed At", &format!("Block {}", result.executed_at));
}
}
Ok(())
}
/// Get treasury overview.
async fn treasury(client: &RpcClient, format: OutputFormat) -> Result<()> {
let pools = client.get_treasury_pools().await?;
let total = client.get_treasury_balance().await?;
match format {
OutputFormat::Json => {
let result = json!({
"total_balance": total,
"pools": pools
});
println!("{}", serde_json::to_string_pretty(&result)?);
}
OutputFormat::Text => {
output::print_header("Treasury Overview");
output::print_kv("Total Balance", &format_synor(total));
println!();
println!("Pools:");
for pool in &pools {
println!();
let status = if pool.frozen {
"🔒 FROZEN"
} else {
"✅ Active"
};
println!(" {} [{}]", pool.name, status);
println!(" ID: {}", pool.id);
println!(" Balance: {}", format_synor(pool.balance));
println!(
" Total Deposited: {}",
format_synor(pool.total_deposited)
);
println!(" Total Spent: {}", format_synor(pool.total_spent));
}
}
}
Ok(())
}
/// Get treasury pool details.
async fn treasury_pool(client: &RpcClient, id: &str, format: OutputFormat) -> Result<()> {
let pool = client.get_treasury_pool(id).await?;
match format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&pool)?);
}
OutputFormat::Text => {
let status = if pool.frozen {
"🔒 FROZEN"
} else {
"✅ Active"
};
output::print_header(&format!("{} [{}]", pool.name, status));
output::print_kv("ID", &pool.id);
output::print_kv("Balance", &format_synor(pool.balance));
output::print_kv("Total Deposited", &format_synor(pool.total_deposited));
output::print_kv("Total Spent", &format_synor(pool.total_spent));
}
}
Ok(())
}
// ==================== Helper Functions ====================
/// Format SYNOR amount (8 decimal places).
fn format_synor(amount: u64) -> String {
let whole = amount / 100_000_000;
let frac = amount % 100_000_000;
if frac == 0 {
format!("{} SYNOR", whole)
} else {
format!("{}.{:08} SYNOR", whole, frac)
.trim_end_matches('0')
.to_string()
}
}
/// Format blocks as human-readable time.
fn format_blocks(blocks: u64) -> String {
// Assuming ~1 second per block
let seconds = blocks;
if seconds < 60 {
format!("{} blocks (~{} sec)", blocks, seconds)
} else if seconds < 3600 {
format!("{} blocks (~{} min)", blocks, seconds / 60)
} else if seconds < 86400 {
format!("{} blocks (~{:.1} hr)", blocks, seconds as f64 / 3600.0)
} else {
format!("{} blocks (~{:.1} days)", blocks, seconds as f64 / 86400.0)
}
}
/// State emoji.
fn state_emoji(state: &str) -> &'static str {
match state.to_lowercase().as_str() {
"pending" => "",
"active" => "🗳️",
"passed" => "",
"defeated" => "",
"executed" => "🚀",
"cancelled" => "🚫",
"expired" => "",
_ => "",
}
}
/// Capitalize first letter.
fn capitalize(s: &str) -> String {
let mut c = s.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
}
}