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
544 lines
17 KiB
Rust
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(),
|
|
}
|
|
}
|