feat: complete phase 3 developer ecosystem and dao governance

This commit is contained in:
oib
2026-02-28 23:24:26 +01:00
parent 5bc18d684c
commit d8a432ce33
9 changed files with 1341 additions and 0 deletions

View File

@@ -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)

View File

@@ -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")

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

View File

@@ -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"

View File

@@ -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

View File

@@ -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
}
}