|
| 1 | +//! Governance Analytics and Participation Tracking Module |
| 2 | +//! |
| 3 | +//! Tracks participation metrics, voter engagement, and governance health. |
| 4 | +//! Provides data for governance dashboards and compliance reporting. |
| 5 | +//! |
| 6 | +//! # Metrics Tracked |
| 7 | +//! |
| 8 | +//! - Per-address: votes cast, proposals created, power used, delegation activity |
| 9 | +//! - Global: total proposals, total votes, average turnout, pass rate |
| 10 | +
|
| 11 | +use soroban_sdk::{Address, Env}; |
| 12 | + |
| 13 | +use crate::storage::{ANALYTICS, PARTICIPATION}; |
| 14 | +use crate::types::{GovernanceAnalytics, ParticipationRecord}; |
| 15 | + |
| 16 | +pub struct Analytics; |
| 17 | + |
| 18 | +impl Analytics { |
| 19 | + /// Record a vote participation event |
| 20 | + /// |
| 21 | + /// Updates both individual and global analytics |
| 22 | + pub fn record_vote(env: &Env, voter: &Address, power_used: i128) { |
| 23 | + // Update individual participation |
| 24 | + let mut record = Self::get_participation(env, voter).unwrap_or(ParticipationRecord { |
| 25 | + participant: voter.clone(), |
| 26 | + proposals_voted: 0, |
| 27 | + proposals_created: 0, |
| 28 | + total_power_used: 0, |
| 29 | + delegation_count: 0, |
| 30 | + last_active: 0, |
| 31 | + participation_score: 0, |
| 32 | + }); |
| 33 | + |
| 34 | + record.proposals_voted += 1; |
| 35 | + record.total_power_used += power_used; |
| 36 | + record.last_active = env.ledger().timestamp(); |
| 37 | + record.participation_score = Self::calculate_score(&record); |
| 38 | + |
| 39 | + env.storage() |
| 40 | + .persistent() |
| 41 | + .set(&(PARTICIPATION, voter.clone()), &record); |
| 42 | + |
| 43 | + // Update global analytics |
| 44 | + let mut analytics = Self::get_analytics(env); |
| 45 | + analytics.total_votes_cast += 1; |
| 46 | + analytics.last_updated = env.ledger().timestamp(); |
| 47 | + |
| 48 | + env.storage().instance().set(&ANALYTICS, &analytics); |
| 49 | + } |
| 50 | + |
| 51 | + /// Record a proposal creation event |
| 52 | + pub fn record_proposal_created(env: &Env, proposer: &Address) { |
| 53 | + // Update individual |
| 54 | + let mut record = Self::get_participation(env, proposer).unwrap_or(ParticipationRecord { |
| 55 | + participant: proposer.clone(), |
| 56 | + proposals_voted: 0, |
| 57 | + proposals_created: 0, |
| 58 | + total_power_used: 0, |
| 59 | + delegation_count: 0, |
| 60 | + last_active: 0, |
| 61 | + participation_score: 0, |
| 62 | + }); |
| 63 | + |
| 64 | + record.proposals_created += 1; |
| 65 | + record.last_active = env.ledger().timestamp(); |
| 66 | + record.participation_score = Self::calculate_score(&record); |
| 67 | + |
| 68 | + env.storage() |
| 69 | + .persistent() |
| 70 | + .set(&(PARTICIPATION, proposer.clone()), &record); |
| 71 | + |
| 72 | + // Update global |
| 73 | + let mut analytics = Self::get_analytics(env); |
| 74 | + analytics.total_proposals += 1; |
| 75 | + analytics.last_updated = env.ledger().timestamp(); |
| 76 | + |
| 77 | + env.storage().instance().set(&ANALYTICS, &analytics); |
| 78 | + } |
| 79 | + |
| 80 | + /// Record a proposal finalization (passed or failed) |
| 81 | + pub fn record_proposal_finalized(env: &Env, passed: bool) { |
| 82 | + let mut analytics = Self::get_analytics(env); |
| 83 | + |
| 84 | + if passed { |
| 85 | + analytics.proposals_passed += 1; |
| 86 | + } else { |
| 87 | + analytics.proposals_failed += 1; |
| 88 | + } |
| 89 | + |
| 90 | + analytics.last_updated = env.ledger().timestamp(); |
| 91 | + env.storage().instance().set(&ANALYTICS, &analytics); |
| 92 | + } |
| 93 | + |
| 94 | + /// Record a delegation event |
| 95 | + pub fn record_delegation(env: &Env, delegate: &Address) { |
| 96 | + let mut record = Self::get_participation(env, delegate).unwrap_or(ParticipationRecord { |
| 97 | + participant: delegate.clone(), |
| 98 | + proposals_voted: 0, |
| 99 | + proposals_created: 0, |
| 100 | + total_power_used: 0, |
| 101 | + delegation_count: 0, |
| 102 | + last_active: 0, |
| 103 | + participation_score: 0, |
| 104 | + }); |
| 105 | + |
| 106 | + record.delegation_count += 1; |
| 107 | + record.last_active = env.ledger().timestamp(); |
| 108 | + record.participation_score = Self::calculate_score(&record); |
| 109 | + |
| 110 | + env.storage() |
| 111 | + .persistent() |
| 112 | + .set(&(PARTICIPATION, delegate.clone()), &record); |
| 113 | + |
| 114 | + // Update global analytics |
| 115 | + let mut analytics = Self::get_analytics(env); |
| 116 | + analytics.active_delegations += 1; |
| 117 | + analytics.last_updated = env.ledger().timestamp(); |
| 118 | + |
| 119 | + env.storage().instance().set(&ANALYTICS, &analytics); |
| 120 | + } |
| 121 | + |
| 122 | + /// Record staking event |
| 123 | + pub fn record_staking(env: &Env, amount: i128) { |
| 124 | + let mut analytics = Self::get_analytics(env); |
| 125 | + analytics.total_staked += amount; |
| 126 | + analytics.last_updated = env.ledger().timestamp(); |
| 127 | + |
| 128 | + env.storage().instance().set(&ANALYTICS, &analytics); |
| 129 | + } |
| 130 | + |
| 131 | + /// Record unstaking event |
| 132 | + pub fn record_unstaking(env: &Env, amount: i128) { |
| 133 | + let mut analytics = Self::get_analytics(env); |
| 134 | + analytics.total_staked = if analytics.total_staked > amount { |
| 135 | + analytics.total_staked - amount |
| 136 | + } else { |
| 137 | + 0 |
| 138 | + }; |
| 139 | + analytics.last_updated = env.ledger().timestamp(); |
| 140 | + |
| 141 | + env.storage().instance().set(&ANALYTICS, &analytics); |
| 142 | + } |
| 143 | + |
| 144 | + /// Get participation record for an address |
| 145 | + pub fn get_participation(env: &Env, participant: &Address) -> Option<ParticipationRecord> { |
| 146 | + env.storage() |
| 147 | + .persistent() |
| 148 | + .get(&(PARTICIPATION, participant.clone())) |
| 149 | + } |
| 150 | + |
| 151 | + /// Get global governance analytics |
| 152 | + pub fn get_analytics(env: &Env) -> GovernanceAnalytics { |
| 153 | + env.storage() |
| 154 | + .instance() |
| 155 | + .get(&ANALYTICS) |
| 156 | + .unwrap_or(GovernanceAnalytics { |
| 157 | + total_proposals: 0, |
| 158 | + total_votes_cast: 0, |
| 159 | + unique_voters: 0, |
| 160 | + avg_turnout_bps: 0, |
| 161 | + active_delegations: 0, |
| 162 | + total_staked: 0, |
| 163 | + proposals_passed: 0, |
| 164 | + proposals_failed: 0, |
| 165 | + last_updated: 0, |
| 166 | + }) |
| 167 | + } |
| 168 | + |
| 169 | + /// Calculate participation score (0-10000 basis points) |
| 170 | + /// |
| 171 | + /// Score is based on: |
| 172 | + /// - Number of proposals voted on (40% weight) |
| 173 | + /// - Number of proposals created (30% weight) |
| 174 | + /// - Delegation activity (20% weight) |
| 175 | + /// - Recency bonus (10% weight) |
| 176 | + fn calculate_score(record: &ParticipationRecord) -> u32 { |
| 177 | + let vote_score = u32::min(record.proposals_voted * 400, 4000); |
| 178 | + let create_score = u32::min(record.proposals_created * 1000, 3000); |
| 179 | + let delegation_score = u32::min(record.delegation_count * 500, 2000); |
| 180 | + |
| 181 | + // Recency is hard to compute without context, give base score |
| 182 | + let recency_score: u32 = if record.last_active > 0 { 1000 } else { 0 }; |
| 183 | + |
| 184 | + u32::min( |
| 185 | + vote_score + create_score + delegation_score + recency_score, |
| 186 | + 10000, |
| 187 | + ) |
| 188 | + } |
| 189 | + |
| 190 | + /// Update global turnout average after a proposal is finalized |
| 191 | + pub fn update_turnout(env: &Env, _total_supply: i128, _votes_in_proposal: i128) { |
| 192 | + let mut analytics = Self::get_analytics(env); |
| 193 | + |
| 194 | + // Simple running average for now |
| 195 | + if analytics.total_proposals > 0 { |
| 196 | + let total_decided = analytics.proposals_passed + analytics.proposals_failed; |
| 197 | + if total_decided > 0 { |
| 198 | + // Approximate turnout tracking |
| 199 | + analytics.avg_turnout_bps = u32::min( |
| 200 | + (analytics.total_votes_cast as u32 * 10000) / (total_decided as u32 * 10), |
| 201 | + 10000, |
| 202 | + ); |
| 203 | + } |
| 204 | + } |
| 205 | + |
| 206 | + analytics.last_updated = env.ledger().timestamp(); |
| 207 | + env.storage().instance().set(&ANALYTICS, &analytics); |
| 208 | + } |
| 209 | +} |
0 commit comments