1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
5use thiserror::Error;
6
7#[derive(Debug, Error)]
8pub enum VotingError {
9 #[error("Invalid vote: {0}")]
10 InvalidVote(String),
11
12 #[error("Voting not allowed: {0}")]
13 VotingNotAllowed(String),
14
15 #[error("Quadratic calculation error: {0}")]
16 QuadraticError(String),
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct Vote {
22 pub proposal_id: String,
24
25 pub voter: String,
27
28 pub power: u64,
30
31 pub direction: VoteDirection,
33
34 pub bitcoin_signature: Option<String>,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
40pub enum VoteDirection {
41 For,
43
44 Against,
46
47 Abstain,
49}
50
51pub struct QuadraticVoting {
53 votes: HashMap<String, HashMap<String, Vote>>,
55}
56
57impl Default for QuadraticVoting {
58 fn default() -> Self {
59 Self::new()
60 }
61}
62
63impl QuadraticVoting {
64 pub fn new() -> Self {
66 Self {
67 votes: HashMap::new(),
68 }
69 }
70
71 pub fn cast_vote(&mut self, vote: Vote) -> Result<(), VotingError> {
73 if vote.power == 0 {
75 return Err(VotingError::InvalidVote(
76 "Vote power cannot be zero".to_string(),
77 ));
78 }
79
80 if vote.bitcoin_signature.is_none() {
82 return Err(VotingError::InvalidVote(
83 "Bitcoin signature required for BPC-3 compliance".to_string(),
84 ));
85 }
86
87 let proposal_votes = self.votes.entry(vote.proposal_id.clone()).or_default();
89
90 proposal_votes.insert(vote.voter.clone(), vote);
92
93 Ok(())
94 }
95
96 pub fn tally_votes(&self, proposal_id: &str) -> Result<VoteTally, VotingError> {
98 let proposal_votes = self.votes.get(proposal_id).ok_or_else(|| {
99 VotingError::InvalidVote(format!("No votes found for proposal {proposal_id}"))
100 })?;
101
102 let mut for_votes = 0.0;
103 let mut against_votes = 0.0;
104 let mut abstain_votes = 0.0;
105
106 for vote in proposal_votes.values() {
107 let quadratic_power = (vote.power as f64).sqrt();
109
110 match vote.direction {
111 VoteDirection::For => for_votes += quadratic_power,
112 VoteDirection::Against => against_votes += quadratic_power,
113 VoteDirection::Abstain => abstain_votes += quadratic_power,
114 }
115 }
116
117 Ok(VoteTally {
118 proposal_id: proposal_id.to_string(),
119 for_votes,
120 against_votes,
121 abstain_votes,
122 total_votes: proposal_votes.len(),
123 })
124 }
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct VoteTally {
130 pub proposal_id: String,
132
133 pub for_votes: f64,
135
136 pub against_votes: f64,
138
139 pub abstain_votes: f64,
141
142 pub total_votes: usize,
144}