feat: complete phase 3 developer ecosystem and dao governance
This commit is contained in:
114
apps/coordinator-api/src/app/domain/dao_governance.py
Normal file
114
apps/coordinator-api/src/app/domain/dao_governance.py
Normal 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)
|
||||
136
apps/coordinator-api/src/app/domain/developer_platform.py
Normal file
136
apps/coordinator-api/src/app/domain/developer_platform.py
Normal 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")
|
||||
29
apps/coordinator-api/src/app/schemas/dao_governance.py
Normal file
29
apps/coordinator-api/src/app/schemas/dao_governance.py
Normal 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
|
||||
31
apps/coordinator-api/src/app/schemas/developer_platform.py
Normal file
31
apps/coordinator-api/src/app/schemas/developer_platform.py
Normal 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
|
||||
202
apps/coordinator-api/src/app/services/dao_governance_service.py
Normal file
202
apps/coordinator-api/src/app/services/dao_governance_service.py
Normal 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
|
||||
@@ -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
|
||||
}
|
||||
124
apps/coordinator-api/tests/test_dao_governance.py
Normal file
124
apps/coordinator-api/tests/test_dao_governance.py
Normal 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"
|
||||
110
apps/coordinator-api/tests/test_developer_platform.py
Normal file
110
apps/coordinator-api/tests/test_developer_platform.py
Normal 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
|
||||
Reference in New Issue
Block a user