//! 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, 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::() + c.as_str(), } }