Files
microdao-daarion/services/dao-service/governance_engine.py
Apple 3de3c8cb36 feat: Add presence heartbeat for Matrix online status
- matrix-gateway: POST /internal/matrix/presence/online endpoint
- usePresenceHeartbeat hook with activity tracking
- Auto away after 5 min inactivity
- Offline on page close/visibility change
- Integrated in MatrixChatRoom component
2025-11-27 00:19:40 -08:00

208 lines
6.9 KiB
Python

"""
Governance Engine — Voting power calculation and proposal evaluation
Phase 8: DAO Dashboard
"""
from decimal import Decimal, getcontext
from typing import List, Optional
from models import DaoRead, ProposalRead, VoteRead, ProposalResult
# Set decimal precision
getcontext().prec = 28
class GovernanceEngine:
"""Engine for calculating voting power and evaluating proposals"""
# ========================================================================
# Voting Power Calculation
# ========================================================================
async def calculate_voting_power(
self,
actor_user_id: str,
dao: DaoRead,
base_power: Optional[Decimal] = None
) -> Decimal:
"""
Calculate voting power based on DAO's governance model
Args:
actor_user_id: User ID
dao: DAO configuration
base_power: Base voting power (e.g., token balance)
Returns:
Calculated voting weight
"""
if dao.governance_model == "simple":
return await self.calculate_voting_power_simple(actor_user_id, dao)
elif dao.governance_model == "quadratic":
return await self.calculate_voting_power_quadratic(actor_user_id, dao, base_power or Decimal('1'))
elif dao.governance_model == "delegated":
return await self.calculate_voting_power_delegated(actor_user_id, dao)
else:
# Default to simple
return Decimal('1')
async def calculate_voting_power_simple(
self,
actor_user_id: str,
dao: DaoRead
) -> Decimal:
"""
Simple voting: 1 user = 1 vote
Returns:
Always returns 1
"""
return Decimal('1')
async def calculate_voting_power_quadratic(
self,
actor_user_id: str,
dao: DaoRead,
base_power: Decimal
) -> Decimal:
"""
Quadratic voting: weight = sqrt(base_power)
Reduces influence of whales
Args:
actor_user_id: User ID
dao: DAO configuration
base_power: Base voting power (e.g., token count)
Returns:
Square root of base power
"""
if base_power <= 0:
return Decimal('0')
# Calculate square root using Decimal
return base_power.sqrt()
async def calculate_voting_power_delegated(
self,
actor_user_id: str,
dao: DaoRead
) -> Decimal:
"""
Delegated voting: includes delegated votes from other users
TODO: Implement delegation graph traversal
For MVP, return simple power
Args:
actor_user_id: User ID
dao: DAO configuration
Returns:
Voting power including delegated votes
"""
# For MVP, return simple power
# In production, would query delegation graph
return Decimal('1')
# ========================================================================
# Proposal Evaluation
# ========================================================================
async def evaluate_proposal(
self,
dao: DaoRead,
proposal: ProposalRead,
votes: List[VoteRead],
total_eligible_voters: int
) -> ProposalResult:
"""
Evaluate proposal outcome based on votes and DAO configuration
Args:
dao: DAO configuration
proposal: Proposal being evaluated
votes: List of all votes
total_eligible_voters: Total number of DAO members eligible to vote
Returns:
ProposalResult with evaluation outcome
"""
# Calculate vote counts
votes_yes = len([v for v in votes if v.vote_value == 'yes'])
votes_no = len([v for v in votes if v.vote_value == 'no'])
votes_abstain = len([v for v in votes if v.vote_value == 'abstain'])
# Calculate vote weights
total_weight_yes = sum([v.weight for v in votes if v.vote_value == 'yes'], Decimal('0'))
total_weight_no = sum([v.weight for v in votes if v.vote_value == 'no'], Decimal('0'))
total_weight_abstain = sum([v.weight for v in votes if v.vote_value == 'abstain'], Decimal('0'))
# Total participating voters
total_participating = votes_yes + votes_no + votes_abstain
# Get quorum requirement (from proposal override or DAO default)
quorum_percent = proposal.quorum_percent_override or dao.quorum_percent
quorum_required = Decimal(str(quorum_percent))
# Calculate participation rate
if total_eligible_voters > 0:
participation_rate = Decimal(str(total_participating)) / Decimal(str(total_eligible_voters)) * Decimal('100')
else:
participation_rate = Decimal('0')
# Check if quorum is reached
quorum_reached = participation_rate >= quorum_required
# Determine if proposal passed
is_passed = False
winning_option = None
if quorum_reached:
# Simple majority: yes must be greater than no
if total_weight_yes > total_weight_no:
is_passed = True
winning_option = "yes"
elif total_weight_no > total_weight_yes:
winning_option = "no"
# If equal, proposal fails (no action taken)
# Determine status
if proposal.status == "active":
# Proposal is still active, don't change status
status = "active"
elif not quorum_reached:
status = "rejected" # Failed due to quorum
elif is_passed:
status = "passed"
else:
status = "rejected"
return ProposalResult(
proposal_id=proposal.id,
status=status,
votes_yes=votes_yes,
votes_no=votes_no,
votes_abstain=votes_abstain,
total_weight_yes=total_weight_yes,
total_weight_no=total_weight_no,
total_weight_abstain=total_weight_abstain,
total_eligible_voters=total_eligible_voters,
quorum_required=quorum_required,
quorum_reached=quorum_reached,
is_passed=is_passed,
winning_option=winning_option
)
# ========================================================================
# Helper Methods
# ========================================================================
def calculate_participation_rate(
self,
total_votes: int,
total_eligible: int
) -> Decimal:
"""Calculate participation rate as percentage"""
if total_eligible == 0:
return Decimal('0')
return Decimal(str(total_votes)) / Decimal(str(total_eligible)) * Decimal('100')