Files
aitbc/apps/blockchain-node/src/aitbc_chain/contracts/escrow.py
aitbc c876b0aa20 feat: implement AITBC mesh network deployment infrastructure
 Phase 0: Pre-implementation checklist completed
- Environment configurations (dev/staging/production)
- Directory structure setup (logs, backups, monitoring)
- Virtual environment with dependencies

 Master deployment script created
- Single command deployment with validation
- Progress tracking and rollback capability
- Health checks and deployment reporting

 Validation script created
- Module import validation
- Basic functionality testing
- Configuration and script verification

 Implementation fixes
- Fixed dataclass import in consensus keys
- Fixed async function syntax in tests
- Updated deployment script for virtual environment

🚀 Ready for deployment: ./scripts/deploy-mesh-network.sh dev
2026-04-02 12:08:15 +02:00

560 lines
22 KiB
Python

"""
Smart Contract Escrow System
Handles automated payment holding and release for AI job marketplace
"""
import asyncio
import time
import json
from typing import Dict, List, Optional, Tuple, Set
from dataclasses import dataclass, asdict
from enum import Enum
from decimal import Decimal
class EscrowState(Enum):
CREATED = "created"
FUNDED = "funded"
JOB_STARTED = "job_started"
JOB_COMPLETED = "job_completed"
DISPUTED = "disputed"
RESOLVED = "resolved"
RELEASED = "released"
REFUNDED = "refunded"
EXPIRED = "expired"
class DisputeReason(Enum):
QUALITY_ISSUES = "quality_issues"
DELIVERY_LATE = "delivery_late"
INCOMPLETE_WORK = "incomplete_work"
TECHNICAL_ISSUES = "technical_issues"
PAYMENT_DISPUTE = "payment_dispute"
OTHER = "other"
@dataclass
class EscrowContract:
contract_id: str
job_id: str
client_address: str
agent_address: str
amount: Decimal
fee_rate: Decimal # Platform fee rate
created_at: float
expires_at: float
state: EscrowState
milestones: List[Dict]
current_milestone: int
dispute_reason: Optional[DisputeReason]
dispute_evidence: List[Dict]
resolution: Optional[Dict]
released_amount: Decimal
refunded_amount: Decimal
@dataclass
class Milestone:
milestone_id: str
description: str
amount: Decimal
completed: bool
completed_at: Optional[float]
verified: bool
class EscrowManager:
"""Manages escrow contracts for AI job marketplace"""
def __init__(self):
self.escrow_contracts: Dict[str, EscrowContract] = {}
self.active_contracts: Set[str] = set()
self.disputed_contracts: Set[str] = set()
# Escrow parameters
self.default_fee_rate = Decimal('0.025') # 2.5% platform fee
self.max_contract_duration = 86400 * 30 # 30 days
self.dispute_timeout = 86400 * 7 # 7 days for dispute resolution
self.min_dispute_evidence = 1
self.max_dispute_evidence = 10
# Milestone parameters
self.min_milestone_amount = Decimal('0.01')
self.max_milestones = 10
self.verification_timeout = 86400 # 24 hours for milestone verification
async def create_contract(self, job_id: str, client_address: str, agent_address: str,
amount: Decimal, fee_rate: Optional[Decimal] = None,
milestones: Optional[List[Dict]] = None,
duration_days: int = 30) -> Tuple[bool, str, Optional[str]]:
"""Create new escrow contract"""
try:
# Validate inputs
if not self._validate_contract_inputs(job_id, client_address, agent_address, amount):
return False, "Invalid contract inputs", None
# Calculate fee
fee_rate = fee_rate or self.default_fee_rate
platform_fee = amount * fee_rate
total_amount = amount + platform_fee
# Validate milestones
validated_milestones = []
if milestones:
validated_milestones = await self._validate_milestones(milestones, amount)
if not validated_milestones:
return False, "Invalid milestones configuration", None
else:
# Create single milestone for full amount
validated_milestones = [{
'milestone_id': 'milestone_1',
'description': 'Complete job',
'amount': amount,
'completed': False
}]
# Create contract
contract_id = self._generate_contract_id(client_address, agent_address, job_id)
current_time = time.time()
contract = EscrowContract(
contract_id=contract_id,
job_id=job_id,
client_address=client_address,
agent_address=agent_address,
amount=total_amount,
fee_rate=fee_rate,
created_at=current_time,
expires_at=current_time + (duration_days * 86400),
state=EscrowState.CREATED,
milestones=validated_milestones,
current_milestone=0,
dispute_reason=None,
dispute_evidence=[],
resolution=None,
released_amount=Decimal('0'),
refunded_amount=Decimal('0')
)
self.escrow_contracts[contract_id] = contract
log_info(f"Escrow contract created: {contract_id} for job {job_id}")
return True, "Contract created successfully", contract_id
except Exception as e:
return False, f"Contract creation failed: {str(e)}", None
def _validate_contract_inputs(self, job_id: str, client_address: str,
agent_address: str, amount: Decimal) -> bool:
"""Validate contract creation inputs"""
if not all([job_id, client_address, agent_address]):
return False
# Validate addresses (simplified)
if not (client_address.startswith('0x') and len(client_address) == 42):
return False
if not (agent_address.startswith('0x') and len(agent_address) == 42):
return False
# Validate amount
if amount <= 0:
return False
# Check for existing contract
for contract in self.escrow_contracts.values():
if contract.job_id == job_id:
return False # Contract already exists for this job
return True
async def _validate_milestones(self, milestones: List[Dict], total_amount: Decimal) -> Optional[List[Dict]]:
"""Validate milestone configuration"""
if not milestones or len(milestones) > self.max_milestones:
return None
validated_milestones = []
milestone_total = Decimal('0')
for i, milestone_data in enumerate(milestones):
# Validate required fields
required_fields = ['milestone_id', 'description', 'amount']
if not all(field in milestone_data for field in required_fields):
return None
# Validate amount
amount = Decimal(str(milestone_data['amount']))
if amount < self.min_milestone_amount:
return None
milestone_total += amount
validated_milestones.append({
'milestone_id': milestone_data['milestone_id'],
'description': milestone_data['description'],
'amount': amount,
'completed': False
})
# Check if milestone amounts sum to total
if abs(milestone_total - total_amount) > Decimal('0.01'): # Allow small rounding difference
return None
return validated_milestones
def _generate_contract_id(self, client_address: str, agent_address: str, job_id: str) -> str:
"""Generate unique contract ID"""
import hashlib
content = f"{client_address}:{agent_address}:{job_id}:{time.time()}"
return hashlib.sha256(content.encode()).hexdigest()[:16]
async def fund_contract(self, contract_id: str, payment_tx_hash: str) -> Tuple[bool, str]:
"""Fund escrow contract"""
contract = self.escrow_contracts.get(contract_id)
if not contract:
return False, "Contract not found"
if contract.state != EscrowState.CREATED:
return False, f"Cannot fund contract in {contract.state.value} state"
# In real implementation, this would verify the payment transaction
# For now, assume payment is valid
contract.state = EscrowState.FUNDED
self.active_contracts.add(contract_id)
log_info(f"Contract funded: {contract_id}")
return True, "Contract funded successfully"
async def start_job(self, contract_id: str) -> Tuple[bool, str]:
"""Mark job as started"""
contract = self.escrow_contracts.get(contract_id)
if not contract:
return False, "Contract not found"
if contract.state != EscrowState.FUNDED:
return False, f"Cannot start job in {contract.state.value} state"
contract.state = EscrowState.JOB_STARTED
log_info(f"Job started for contract: {contract_id}")
return True, "Job started successfully"
async def complete_milestone(self, contract_id: str, milestone_id: str,
evidence: Dict = None) -> Tuple[bool, str]:
"""Mark milestone as completed"""
contract = self.escrow_contracts.get(contract_id)
if not contract:
return False, "Contract not found"
if contract.state not in [EscrowState.JOB_STARTED, EscrowState.JOB_COMPLETED]:
return False, f"Cannot complete milestone in {contract.state.value} state"
# Find milestone
milestone = None
for ms in contract.milestones:
if ms['milestone_id'] == milestone_id:
milestone = ms
break
if not milestone:
return False, "Milestone not found"
if milestone['completed']:
return False, "Milestone already completed"
# Mark as completed
milestone['completed'] = True
milestone['completed_at'] = time.time()
# Add evidence if provided
if evidence:
milestone['evidence'] = evidence
# Check if all milestones are completed
all_completed = all(ms['completed'] for ms in contract.milestones)
if all_completed:
contract.state = EscrowState.JOB_COMPLETED
log_info(f"Milestone {milestone_id} completed for contract: {contract_id}")
return True, "Milestone completed successfully"
async def verify_milestone(self, contract_id: str, milestone_id: str,
verified: bool, feedback: str = "") -> Tuple[bool, str]:
"""Verify milestone completion"""
contract = self.escrow_contracts.get(contract_id)
if not contract:
return False, "Contract not found"
# Find milestone
milestone = None
for ms in contract.milestones:
if ms['milestone_id'] == milestone_id:
milestone = ms
break
if not milestone:
return False, "Milestone not found"
if not milestone['completed']:
return False, "Milestone not completed yet"
# Set verification status
milestone['verified'] = verified
milestone['verification_feedback'] = feedback
if verified:
# Release milestone payment
await self._release_milestone_payment(contract_id, milestone_id)
else:
# Create dispute if verification fails
await self._create_dispute(contract_id, DisputeReason.QUALITY_ISSUES,
f"Milestone {milestone_id} verification failed: {feedback}")
log_info(f"Milestone {milestone_id} verification: {verified} for contract: {contract_id}")
return True, "Milestone verification processed"
async def _release_milestone_payment(self, contract_id: str, milestone_id: str):
"""Release payment for verified milestone"""
contract = self.escrow_contracts.get(contract_id)
if not contract:
return
# Find milestone
milestone = None
for ms in contract.milestones:
if ms['milestone_id'] == milestone_id:
milestone = ms
break
if not milestone:
return
# Calculate payment amount (minus platform fee)
milestone_amount = Decimal(str(milestone['amount']))
platform_fee = milestone_amount * contract.fee_rate
payment_amount = milestone_amount - platform_fee
# Update released amount
contract.released_amount += payment_amount
# In real implementation, this would trigger actual payment transfer
log_info(f"Released {payment_amount} for milestone {milestone_id} in contract {contract_id}")
async def release_full_payment(self, contract_id: str) -> Tuple[bool, str]:
"""Release full payment to agent"""
contract = self.escrow_contracts.get(contract_id)
if not contract:
return False, "Contract not found"
if contract.state != EscrowState.JOB_COMPLETED:
return False, f"Cannot release payment in {contract.state.value} state"
# Check if all milestones are verified
all_verified = all(ms.get('verified', False) for ms in contract.milestones)
if not all_verified:
return False, "Not all milestones are verified"
# Calculate remaining payment
total_milestone_amount = sum(Decimal(str(ms['amount'])) for ms in contract.milestones)
platform_fee_total = total_milestone_amount * contract.fee_rate
remaining_payment = total_milestone_amount - contract.released_amount - platform_fee_total
if remaining_payment > 0:
contract.released_amount += remaining_payment
contract.state = EscrowState.RELEASED
self.active_contracts.discard(contract_id)
log_info(f"Full payment released for contract: {contract_id}")
return True, "Payment released successfully"
async def create_dispute(self, contract_id: str, reason: DisputeReason,
description: str, evidence: List[Dict] = None) -> Tuple[bool, str]:
"""Create dispute for contract"""
return await self._create_dispute(contract_id, reason, description, evidence)
async def _create_dispute(self, contract_id: str, reason: DisputeReason,
description: str, evidence: List[Dict] = None):
"""Internal dispute creation method"""
contract = self.escrow_contracts.get(contract_id)
if not contract:
return False, "Contract not found"
if contract.state == EscrowState.DISPUTED:
return False, "Contract already disputed"
if contract.state not in [EscrowState.FUNDED, EscrowState.JOB_STARTED, EscrowState.JOB_COMPLETED]:
return False, f"Cannot dispute contract in {contract.state.value} state"
# Validate evidence
if evidence and (len(evidence) < self.min_dispute_evidence or len(evidence) > self.max_dispute_evidence):
return False, f"Invalid evidence count: {len(evidence)}"
# Create dispute
contract.state = EscrowState.DISPUTED
contract.dispute_reason = reason
contract.dispute_evidence = evidence or []
contract.dispute_created_at = time.time()
self.disputed_contracts.add(contract_id)
log_info(f"Dispute created for contract: {contract_id} - {reason.value}")
return True, "Dispute created successfully"
async def resolve_dispute(self, contract_id: str, resolution: Dict) -> Tuple[bool, str]:
"""Resolve dispute with specified outcome"""
contract = self.escrow_contracts.get(contract_id)
if not contract:
return False, "Contract not found"
if contract.state != EscrowState.DISPUTED:
return False, f"Contract not in disputed state: {contract.state.value}"
# Validate resolution
required_fields = ['winner', 'client_refund', 'agent_payment']
if not all(field in resolution for field in required_fields):
return False, "Invalid resolution format"
winner = resolution['winner']
client_refund = Decimal(str(resolution['client_refund']))
agent_payment = Decimal(str(resolution['agent_payment']))
# Validate amounts
total_refund = client_refund + agent_payment
if total_refund > contract.amount:
return False, "Refund amounts exceed contract amount"
# Apply resolution
contract.resolution = resolution
contract.state = EscrowState.RESOLVED
# Update amounts
contract.released_amount += agent_payment
contract.refunded_amount += client_refund
# Remove from disputed contracts
self.disputed_contracts.discard(contract_id)
self.active_contracts.discard(contract_id)
log_info(f"Dispute resolved for contract: {contract_id} - Winner: {winner}")
return True, "Dispute resolved successfully"
async def refund_contract(self, contract_id: str, reason: str = "") -> Tuple[bool, str]:
"""Refund contract to client"""
contract = self.escrow_contracts.get(contract_id)
if not contract:
return False, "Contract not found"
if contract.state in [EscrowState.RELEASED, EscrowState.REFUNDED, EscrowState.EXPIRED]:
return False, f"Cannot refund contract in {contract.state.value} state"
# Calculate refund amount (minus any released payments)
refund_amount = contract.amount - contract.released_amount
if refund_amount <= 0:
return False, "No amount available for refund"
contract.state = EscrowState.REFUNDED
contract.refunded_amount = refund_amount
self.active_contracts.discard(contract_id)
self.disputed_contracts.discard(contract_id)
log_info(f"Contract refunded: {contract_id} - Amount: {refund_amount}")
return True, "Contract refunded successfully"
async def expire_contract(self, contract_id: str) -> Tuple[bool, str]:
"""Mark contract as expired"""
contract = self.escrow_contracts.get(contract_id)
if not contract:
return False, "Contract not found"
if time.time() < contract.expires_at:
return False, "Contract has not expired yet"
if contract.state in [EscrowState.RELEASED, EscrowState.REFUNDED, EscrowState.EXPIRED]:
return False, f"Contract already in final state: {contract.state.value}"
# Auto-refund if no work has been done
if contract.state == EscrowState.FUNDED:
return await self.refund_contract(contract_id, "Contract expired")
# Handle other states based on work completion
contract.state = EscrowState.EXPIRED
self.active_contracts.discard(contract_id)
self.disputed_contracts.discard(contract_id)
log_info(f"Contract expired: {contract_id}")
return True, "Contract expired successfully"
async def get_contract_info(self, contract_id: str) -> Optional[EscrowContract]:
"""Get contract information"""
return self.escrow_contracts.get(contract_id)
async def get_contracts_by_client(self, client_address: str) -> List[EscrowContract]:
"""Get contracts for specific client"""
return [
contract for contract in self.escrow_contracts.values()
if contract.client_address == client_address
]
async def get_contracts_by_agent(self, agent_address: str) -> List[EscrowContract]:
"""Get contracts for specific agent"""
return [
contract for contract in self.escrow_contracts.values()
if contract.agent_address == agent_address
]
async def get_active_contracts(self) -> List[EscrowContract]:
"""Get all active contracts"""
return [
self.escrow_contracts[contract_id]
for contract_id in self.active_contracts
if contract_id in self.escrow_contracts
]
async def get_disputed_contracts(self) -> List[EscrowContract]:
"""Get all disputed contracts"""
return [
self.escrow_contracts[contract_id]
for contract_id in self.disputed_contracts
if contract_id in self.escrow_contracts
]
async def get_escrow_statistics(self) -> Dict:
"""Get escrow system statistics"""
total_contracts = len(self.escrow_contracts)
active_count = len(self.active_contracts)
disputed_count = len(self.disputed_contracts)
# State distribution
state_counts = {}
for contract in self.escrow_contracts.values():
state = contract.state.value
state_counts[state] = state_counts.get(state, 0) + 1
# Financial statistics
total_amount = sum(contract.amount for contract in self.escrow_contracts.values())
total_released = sum(contract.released_amount for contract in self.escrow_contracts.values())
total_refunded = sum(contract.refunded_amount for contract in self.escrow_contracts.values())
total_fees = total_amount - total_released - total_refunded
return {
'total_contracts': total_contracts,
'active_contracts': active_count,
'disputed_contracts': disputed_count,
'state_distribution': state_counts,
'total_amount': float(total_amount),
'total_released': float(total_released),
'total_refunded': float(total_refunded),
'total_fees': float(total_fees),
'average_contract_value': float(total_amount / total_contracts) if total_contracts > 0 else 0
}
# Global escrow manager
escrow_manager: Optional[EscrowManager] = None
def get_escrow_manager() -> Optional[EscrowManager]:
"""Get global escrow manager"""
return escrow_manager
def create_escrow_manager() -> EscrowManager:
"""Create and set global escrow manager"""
global escrow_manager
escrow_manager = EscrowManager()
return escrow_manager