use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use std::fmt;
use uuid::Uuid;
use super::user::ValidationError;
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct Proposal {
pub proposal_id: Uuid,
pub proposer_user_id: Uuid,
pub title: String,
pub description: String,
pub proposal_type: ProposalType,
pub quorum_required: Decimal,
pub approval_threshold: Decimal,
pub votes_for: Decimal,
pub votes_against: Decimal,
pub votes_abstain: Decimal,
pub status: ProposalStatus,
pub voting_start: DateTime<Utc>,
pub voting_end: DateTime<Utc>,
pub execution_time: Option<DateTime<Utc>>,
pub time_lock_seconds: i64,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, sqlx::Type, PartialEq, Eq)]
#[sqlx(type_name = "varchar", rename_all = "snake_case")]
#[derive(Default)]
pub enum ProposalType {
ParameterChange,
TokenListing,
ContractUpgrade,
TreasurySpend,
FeeChange,
EmergencyAction,
#[default]
General,
}
impl fmt::Display for ProposalType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ProposalType::ParameterChange => write!(f, "parameter_change"),
ProposalType::TokenListing => write!(f, "token_listing"),
ProposalType::ContractUpgrade => write!(f, "contract_upgrade"),
ProposalType::TreasurySpend => write!(f, "treasury_spend"),
ProposalType::FeeChange => write!(f, "fee_change"),
ProposalType::EmergencyAction => write!(f, "emergency_action"),
ProposalType::General => write!(f, "general"),
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, sqlx::Type, PartialEq, Eq)]
#[sqlx(type_name = "varchar", rename_all = "lowercase")]
#[derive(Default)]
pub enum ProposalStatus {
#[default]
Active,
Passed,
Rejected,
Executed,
Cancelled,
Expired,
}
impl fmt::Display for ProposalStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ProposalStatus::Active => write!(f, "active"),
ProposalStatus::Passed => write!(f, "passed"),
ProposalStatus::Rejected => write!(f, "rejected"),
ProposalStatus::Executed => write!(f, "executed"),
ProposalStatus::Cancelled => write!(f, "cancelled"),
ProposalStatus::Expired => write!(f, "expired"),
}
}
}
impl Proposal {
pub fn total_votes(&self) -> Decimal {
self.votes_for + self.votes_against + self.votes_abstain
}
pub fn is_quorum_reached(&self) -> bool {
self.total_votes() >= self.quorum_required
}
pub fn is_approved(&self) -> bool {
let total_decisive_votes = self.votes_for + self.votes_against;
if total_decisive_votes == dec!(0) {
return false;
}
let approval_rate = self.votes_for / total_decisive_votes;
approval_rate >= self.approval_threshold
}
pub fn is_voting_ended(&self) -> bool {
Utc::now() >= self.voting_end
}
pub fn can_execute(&self) -> bool {
if self.status != ProposalStatus::Passed {
return false;
}
if let Some(exec_time) = self.execution_time {
Utc::now() >= exec_time
} else {
true
}
}
pub fn approval_percentage(&self) -> Decimal {
let total_decisive = self.votes_for + self.votes_against;
if total_decisive == dec!(0) {
return dec!(0);
}
(self.votes_for / total_decisive) * dec!(100)
}
pub fn participation_rate(&self, total_voting_power: Decimal) -> Decimal {
if total_voting_power == dec!(0) {
return dec!(0);
}
(self.total_votes() / total_voting_power) * dec!(100)
}
pub fn validate(&self) -> Result<(), ValidationError> {
if self.title.is_empty() {
return Err(ValidationError("Title is required".to_string()));
}
if self.title.len() > 200 {
return Err(ValidationError(
"Title must be at most 200 characters".to_string(),
));
}
if self.description.is_empty() {
return Err(ValidationError("Description is required".to_string()));
}
if self.quorum_required <= dec!(0) {
return Err(ValidationError("Quorum must be positive".to_string()));
}
if self.approval_threshold <= dec!(0) || self.approval_threshold > dec!(1) {
return Err(ValidationError(
"Approval threshold must be between 0 and 1".to_string(),
));
}
if self.voting_end <= self.voting_start {
return Err(ValidationError(
"Voting end must be after voting start".to_string(),
));
}
if self.time_lock_seconds < 0 {
return Err(ValidationError("Time lock cannot be negative".to_string()));
}
Ok(())
}
}
impl fmt::Display for Proposal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Proposal({}, title='{}', status={})",
self.proposal_id, self.title, self.status
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct Vote {
pub vote_id: Uuid,
pub proposal_id: Uuid,
pub voter_user_id: Uuid,
pub voting_power: Decimal,
pub choice: VoteChoice,
pub reason: Option<String>,
pub voted_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, sqlx::Type, PartialEq, Eq)]
#[sqlx(type_name = "varchar", rename_all = "lowercase")]
#[derive(Default)]
pub enum VoteChoice {
For,
Against,
#[default]
Abstain,
}
impl fmt::Display for VoteChoice {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
VoteChoice::For => write!(f, "for"),
VoteChoice::Against => write!(f, "against"),
VoteChoice::Abstain => write!(f, "abstain"),
}
}
}
#[derive(Debug, Deserialize)]
pub struct CreateProposalRequest {
pub title: String,
pub description: String,
pub proposal_type: ProposalType,
pub voting_duration_hours: i64,
pub time_lock_hours: Option<i64>,
}
impl CreateProposalRequest {
pub fn validate(&self) -> Result<(), ValidationError> {
if self.title.is_empty() {
return Err(ValidationError("Title is required".to_string()));
}
if self.title.len() > 200 {
return Err(ValidationError(
"Title must be at most 200 characters".to_string(),
));
}
if self.description.is_empty() {
return Err(ValidationError("Description is required".to_string()));
}
if self.voting_duration_hours <= 0 {
return Err(ValidationError(
"Voting duration must be positive".to_string(),
));
}
if let Some(time_lock) = self.time_lock_hours {
if time_lock < 0 {
return Err(ValidationError("Time lock cannot be negative".to_string()));
}
}
Ok(())
}
}
#[derive(Debug, Deserialize)]
pub struct CastVoteRequest {
pub proposal_id: Uuid,
pub choice: VoteChoice,
pub reason: Option<String>,
}
impl CastVoteRequest {
pub fn validate(&self) -> Result<(), ValidationError> {
if let Some(ref reason) = self.reason {
if reason.len() > 1000 {
return Err(ValidationError(
"Reason must be at most 1000 characters".to_string(),
));
}
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GovernanceConfig {
pub min_proposal_power: Decimal,
pub default_quorum: Decimal,
pub default_approval_threshold: Decimal,
pub default_voting_duration_hours: i64,
pub default_time_lock_hours: i64,
}
impl Default for GovernanceConfig {
fn default() -> Self {
Self {
min_proposal_power: dec!(1000), default_quorum: dec!(0.1), default_approval_threshold: dec!(0.66), default_voting_duration_hours: 72, default_time_lock_hours: 24, }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_proposal_quorum() {
let proposal = Proposal {
proposal_id: Uuid::new_v4(),
proposer_user_id: Uuid::new_v4(),
title: "Test Proposal".to_string(),
description: "Test Description".to_string(),
proposal_type: ProposalType::General,
quorum_required: dec!(1000),
approval_threshold: dec!(0.66),
votes_for: dec!(600),
votes_against: dec!(200),
votes_abstain: dec!(100),
status: ProposalStatus::Active,
voting_start: Utc::now(),
voting_end: Utc::now() + chrono::Duration::hours(72),
execution_time: None,
time_lock_seconds: 0,
created_at: Utc::now(),
updated_at: Utc::now(),
};
assert_eq!(proposal.total_votes(), dec!(900));
assert!(!proposal.is_quorum_reached());
let mut proposal_with_quorum = proposal.clone();
proposal_with_quorum.votes_for = dec!(700);
assert_eq!(proposal_with_quorum.total_votes(), dec!(1000));
assert!(proposal_with_quorum.is_quorum_reached());
}
#[test]
fn test_proposal_approval() {
let proposal = Proposal {
proposal_id: Uuid::new_v4(),
proposer_user_id: Uuid::new_v4(),
title: "Test Proposal".to_string(),
description: "Test Description".to_string(),
proposal_type: ProposalType::General,
quorum_required: dec!(100),
approval_threshold: dec!(0.66),
votes_for: dec!(700),
votes_against: dec!(300),
votes_abstain: dec!(0),
status: ProposalStatus::Active,
voting_start: Utc::now(),
voting_end: Utc::now() + chrono::Duration::hours(72),
execution_time: None,
time_lock_seconds: 0,
created_at: Utc::now(),
updated_at: Utc::now(),
};
assert!(proposal.is_approved());
let mut rejected_proposal = proposal.clone();
rejected_proposal.votes_for = dec!(600);
rejected_proposal.votes_against = dec!(400);
assert!(!rejected_proposal.is_approved());
}
#[test]
fn test_proposal_approval_percentage() {
let proposal = Proposal {
proposal_id: Uuid::new_v4(),
proposer_user_id: Uuid::new_v4(),
title: "Test Proposal".to_string(),
description: "Test Description".to_string(),
proposal_type: ProposalType::General,
quorum_required: dec!(100),
approval_threshold: dec!(0.5),
votes_for: dec!(750),
votes_against: dec!(250),
votes_abstain: dec!(0),
status: ProposalStatus::Active,
voting_start: Utc::now(),
voting_end: Utc::now() + chrono::Duration::hours(72),
execution_time: None,
time_lock_seconds: 0,
created_at: Utc::now(),
updated_at: Utc::now(),
};
assert_eq!(proposal.approval_percentage(), dec!(75));
}
#[test]
fn test_participation_rate() {
let proposal = Proposal {
proposal_id: Uuid::new_v4(),
proposer_user_id: Uuid::new_v4(),
title: "Test Proposal".to_string(),
description: "Test Description".to_string(),
proposal_type: ProposalType::General,
quorum_required: dec!(100),
approval_threshold: dec!(0.5),
votes_for: dec!(300),
votes_against: dec!(200),
votes_abstain: dec!(100),
status: ProposalStatus::Active,
voting_start: Utc::now(),
voting_end: Utc::now() + chrono::Duration::hours(72),
execution_time: None,
time_lock_seconds: 0,
created_at: Utc::now(),
updated_at: Utc::now(),
};
assert_eq!(proposal.participation_rate(dec!(2000)), dec!(30));
}
}