From d8a432ce3321d1cd3bf1e10ff815f678ed9cdcee Mon Sep 17 00:00:00 2001 From: oib Date: Sat, 28 Feb 2026 23:24:26 +0100 Subject: [PATCH] feat: complete phase 3 developer ecosystem and dao governance --- .../src/app/domain/dao_governance.py | 114 +++++ .../src/app/domain/developer_platform.py | 136 ++++++ .../src/app/schemas/dao_governance.py | 29 ++ .../src/app/schemas/developer_platform.py | 31 ++ .../app/services/dao_governance_service.py | 202 +++++++++ .../services/developer_platform_service.py | 417 ++++++++++++++++++ .../tests/test_dao_governance.py | 124 ++++++ .../tests/test_developer_platform.py | 110 +++++ contracts/contracts/DAOGovernance.sol | 178 ++++++++ 9 files changed, 1341 insertions(+) create mode 100644 apps/coordinator-api/src/app/domain/dao_governance.py create mode 100644 apps/coordinator-api/src/app/domain/developer_platform.py create mode 100644 apps/coordinator-api/src/app/schemas/dao_governance.py create mode 100644 apps/coordinator-api/src/app/schemas/developer_platform.py create mode 100644 apps/coordinator-api/src/app/services/dao_governance_service.py create mode 100644 apps/coordinator-api/src/app/services/developer_platform_service.py create mode 100644 apps/coordinator-api/tests/test_dao_governance.py create mode 100644 apps/coordinator-api/tests/test_developer_platform.py create mode 100644 contracts/contracts/DAOGovernance.sol diff --git a/apps/coordinator-api/src/app/domain/dao_governance.py b/apps/coordinator-api/src/app/domain/dao_governance.py new file mode 100644 index 00000000..4f012b00 --- /dev/null +++ b/apps/coordinator-api/src/app/domain/dao_governance.py @@ -0,0 +1,114 @@ +""" +DAO Governance Domain Models + +Domain models for managing multi-jurisdictional DAOs, regional councils, and global treasuries. +""" + +from __future__ import annotations + +from datetime import datetime +from enum import Enum +from typing import Dict, List, Optional +from uuid import uuid4 + +from sqlalchemy import Column, JSON +from sqlmodel import Field, SQLModel, Relationship + +class ProposalState(str, Enum): + PENDING = "pending" + ACTIVE = "active" + CANCELED = "canceled" + DEFEATED = "defeated" + SUCCEEDED = "succeeded" + QUEUED = "queued" + EXPIRED = "expired" + EXECUTED = "executed" + +class ProposalType(str, Enum): + GRANT = "grant" + PARAMETER_CHANGE = "parameter_change" + MEMBER_ELECTION = "member_election" + GENERAL = "general" + +class DAOMember(SQLModel, table=True): + """A member participating in DAO governance""" + __tablename__ = "dao_member" + + id: str = Field(default_factory=lambda: uuid4().hex, primary_key=True) + wallet_address: str = Field(index=True, unique=True) + + staked_amount: float = Field(default=0.0) + voting_power: float = Field(default=0.0) + + is_council_member: bool = Field(default=False) + council_region: Optional[str] = Field(default=None, index=True) + + joined_at: datetime = Field(default_factory=datetime.utcnow) + last_active: datetime = Field(default_factory=datetime.utcnow) + + # Relationships + votes: List["Vote"] = Relationship(back_populates="member") + +class DAOProposal(SQLModel, table=True): + """A governance proposal""" + __tablename__ = "dao_proposal" + + id: str = Field(default_factory=lambda: uuid4().hex, primary_key=True) + contract_proposal_id: Optional[str] = Field(default=None, index=True) + + proposer_address: str = Field(index=True) + title: str = Field() + description: str = Field() + + proposal_type: ProposalType = Field(default=ProposalType.GENERAL) + target_region: Optional[str] = Field(default=None, index=True) # None = Global + + status: ProposalState = Field(default=ProposalState.PENDING, index=True) + + for_votes: float = Field(default=0.0) + against_votes: float = Field(default=0.0) + abstain_votes: float = Field(default=0.0) + + execution_payload: Dict[str, str] = Field(default_factory=dict, sa_column=Column(JSON)) + + start_time: datetime = Field(default_factory=datetime.utcnow) + end_time: datetime = Field(default_factory=datetime.utcnow) + + created_at: datetime = Field(default_factory=datetime.utcnow) + + # Relationships + votes: List["Vote"] = Relationship(back_populates="proposal") + +class Vote(SQLModel, table=True): + """A vote cast on a proposal""" + __tablename__ = "dao_vote" + + id: str = Field(default_factory=lambda: uuid4().hex, primary_key=True) + proposal_id: str = Field(foreign_key="dao_proposal.id", index=True) + member_id: str = Field(foreign_key="dao_member.id", index=True) + + support: bool = Field() # True = For, False = Against + weight: float = Field() + + tx_hash: Optional[str] = Field(default=None) + created_at: datetime = Field(default_factory=datetime.utcnow) + + # Relationships + proposal: DAOProposal = Relationship(back_populates="votes") + member: DAOMember = Relationship(back_populates="votes") + +class TreasuryAllocation(SQLModel, table=True): + """Tracks allocations and spending from the global treasury""" + __tablename__ = "treasury_allocation" + + id: str = Field(default_factory=lambda: uuid4().hex, primary_key=True) + proposal_id: Optional[str] = Field(foreign_key="dao_proposal.id", default=None) + + amount: float = Field() + token_symbol: str = Field(default="AITBC") + + recipient_address: str = Field() + purpose: str = Field() + + tx_hash: Optional[str] = Field(default=None) + executed_at: datetime = Field(default_factory=datetime.utcnow) diff --git a/apps/coordinator-api/src/app/domain/developer_platform.py b/apps/coordinator-api/src/app/domain/developer_platform.py new file mode 100644 index 00000000..a7319f0c --- /dev/null +++ b/apps/coordinator-api/src/app/domain/developer_platform.py @@ -0,0 +1,136 @@ +""" +Developer Platform Domain Models + +Domain models for managing the developer ecosystem, bounties, certifications, and regional hubs. +""" + +from __future__ import annotations + +from datetime import datetime +from enum import Enum +from typing import Dict, List, Optional +from uuid import uuid4 + +from sqlalchemy import Column, JSON +from sqlmodel import Field, SQLModel, Relationship + +class BountyStatus(str, Enum): + OPEN = "open" + IN_PROGRESS = "in_progress" + IN_REVIEW = "in_review" + COMPLETED = "completed" + CANCELLED = "cancelled" + +class CertificationLevel(str, Enum): + BEGINNER = "beginner" + INTERMEDIATE = "intermediate" + ADVANCED = "advanced" + EXPERT = "expert" + +class DeveloperProfile(SQLModel, table=True): + """Profile for a developer in the AITBC ecosystem""" + __tablename__ = "developer_profile" + + id: str = Field(default_factory=lambda: uuid4().hex, primary_key=True) + wallet_address: str = Field(index=True, unique=True) + github_handle: Optional[str] = Field(default=None) + email: Optional[str] = Field(default=None) + + reputation_score: float = Field(default=0.0) + total_earned_aitbc: float = Field(default=0.0) + + skills: List[str] = Field(default_factory=list, sa_column=Column(JSON)) + + is_active: bool = Field(default=True) + created_at: datetime = Field(default_factory=datetime.utcnow) + updated_at: datetime = Field(default_factory=datetime.utcnow) + + # Relationships + certifications: List["DeveloperCertification"] = Relationship(back_populates="developer") + bounty_submissions: List["BountySubmission"] = Relationship(back_populates="developer") + +class DeveloperCertification(SQLModel, table=True): + """Certifications earned by developers""" + __tablename__ = "developer_certification" + + id: str = Field(default_factory=lambda: uuid4().hex, primary_key=True) + developer_id: str = Field(foreign_key="developer_profile.id", index=True) + + certification_name: str = Field(index=True) + level: CertificationLevel = Field(default=CertificationLevel.BEGINNER) + + issued_by: str = Field() # Could be an agent or a DAO entity + issued_at: datetime = Field(default_factory=datetime.utcnow) + expires_at: Optional[datetime] = Field(default=None) + + ipfs_credential_cid: Optional[str] = Field(default=None) # Proof of certification + + # Relationships + developer: DeveloperProfile = Relationship(back_populates="certifications") + +class RegionalHub(SQLModel, table=True): + """Regional developer hubs for local coordination""" + __tablename__ = "regional_hub" + + id: str = Field(default_factory=lambda: uuid4().hex, primary_key=True) + region_code: str = Field(index=True, unique=True) # e.g. "US-EAST", "EU-CENTRAL" + name: str = Field() + description: Optional[str] = Field(default=None) + + lead_wallet_address: str = Field() # Hub lead + member_count: int = Field(default=0) + + budget_allocation: float = Field(default=0.0) + spent_budget: float = Field(default=0.0) + + created_at: datetime = Field(default_factory=datetime.utcnow) + +class BountyTask(SQLModel, table=True): + """Automated bounty board tasks""" + __tablename__ = "bounty_task" + + id: str = Field(default_factory=lambda: uuid4().hex, primary_key=True) + title: str = Field() + description: str = Field() + + required_skills: List[str] = Field(default_factory=list, sa_column=Column(JSON)) + difficulty_level: CertificationLevel = Field(default=CertificationLevel.INTERMEDIATE) + + reward_amount: float = Field() + reward_token: str = Field(default="AITBC") + + status: BountyStatus = Field(default=BountyStatus.OPEN, index=True) + + creator_address: str = Field(index=True) + assigned_developer_id: Optional[str] = Field(foreign_key="developer_profile.id", default=None) + + deadline: Optional[datetime] = Field(default=None) + created_at: datetime = Field(default_factory=datetime.utcnow) + updated_at: datetime = Field(default_factory=datetime.utcnow) + + # Relationships + submissions: List["BountySubmission"] = Relationship(back_populates="bounty") + +class BountySubmission(SQLModel, table=True): + """Submissions for bounty tasks""" + __tablename__ = "bounty_submission" + + id: str = Field(default_factory=lambda: uuid4().hex, primary_key=True) + bounty_id: str = Field(foreign_key="bounty_task.id", index=True) + developer_id: str = Field(foreign_key="developer_profile.id", index=True) + + github_pr_url: Optional[str] = Field(default=None) + submission_notes: str = Field(default="") + + is_approved: bool = Field(default=False) + review_notes: Optional[str] = Field(default=None) + reviewer_address: Optional[str] = Field(default=None) + + tx_hash_reward: Optional[str] = Field(default=None) # Hash of the reward payout transaction + + submitted_at: datetime = Field(default_factory=datetime.utcnow) + reviewed_at: Optional[datetime] = Field(default=None) + + # Relationships + bounty: BountyTask = Relationship(back_populates="submissions") + developer: DeveloperProfile = Relationship(back_populates="bounty_submissions") diff --git a/apps/coordinator-api/src/app/schemas/dao_governance.py b/apps/coordinator-api/src/app/schemas/dao_governance.py new file mode 100644 index 00000000..496f9ef9 --- /dev/null +++ b/apps/coordinator-api/src/app/schemas/dao_governance.py @@ -0,0 +1,29 @@ +from pydantic import BaseModel, Field +from typing import Optional, Dict +from datetime import datetime +from .dao_governance import ProposalState, ProposalType + +class MemberCreate(BaseModel): + wallet_address: str + staked_amount: float = 0.0 + +class ProposalCreate(BaseModel): + proposer_address: str + title: str + description: str + proposal_type: ProposalType = ProposalType.GENERAL + target_region: Optional[str] = None + execution_payload: Dict[str, str] = Field(default_factory=dict) + voting_period_days: int = 7 + +class VoteCreate(BaseModel): + member_address: str + proposal_id: str + support: bool + +class AllocationCreate(BaseModel): + proposal_id: Optional[str] = None + amount: float + token_symbol: str = "AITBC" + recipient_address: str + purpose: str diff --git a/apps/coordinator-api/src/app/schemas/developer_platform.py b/apps/coordinator-api/src/app/schemas/developer_platform.py new file mode 100644 index 00000000..26598914 --- /dev/null +++ b/apps/coordinator-api/src/app/schemas/developer_platform.py @@ -0,0 +1,31 @@ +from pydantic import BaseModel +from typing import Optional, List +from datetime import datetime +from .developer_platform import BountyStatus, CertificationLevel + +class DeveloperCreate(BaseModel): + wallet_address: str + github_handle: Optional[str] = None + email: Optional[str] = None + skills: List[str] = [] + +class BountyCreate(BaseModel): + title: str + description: str + required_skills: List[str] = [] + difficulty_level: CertificationLevel = CertificationLevel.INTERMEDIATE + reward_amount: float + creator_address: str + deadline: Optional[datetime] = None + +class BountySubmissionCreate(BaseModel): + developer_id: str + github_pr_url: Optional[str] = None + submission_notes: str = "" + +class CertificationGrant(BaseModel): + developer_id: str + certification_name: str + level: CertificationLevel + issued_by: str + ipfs_credential_cid: Optional[str] = None diff --git a/apps/coordinator-api/src/app/services/dao_governance_service.py b/apps/coordinator-api/src/app/services/dao_governance_service.py new file mode 100644 index 00000000..85554fe8 --- /dev/null +++ b/apps/coordinator-api/src/app/services/dao_governance_service.py @@ -0,0 +1,202 @@ +""" +DAO Governance Service + +Service for managing multi-jurisdictional DAOs, regional councils, and global treasuries. +""" + +from __future__ import annotations + +import logging +from datetime import datetime, timedelta +from typing import List, Optional + +from sqlmodel import Session, select +from fastapi import HTTPException + +from ..domain.dao_governance import ( + DAOMember, DAOProposal, Vote, TreasuryAllocation, + ProposalState, ProposalType +) +from ..schemas.dao_governance import ( + MemberCreate, ProposalCreate, VoteCreate, AllocationCreate +) +from ..blockchain.contract_interactions import ContractInteractionService + +logger = logging.getLogger(__name__) + +class DAOGovernanceService: + def __init__( + self, + session: Session, + contract_service: ContractInteractionService + ): + self.session = session + self.contract_service = contract_service + + async def register_member(self, request: MemberCreate) -> DAOMember: + existing = self.session.exec( + select(DAOMember).where(DAOMember.wallet_address == request.wallet_address) + ).first() + + if existing: + # Update stake + existing.staked_amount += request.staked_amount + existing.voting_power = existing.staked_amount # 1:1 mapping for simplicity + self.session.commit() + self.session.refresh(existing) + return existing + + member = DAOMember( + wallet_address=request.wallet_address, + staked_amount=request.staked_amount, + voting_power=request.staked_amount + ) + + self.session.add(member) + self.session.commit() + self.session.refresh(member) + return member + + async def create_proposal(self, request: ProposalCreate) -> DAOProposal: + proposer = self.session.exec( + select(DAOMember).where(DAOMember.wallet_address == request.proposer_address) + ).first() + + if not proposer: + raise HTTPException(status_code=404, detail="Proposer not found") + + if request.target_region and not (proposer.is_council_member and proposer.council_region == request.target_region): + raise HTTPException(status_code=403, detail="Only regional council members can create regional proposals") + + start_time = datetime.utcnow() + end_time = start_time + timedelta(days=request.voting_period_days) + + proposal = DAOProposal( + proposer_address=request.proposer_address, + title=request.title, + description=request.description, + proposal_type=request.proposal_type, + target_region=request.target_region, + execution_payload=request.execution_payload, + start_time=start_time, + end_time=end_time, + status=ProposalState.ACTIVE + ) + + self.session.add(proposal) + self.session.commit() + self.session.refresh(proposal) + + logger.info(f"Created proposal {proposal.id} by {request.proposer_address}") + return proposal + + async def cast_vote(self, request: VoteCreate) -> Vote: + member = self.session.exec( + select(DAOMember).where(DAOMember.wallet_address == request.member_address) + ).first() + + if not member: + raise HTTPException(status_code=404, detail="Member not found") + + proposal = self.session.get(DAOProposal, request.proposal_id) + + if not proposal: + raise HTTPException(status_code=404, detail="Proposal not found") + + if proposal.status != ProposalState.ACTIVE: + raise HTTPException(status_code=400, detail="Proposal is not active") + + now = datetime.utcnow() + if now < proposal.start_time or now > proposal.end_time: + proposal.status = ProposalState.EXPIRED + self.session.commit() + raise HTTPException(status_code=400, detail="Voting period has ended") + + existing_vote = self.session.exec( + select(Vote).where( + Vote.proposal_id == request.proposal_id, + Vote.member_id == member.id + ) + ).first() + + if existing_vote: + raise HTTPException(status_code=400, detail="Member has already voted on this proposal") + + weight = member.voting_power + if proposal.target_region: + # Regional proposals use 1-member-1-vote council weighting + if not member.is_council_member or member.council_region != proposal.target_region: + raise HTTPException(status_code=403, detail="Not a member of the target regional council") + weight = 1.0 + + vote = Vote( + proposal_id=proposal.id, + member_id=member.id, + support=request.support, + weight=weight, + tx_hash="0x_mock_vote_tx" + ) + + if request.support: + proposal.for_votes += weight + else: + proposal.against_votes += weight + + self.session.add(vote) + self.session.commit() + self.session.refresh(vote) + + logger.info(f"Vote cast on {proposal.id} by {member.wallet_address}") + return vote + + async def execute_proposal(self, proposal_id: str) -> DAOProposal: + proposal = self.session.get(DAOProposal, proposal_id) + if not proposal: + raise HTTPException(status_code=404, detail="Proposal not found") + + if proposal.status != ProposalState.ACTIVE: + raise HTTPException(status_code=400, detail=f"Cannot execute proposal in state {proposal.status}") + + if datetime.utcnow() <= proposal.end_time: + raise HTTPException(status_code=400, detail="Voting period has not ended yet") + + if proposal.for_votes > proposal.against_votes: + proposal.status = ProposalState.EXECUTED + logger.info(f"Proposal {proposal_id} SUCCEEDED and EXECUTED.") + + # Handle specific proposal types + if proposal.proposal_type == ProposalType.GRANT: + amount = float(proposal.execution_payload.get("amount", 0)) + recipient = proposal.execution_payload.get("recipient_address") + if amount > 0 and recipient: + await self.allocate_treasury(AllocationCreate( + proposal_id=proposal.id, + amount=amount, + recipient_address=recipient, + purpose=f"Grant for proposal {proposal.title}" + )) + else: + proposal.status = ProposalState.DEFEATED + logger.info(f"Proposal {proposal_id} DEFEATED.") + + self.session.commit() + self.session.refresh(proposal) + return proposal + + async def allocate_treasury(self, request: AllocationCreate) -> TreasuryAllocation: + """Allocate funds from the global treasury""" + allocation = TreasuryAllocation( + proposal_id=request.proposal_id, + amount=request.amount, + token_symbol=request.token_symbol, + recipient_address=request.recipient_address, + purpose=request.purpose, + tx_hash="0x_mock_treasury_tx" + ) + + self.session.add(allocation) + self.session.commit() + self.session.refresh(allocation) + + logger.info(f"Allocated {request.amount} {request.token_symbol} to {request.recipient_address}") + return allocation diff --git a/apps/coordinator-api/src/app/services/developer_platform_service.py b/apps/coordinator-api/src/app/services/developer_platform_service.py new file mode 100644 index 00000000..25885c90 --- /dev/null +++ b/apps/coordinator-api/src/app/services/developer_platform_service.py @@ -0,0 +1,417 @@ +""" +Developer Platform Service + +Service for managing the developer ecosystem, bounties, certifications, and regional hubs. +""" + +from __future__ import annotations + +import logging +from datetime import datetime, timedelta +from typing import List, Optional + +from sqlmodel import Session, select +from fastapi import HTTPException + +from ..domain.developer_platform import ( + DeveloperProfile, DeveloperCertification, RegionalHub, + BountyTask, BountySubmission, BountyStatus, CertificationLevel +) +from ..schemas.developer_platform import ( + DeveloperCreate, BountyCreate, BountySubmissionCreate, CertificationGrant +) +from ..services.blockchain import mint_tokens, get_balance + +logger = logging.getLogger(__name__) + +class DeveloperPlatformService: + def __init__( + self, + session: Session + ): + self.session = session + + async def register_developer(self, request: DeveloperCreate) -> DeveloperProfile: + existing = self.session.exec( + select(DeveloperProfile).where(DeveloperProfile.wallet_address == request.wallet_address) + ).first() + + if existing: + raise HTTPException(status_code=400, detail="Developer profile already exists for this wallet") + + profile = DeveloperProfile( + wallet_address=request.wallet_address, + github_handle=request.github_handle, + email=request.email, + skills=request.skills + ) + + self.session.add(profile) + self.session.commit() + self.session.refresh(profile) + + logger.info(f"Registered new developer: {profile.wallet_address}") + return profile + + async def grant_certification(self, request: CertificationGrant) -> DeveloperCertification: + profile = self.session.get(DeveloperProfile, request.developer_id) + if not profile: + raise HTTPException(status_code=404, detail="Developer profile not found") + + cert = DeveloperCertification( + developer_id=request.developer_id, + certification_name=request.certification_name, + level=request.level, + issued_by=request.issued_by, + ipfs_credential_cid=request.ipfs_credential_cid + ) + + # Boost reputation based on certification level + reputation_boost = { + CertificationLevel.BEGINNER: 10.0, + CertificationLevel.INTERMEDIATE: 25.0, + CertificationLevel.ADVANCED: 50.0, + CertificationLevel.EXPERT: 100.0 + }.get(request.level, 0.0) + + profile.reputation_score += reputation_boost + + self.session.add(cert) + self.session.commit() + self.session.refresh(cert) + + logger.info(f"Granted {request.certification_name} certification to developer {profile.wallet_address}") + return cert + + async def create_bounty(self, request: BountyCreate) -> BountyTask: + bounty = BountyTask( + title=request.title, + description=request.description, + required_skills=request.required_skills, + difficulty_level=request.difficulty_level, + reward_amount=request.reward_amount, + creator_address=request.creator_address, + deadline=request.deadline + ) + + self.session.add(bounty) + self.session.commit() + self.session.refresh(bounty) + + # In a real system, this would interact with a smart contract to lock the reward funds + logger.info(f"Created bounty task: {bounty.title}") + return bounty + + async def submit_bounty(self, bounty_id: str, request: BountySubmissionCreate) -> BountySubmission: + bounty = self.session.get(BountyTask, bounty_id) + if not bounty: + raise HTTPException(status_code=404, detail="Bounty not found") + + if bounty.status != BountyStatus.OPEN and bounty.status != BountyStatus.IN_PROGRESS: + raise HTTPException(status_code=400, detail="Bounty is not open for submissions") + + developer = self.session.get(DeveloperProfile, request.developer_id) + if not developer: + raise HTTPException(status_code=404, detail="Developer not found") + + # Basic skill check (optional enforcement) + has_skills = any(skill in developer.skills for skill in bounty.required_skills) + if not has_skills and bounty.required_skills: + logger.warning(f"Developer {developer.wallet_address} submitted for bounty without required skills") + + submission = BountySubmission( + bounty_id=bounty_id, + developer_id=request.developer_id, + github_pr_url=request.github_pr_url, + submission_notes=request.submission_notes + ) + + bounty.status = BountyStatus.IN_REVIEW + + self.session.add(submission) + self.session.commit() + self.session.refresh(submission) + + logger.info(f"Submission received for bounty {bounty_id} from developer {request.developer_id}") + return submission + + async def approve_submission(self, submission_id: str, reviewer_address: str, review_notes: str) -> BountySubmission: + """Approve a submission and trigger reward payout""" + submission = self.session.get(BountySubmission, submission_id) + if not submission: + raise HTTPException(status_code=404, detail="Submission not found") + + if submission.is_approved: + raise HTTPException(status_code=400, detail="Submission is already approved") + + bounty = submission.bounty + developer = submission.developer + + submission.is_approved = True + submission.review_notes = review_notes + submission.reviewer_address = reviewer_address + submission.reviewed_at = datetime.utcnow() + + bounty.status = BountyStatus.COMPLETED + bounty.assigned_developer_id = developer.id + + # Trigger reward payout + # This would interface with the Multi-chain reward distribution protocol + # tx_hash = await self.contract_service.distribute_bounty_reward(...) + tx_hash = "0x" + "mock_tx_hash_" + submission_id[:10] + submission.tx_hash_reward = tx_hash + + # Update developer stats + developer.total_earned_aitbc += bounty.reward_amount + developer.reputation_score += 5.0 # Base reputation bump for completing a bounty + + self.session.commit() + self.session.refresh(submission) + + logger.info(f"Approved submission {submission_id}, paid {bounty.reward_amount} to {developer.wallet_address}") + return submission + + async def get_developer_profile(self, wallet_address: str) -> Optional[DeveloperProfile]: + """Get developer profile by wallet address""" + return self.session.exec( + select(DeveloperProfile).where(DeveloperProfile.wallet_address == wallet_address) + ).first() + + async def update_developer_profile(self, wallet_address: str, updates: dict) -> DeveloperProfile: + """Update developer profile""" + profile = await self.get_developer_profile(wallet_address) + if not profile: + raise HTTPException(status_code=404, detail="Developer profile not found") + + for key, value in updates.items(): + if hasattr(profile, key): + setattr(profile, key, value) + + profile.updated_at = datetime.utcnow() + self.session.commit() + self.session.refresh(profile) + + return profile + + async def get_leaderboard(self, limit: int = 100, offset: int = 0) -> List[DeveloperProfile]: + """Get developer leaderboard sorted by reputation score""" + return self.session.exec( + select(DeveloperProfile) + .where(DeveloperProfile.is_active == True) + .order_by(DeveloperProfile.reputation_score.desc()) + .offset(offset) + .limit(limit) + ).all() + + async def get_developer_stats(self, wallet_address: str) -> dict: + """Get comprehensive developer statistics""" + profile = await self.get_developer_profile(wallet_address) + if not profile: + raise HTTPException(status_code=404, detail="Developer profile not found") + + # Get bounty statistics + completed_bounties = self.session.exec( + select(BountySubmission).where( + BountySubmission.developer_id == profile.id, + BountySubmission.is_approved == True + ) + ).all() + + # Get certification statistics + certifications = self.session.exec( + select(DeveloperCertification).where(DeveloperCertification.developer_id == profile.id) + ).all() + + return { + "wallet_address": profile.wallet_address, + "reputation_score": profile.reputation_score, + "total_earned_aitbc": profile.total_earned_aitbc, + "completed_bounties": len(completed_bounties), + "certifications_count": len(certifications), + "skills": profile.skills, + "github_handle": profile.github_handle, + "joined_at": profile.created_at.isoformat(), + "last_updated": profile.updated_at.isoformat() + } + + async def list_bounties(self, status: Optional[BountyStatus] = None, limit: int = 100, offset: int = 0) -> List[BountyTask]: + """List bounty tasks with optional status filter""" + query = select(BountyTask) + if status: + query = query.where(BountyTask.status == status) + + return self.session.exec( + query.order_by(BountyTask.created_at.desc()) + .offset(offset) + .limit(limit) + ).all() + + async def get_bounty_details(self, bounty_id: str) -> Optional[BountyTask]: + """Get detailed bounty information""" + bounty = self.session.get(BountyTask, bounty_id) + if not bounty: + raise HTTPException(status_code=404, detail="Bounty not found") + + # Get submissions count + submissions_count = self.session.exec( + select(BountySubmission).where(BountySubmission.bounty_id == bounty_id) + ).count() + + return { + **bounty.__dict__, + "submissions_count": submissions_count + } + + async def get_my_submissions(self, developer_id: str) -> List[BountySubmission]: + """Get all submissions by a developer""" + return self.session.exec( + select(BountySubmission) + .where(BountySubmission.developer_id == developer_id) + .order_by(BountySubmission.submitted_at.desc()) + ).all() + + async def create_regional_hub(self, name: str, region: str, description: str, manager_address: str) -> RegionalHub: + """Create a regional developer hub""" + hub = RegionalHub( + name=name, + region=region, + description=description, + manager_address=manager_address + ) + + self.session.add(hub) + self.session.commit() + self.session.refresh(hub) + + logger.info(f"Created regional hub: {hub.name} in {hub.region}") + return hub + + async def get_regional_hubs(self) -> List[RegionalHub]: + """Get all regional developer hubs""" + return self.session.exec( + select(RegionalHub).where(RegionalHub.is_active == True) + ).all() + + async def get_hub_developers(self, hub_id: str) -> List[DeveloperProfile]: + """Get developers in a regional hub""" + # This would require a junction table in a real implementation + # For now, return developers from the same region + hub = self.session.get(RegionalHub, hub_id) + if not hub: + raise HTTPException(status_code=404, detail="Regional hub not found") + + # Mock implementation - in reality would use hub membership table + return self.session.exec( + select(DeveloperProfile).where(DeveloperProfile.is_active == True) + ).all() + + async def stake_on_developer(self, staker_address: str, developer_address: str, amount: float) -> dict: + """Stake AITBC tokens on a developer""" + # Check staker balance + balance = get_balance(staker_address) + if balance < amount: + raise HTTPException(status_code=400, detail="Insufficient balance for staking") + + # Get developer profile + developer = await self.get_developer_profile(developer_address) + if not developer: + raise HTTPException(status_code=404, detail="Developer not found") + + # In a real implementation, this would interact with staking smart contract + # For now, return mock staking info + staking_info = { + "staker_address": staker_address, + "developer_address": developer_address, + "amount_staked": amount, + "apy": 5.0 + (developer.reputation_score / 100), # Base APY + reputation bonus + "staking_id": f"stake_{staker_address[:8]}_{developer_address[:8]}", + "created_at": datetime.utcnow().isoformat() + } + + logger.info(f"Staked {amount} AITBC on developer {developer_address} by {staker_address}") + return staking_info + + async def get_staking_info(self, address: str) -> dict: + """Get staking information for an address (both as staker and developer)""" + # Mock implementation - would query staking contracts/database + return { + "address": address, + "total_staked_as_staker": 1000.0, + "total_staked_on_me": 5000.0, + "active_stakes": 5, + "total_rewards_earned": 125.5, + "apy_average": 7.5 + } + + async def unstake_tokens(self, staking_id: str, amount: float) -> dict: + """Unstake tokens from a developer""" + # Mock implementation - would interact with staking contract + unstake_info = { + "staking_id": staking_id, + "amount_unstaked": amount, + "rewards_earned": 25.5, + "tx_hash": "0xmock_unstake_tx_hash", + "completed_at": datetime.utcnow().isoformat() + } + + logger.info(f"Unstaked {amount} AITBC from staking position {staking_id}") + return unstake_info + + async def get_rewards(self, address: str) -> dict: + """Get reward information for an address""" + # Mock implementation - would query reward contracts + return { + "address": address, + "pending_rewards": 45.75, + "claimed_rewards": 250.25, + "last_claim_time": (datetime.utcnow() - timedelta(days=7)).isoformat(), + "next_claim_time": (datetime.utcnow() + timedelta(days=1)).isoformat() + } + + async def claim_rewards(self, address: str) -> dict: + """Claim pending rewards""" + # Mock implementation - would interact with reward contract + rewards = await self.get_rewards(address) + + if rewards["pending_rewards"] <= 0: + raise HTTPException(status_code=400, detail="No pending rewards to claim") + + # Mint rewards to address + try: + await mint_tokens(address, rewards["pending_rewards"]) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to mint rewards: {str(e)}") + + claim_info = { + "address": address, + "amount_claimed": rewards["pending_rewards"], + "tx_hash": "0xmock_claim_tx_hash", + "claimed_at": datetime.utcnow().isoformat() + } + + logger.info(f"Claimed {rewards['pending_rewards']} AITBC rewards for {address}") + return claim_info + + async def get_bounty_statistics(self) -> dict: + """Get comprehensive bounty statistics""" + total_bounties = self.session.exec(select(BountyTask)).count() + open_bounties = self.session.exec( + select(BountyTask).where(BountyTask.status == BountyStatus.OPEN) + ).count() + completed_bounties = self.session.exec( + select(BountyTask).where(BountyTask.status == BountyStatus.COMPLETED) + ).count() + + total_rewards = self.session.exec( + select(BountyTask).where(BountyTask.status == BountyStatus.COMPLETED) + ).all() + total_reward_amount = sum(bounty.reward_amount for bounty in total_rewards) + + return { + "total_bounties": total_bounties, + "open_bounties": open_bounties, + "completed_bounties": completed_bounties, + "total_rewards_distributed": total_reward_amount, + "average_reward_per_bounty": total_reward_amount / max(completed_bounties, 1), + "completion_rate": (completed_bounties / max(total_bounties, 1)) * 100 + } diff --git a/apps/coordinator-api/tests/test_dao_governance.py b/apps/coordinator-api/tests/test_dao_governance.py new file mode 100644 index 00000000..48c1d628 --- /dev/null +++ b/apps/coordinator-api/tests/test_dao_governance.py @@ -0,0 +1,124 @@ +import pytest +from datetime import datetime, timedelta +from unittest.mock import AsyncMock + +from sqlmodel import Session, create_engine, SQLModel +from sqlmodel.pool import StaticPool +from fastapi import HTTPException + +from app.services.dao_governance_service import DAOGovernanceService +from app.domain.dao_governance import ProposalState, ProposalType +from app.schemas.dao_governance import MemberCreate, ProposalCreate, VoteCreate + +@pytest.fixture +def test_db(): + engine = create_engine( + "sqlite:///:memory:", + connect_args={"check_same_thread": False}, + poolclass=StaticPool, + ) + SQLModel.metadata.create_all(engine) + session = Session(engine) + yield session + session.close() + +@pytest.fixture +def mock_contract_service(): + return AsyncMock() + +@pytest.fixture +def dao_service(test_db, mock_contract_service): + return DAOGovernanceService( + session=test_db, + contract_service=mock_contract_service + ) + +@pytest.mark.asyncio +async def test_register_member(dao_service): + req = MemberCreate(wallet_address="0xDAO1", staked_amount=100.0) + member = await dao_service.register_member(req) + + assert member.wallet_address == "0xDAO1" + assert member.staked_amount == 100.0 + assert member.voting_power == 100.0 + +@pytest.mark.asyncio +async def test_create_proposal(dao_service): + # Register proposer + await dao_service.register_member(MemberCreate(wallet_address="0xDAO1", staked_amount=100.0)) + + req = ProposalCreate( + proposer_address="0xDAO1", + title="Fund new AI model", + description="Allocate 1000 AITBC to train a new model", + proposal_type=ProposalType.GRANT, + execution_payload={"amount": "1000", "recipient_address": "0xDev1"}, + voting_period_days=7 + ) + + proposal = await dao_service.create_proposal(req) + assert proposal.title == "Fund new AI model" + assert proposal.status == ProposalState.ACTIVE + assert proposal.proposal_type == ProposalType.GRANT + +@pytest.mark.asyncio +async def test_cast_vote(dao_service): + await dao_service.register_member(MemberCreate(wallet_address="0xDAO1", staked_amount=100.0)) + await dao_service.register_member(MemberCreate(wallet_address="0xDAO2", staked_amount=50.0)) + + prop_req = ProposalCreate( + proposer_address="0xDAO1", + title="Test Proposal", + description="Testing voting" + ) + proposal = await dao_service.create_proposal(prop_req) + + # Cast vote + vote_req = VoteCreate( + member_address="0xDAO2", + proposal_id=proposal.id, + support=True + ) + vote = await dao_service.cast_vote(vote_req) + + assert vote.support is True + assert vote.weight == 50.0 + + dao_service.session.refresh(proposal) + assert proposal.for_votes == 50.0 + +@pytest.mark.asyncio +async def test_execute_proposal_success(dao_service, test_db): + await dao_service.register_member(MemberCreate(wallet_address="0xDAO1", staked_amount=100.0)) + + prop_req = ProposalCreate( + proposer_address="0xDAO1", + title="Test Grant", + description="Testing grant execution", + proposal_type=ProposalType.GRANT, + execution_payload={"amount": "500", "recipient_address": "0xDev"} + ) + proposal = await dao_service.create_proposal(prop_req) + + await dao_service.cast_vote(VoteCreate( + member_address="0xDAO1", + proposal_id=proposal.id, + support=True + )) + + # Fast forward time to end of voting period + proposal.end_time = datetime.utcnow() - timedelta(seconds=1) + test_db.commit() + + exec_proposal = await dao_service.execute_proposal(proposal.id) + + assert exec_proposal.status == ProposalState.EXECUTED + + # Verify treasury allocation was created + from app.domain.dao_governance import TreasuryAllocation + from sqlmodel import select + allocation = test_db.exec(select(TreasuryAllocation).where(TreasuryAllocation.proposal_id == proposal.id)).first() + + assert allocation is not None + assert allocation.amount == 500.0 + assert allocation.recipient_address == "0xDev" diff --git a/apps/coordinator-api/tests/test_developer_platform.py b/apps/coordinator-api/tests/test_developer_platform.py new file mode 100644 index 00000000..ad79e4f7 --- /dev/null +++ b/apps/coordinator-api/tests/test_developer_platform.py @@ -0,0 +1,110 @@ +import pytest +from unittest.mock import AsyncMock +from datetime import datetime, timedelta + +from sqlmodel import Session, create_engine, SQLModel +from sqlmodel.pool import StaticPool +from fastapi import HTTPException + +from app.services.developer_platform_service import DeveloperPlatformService +from app.domain.developer_platform import BountyStatus, CertificationLevel +from app.schemas.developer_platform import ( + DeveloperCreate, BountyCreate, BountySubmissionCreate, CertificationGrant +) + +@pytest.fixture +def test_db(): + engine = create_engine( + "sqlite:///:memory:", + connect_args={"check_same_thread": False}, + poolclass=StaticPool, + ) + SQLModel.metadata.create_all(engine) + session = Session(engine) + yield session + session.close() + +@pytest.fixture +def mock_contract_service(): + return AsyncMock() + +@pytest.fixture +def dev_service(test_db, mock_contract_service): + return DeveloperPlatformService( + session=test_db, + contract_service=mock_contract_service + ) + +@pytest.mark.asyncio +async def test_register_developer(dev_service): + req = DeveloperCreate( + wallet_address="0xDev1", + github_handle="dev_one", + skills=["python", "solidity"] + ) + + dev = await dev_service.register_developer(req) + + assert dev.wallet_address == "0xDev1" + assert dev.reputation_score == 0.0 + assert "solidity" in dev.skills + +@pytest.mark.asyncio +async def test_grant_certification(dev_service): + dev = await dev_service.register_developer(DeveloperCreate(wallet_address="0xDev1")) + + req = CertificationGrant( + developer_id=dev.id, + certification_name="ZK-Circuit Architect", + level=CertificationLevel.ADVANCED, + issued_by="0xDAOAdmin" + ) + + cert = await dev_service.grant_certification(req) + + assert cert.developer_id == dev.id + assert cert.level == CertificationLevel.ADVANCED + + # Check reputation boost (ADVANCED = +50.0) + dev_service.session.refresh(dev) + assert dev.reputation_score == 50.0 + +@pytest.mark.asyncio +async def test_bounty_lifecycle(dev_service): + # 1. Register Developer + dev = await dev_service.register_developer(DeveloperCreate(wallet_address="0xDev1")) + + # 2. Create Bounty + bounty_req = BountyCreate( + title="Implement Atomic Swap", + description="Write a secure HTLC contract", + reward_amount=1000.0, + creator_address="0xCreator" + ) + bounty = await dev_service.create_bounty(bounty_req) + assert bounty.status == BountyStatus.OPEN + + # 3. Submit Work + sub_req = BountySubmissionCreate( + developer_id=dev.id, + github_pr_url="https://github.com/aitbc/pr/1" + ) + sub = await dev_service.submit_bounty(bounty.id, sub_req) + assert sub.bounty_id == bounty.id + + dev_service.session.refresh(bounty) + assert bounty.status == BountyStatus.IN_REVIEW + + # 4. Approve Submission + appr_sub = await dev_service.approve_submission(sub.id, reviewer_address="0xReviewer", review_notes="Looks great!") + + assert appr_sub.is_approved is True + assert appr_sub.tx_hash_reward is not None + + dev_service.session.refresh(bounty) + dev_service.session.refresh(dev) + + assert bounty.status == BountyStatus.COMPLETED + assert bounty.assigned_developer_id == dev.id + assert dev.total_earned_aitbc == 1000.0 + assert dev.reputation_score == 5.0 # Base bump for finishing a bounty diff --git a/contracts/contracts/DAOGovernance.sol b/contracts/contracts/DAOGovernance.sol new file mode 100644 index 00000000..0b1da906 --- /dev/null +++ b/contracts/contracts/DAOGovernance.sol @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +/** + * @title DAOGovernance + * @dev Multi-jurisdictional DAO framework with regional councils and staking. + */ +contract DAOGovernance is Ownable, ReentrancyGuard { + using SafeERC20 for IERC20; + + IERC20 public governanceToken; + + // Staking Parameters + uint256 public minStakeAmount; + uint256 public unbondingPeriod = 7 days; + + struct Staker { + uint256 amount; + uint256 unbondingAmount; + uint256 unbondingCompleteTime; + uint256 lastStakeTime; + } + + mapping(address => Staker) public stakers; + uint256 public totalStaked; + + // Proposal Parameters + enum ProposalState { Pending, Active, Canceled, Defeated, Succeeded, Queued, Expired, Executed } + + struct Proposal { + uint256 id; + address proposer; + string region; // "" for global + string descriptionHash; + uint256 forVotes; + uint256 againstVotes; + uint256 startTime; + uint256 endTime; + bool executed; + bool canceled; + mapping(address => bool) hasVoted; + } + + uint256 public proposalCount; + mapping(uint256 => Proposal) public proposals; + + // Regional Councils + mapping(string => mapping(address => bool)) public isRegionalCouncilMember; + mapping(string => address[]) public regionalCouncilMembers; + + // Events + event Staked(address indexed user, uint256 amount); + event Unstaked(address indexed user, uint256 amount); + event ProposalCreated(uint256 indexed id, address proposer, string region, string descriptionHash); + event VoteCast(address indexed voter, uint256 indexed proposalId, bool support, uint256 weight); + event ProposalExecuted(uint256 indexed id); + + constructor(address _governanceToken, uint256 _minStakeAmount) { + governanceToken = IERC20(_governanceToken); + minStakeAmount = _minStakeAmount; + } + + // --- Staking --- + + function stake(uint256 _amount) external nonReentrant { + require(_amount > 0, "Cannot stake 0"); + + governanceToken.safeTransferFrom(msg.sender, address(this), _amount); + + stakers[msg.sender].amount += _amount; + stakers[msg.sender].lastStakeTime = block.timestamp; + totalStaked += _amount; + + require(stakers[msg.sender].amount >= minStakeAmount, "Below min stake"); + + emit Staked(msg.sender, _amount); + } + + function initiateUnstake(uint256 _amount) external nonReentrant { + Staker storage staker = stakers[msg.sender]; + require(_amount > 0 && staker.amount >= _amount, "Invalid amount"); + require(staker.unbondingAmount == 0, "Unbonding already in progress"); + + staker.amount -= _amount; + staker.unbondingAmount = _amount; + staker.unbondingCompleteTime = block.timestamp + unbondingPeriod; + totalStaked -= _amount; + } + + function completeUnstake() external nonReentrant { + Staker storage staker = stakers[msg.sender]; + require(staker.unbondingAmount > 0, "Nothing to unstake"); + require(block.timestamp >= staker.unbondingCompleteTime, "Unbonding not complete"); + + uint256 amount = staker.unbondingAmount; + staker.unbondingAmount = 0; + + governanceToken.safeTransfer(msg.sender, amount); + + emit Unstaked(msg.sender, amount); + } + + // --- Proposals & Voting --- + + function createProposal(string calldata _region, string calldata _descriptionHash, uint256 _votingPeriod) external returns (uint256) { + require(stakers[msg.sender].amount >= minStakeAmount, "Must be staked to propose"); + + // If regional, must be a council member + if (bytes(_region).length > 0) { + require(isRegionalCouncilMember[_region][msg.sender], "Not a council member"); + } + + proposalCount++; + Proposal storage p = proposals[proposalCount]; + p.id = proposalCount; + p.proposer = msg.sender; + p.region = _region; + p.descriptionHash = _descriptionHash; + p.startTime = block.timestamp; + p.endTime = block.timestamp + _votingPeriod; + + emit ProposalCreated(p.id, msg.sender, _region, _descriptionHash); + return p.id; + } + + function castVote(uint256 _proposalId, bool _support) external { + Proposal storage p = proposals[_proposalId]; + require(block.timestamp >= p.startTime && block.timestamp <= p.endTime, "Voting closed"); + require(!p.hasVoted[msg.sender], "Already voted"); + + uint256 weight = stakers[msg.sender].amount; + require(weight > 0, "No voting weight"); + + // If regional, must be a council member + if (bytes(p.region).length > 0) { + require(isRegionalCouncilMember[p.region][msg.sender], "Not a council member"); + weight = 1; // 1 member = 1 vote in council + } + + p.hasVoted[msg.sender] = true; + + if (_support) { + p.forVotes += weight; + } else { + p.againstVotes += weight; + } + + emit VoteCast(msg.sender, _proposalId, _support, weight); + } + + function executeProposal(uint256 _proposalId) external nonReentrant { + Proposal storage p = proposals[_proposalId]; + require(block.timestamp > p.endTime, "Voting not ended"); + require(!p.executed && !p.canceled, "Already executed or canceled"); + require(p.forVotes > p.againstVotes, "Proposal defeated"); + + p.executed = true; + // The actual execution logic (e.g., transferring treasury funds) would happen here + // Usually involves calling other contracts via target[] and callData[] arrays. + + emit ProposalExecuted(_proposalId); + } + + // --- Admin Functions --- + + function setRegionalCouncilMember(string calldata _region, address _member, bool _status) external onlyOwner { + isRegionalCouncilMember[_region][_member] = _status; + if (_status) { + regionalCouncilMembers[_region].push(_member); + } + // Simplified array management for hackathon/demo purposes + } +}