diff --git a/apps/coordinator-api/aitbc/api/v1/settlement.py b/apps/coordinator-api/aitbc/api/v1/settlement.py deleted file mode 100755 index d676f9c4..00000000 --- a/apps/coordinator-api/aitbc/api/v1/settlement.py +++ /dev/null @@ -1,406 +0,0 @@ -""" -API endpoints for cross-chain settlements -""" - -from typing import Dict, Any, Optional, List -from fastapi import APIRouter, HTTPException, Depends, BackgroundTasks -from pydantic import BaseModel, Field -import asyncio - -from ...settlement.hooks import SettlementHook -from ...settlement.manager import BridgeManager -from ...settlement.bridges.base import SettlementResult -from ...auth import get_api_key -from ...models.job import Job - -router = APIRouter(prefix="/settlement", tags=["settlement"]) - - -class CrossChainSettlementRequest(BaseModel): - """Request model for cross-chain settlement""" - job_id: str = Field(..., description="ID of the job to settle") - target_chain_id: int = Field(..., description="Target blockchain chain ID") - bridge_name: Optional[str] = Field(None, description="Specific bridge to use") - priority: str = Field("cost", description="Settlement priority: 'cost' or 'speed'") - privacy_level: Optional[str] = Field(None, description="Privacy level: 'basic' or 'enhanced'") - use_zk_proof: bool = Field(False, description="Use zero-knowledge proof for privacy") - - -class SettlementEstimateRequest(BaseModel): - """Request model for settlement cost estimation""" - job_id: str = Field(..., description="ID of the job") - target_chain_id: int = Field(..., description="Target blockchain chain ID") - bridge_name: Optional[str] = Field(None, description="Specific bridge to use") - - -class BatchSettlementRequest(BaseModel): - """Request model for batch settlement""" - job_ids: List[str] = Field(..., description="List of job IDs to settle") - target_chain_id: int = Field(..., description="Target blockchain chain ID") - bridge_name: Optional[str] = Field(None, description="Specific bridge to use") - - -class SettlementResponse(BaseModel): - """Response model for settlement operations""" - message_id: str = Field(..., description="Settlement message ID") - status: str = Field(..., description="Settlement status") - transaction_hash: Optional[str] = Field(None, description="Transaction hash") - bridge_name: str = Field(..., description="Bridge used") - estimated_completion: Optional[str] = Field(None, description="Estimated completion time") - error_message: Optional[str] = Field(None, description="Error message if failed") - - -class CostEstimateResponse(BaseModel): - """Response model for cost estimates""" - bridge_costs: Dict[str, Dict[str, Any]] = Field(..., description="Costs by bridge") - recommended_bridge: str = Field(..., description="Recommended bridge") - total_estimates: Dict[str, float] = Field(..., description="Min/Max/Average costs") - - -def get_settlement_hook() -> SettlementHook: - """Dependency injection for settlement hook""" - # This would be properly injected in the app setup - from ...main import settlement_hook - return settlement_hook - - -def get_bridge_manager() -> BridgeManager: - """Dependency injection for bridge manager""" - # This would be properly injected in the app setup - from ...main import bridge_manager - return bridge_manager - - -@router.post("/cross-chain", response_model=SettlementResponse) -async def initiate_cross_chain_settlement( - request: CrossChainSettlementRequest, - background_tasks: BackgroundTasks, - settlement_hook: SettlementHook = Depends(get_settlement_hook) -): - """ - Initiate cross-chain settlement for a completed job - - This endpoint settles job receipts and payments across different blockchains - using various bridge protocols (LayerZero, Chainlink CCIP, etc.). - """ - try: - # Validate job exists and is completed - job = await Job.get(request.job_id) - if not job: - raise HTTPException(status_code=404, detail="Job not found") - - if not job.completed: - raise HTTPException(status_code=400, detail="Job is not completed") - - if job.cross_chain_settlement_id: - raise HTTPException( - status_code=409, - detail=f"Job already has settlement {job.cross_chain_settlement_id}" - ) - - # Initiate settlement - settlement_options = {} - if request.use_zk_proof: - settlement_options["privacy_level"] = request.privacy_level or "basic" - settlement_options["use_zk_proof"] = True - - result = await settlement_hook.initiate_manual_settlement( - job_id=request.job_id, - target_chain_id=request.target_chain_id, - bridge_name=request.bridge_name, - options=settlement_options - ) - - # Add background task to monitor settlement - background_tasks.add_task( - monitor_settlement_completion, - result.message_id, - request.job_id - ) - - return SettlementResponse( - message_id=result.message_id, - status=result.status.value, - transaction_hash=result.transaction_hash, - bridge_name=result.transaction_hash and await get_bridge_from_tx(result.transaction_hash), - estimated_completion=estimate_completion_time(result.status), - error_message=result.error_message - ) - - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) - except Exception as e: - raise HTTPException(status_code=500, detail=f"Settlement failed: {str(e)}") - - -@router.get("/{message_id}/status", response_model=SettlementResponse) -async def get_settlement_status( - message_id: str, - settlement_hook: SettlementHook = Depends(get_settlement_hook) -): - """Get the current status of a cross-chain settlement""" - try: - result = await settlement_hook.get_settlement_status(message_id) - - # Get job info if available - job_id = None - if result.transaction_hash: - job_id = await get_job_id_from_settlement(message_id) - - return SettlementResponse( - message_id=message_id, - status=result.status.value, - transaction_hash=result.transaction_hash, - bridge_name=job_id and await get_bridge_from_job(job_id), - estimated_completion=estimate_completion_time(result.status), - error_message=result.error_message - ) - - except ValueError as e: - raise HTTPException(status_code=404, detail=str(e)) - except Exception as e: - raise HTTPException(status_code=500, detail=f"Failed to get status: {str(e)}") - - -@router.post("/estimate-cost", response_model=CostEstimateResponse) -async def estimate_settlement_cost( - request: SettlementEstimateRequest, - settlement_hook: SettlementHook = Depends(get_settlement_hook) -): - """Estimate the cost of cross-chain settlement""" - try: - # Get cost estimates - estimates = await settlement_hook.estimate_settlement_cost( - job_id=request.job_id, - target_chain_id=request.target_chain_id, - bridge_name=request.bridge_name - ) - - # Calculate totals and recommendations - valid_estimates = { - name: cost for name, cost in estimates.items() - if 'error' not in cost - } - - if not valid_estimates: - raise HTTPException( - status_code=400, - detail="No bridges available for this settlement" - ) - - # Find cheapest option - cheapest_bridge = min(valid_estimates.items(), key=lambda x: x[1]['total']) - - # Calculate statistics - costs = [est['total'] for est in valid_estimates.values()] - total_estimates = { - "min": min(costs), - "max": max(costs), - "average": sum(costs) / len(costs) - } - - return CostEstimateResponse( - bridge_costs=estimates, - recommended_bridge=cheapest_bridge[0], - total_estimates=total_estimates - ) - - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) - except Exception as e: - raise HTTPException(status_code=500, detail=f"Estimation failed: {str(e)}") - - -@router.post("/batch", response_model=List[SettlementResponse]) -async def batch_settle( - request: BatchSettlementRequest, - background_tasks: BackgroundTasks, - settlement_hook: SettlementHook = Depends(get_settlement_hook) -): - """Settle multiple jobs in a batch""" - try: - # Validate all jobs exist and are completed - jobs = [] - for job_id in request.job_ids: - job = await Job.get(job_id) - if not job: - raise HTTPException(status_code=404, detail=f"Job {job_id} not found") - if not job.completed: - raise HTTPException( - status_code=400, - detail=f"Job {job_id} is not completed" - ) - jobs.append(job) - - # Process batch settlement - results = [] - for job in jobs: - try: - result = await settlement_hook.initiate_manual_settlement( - job_id=job.id, - target_chain_id=request.target_chain_id, - bridge_name=request.bridge_name - ) - - # Add monitoring task - background_tasks.add_task( - monitor_settlement_completion, - result.message_id, - job.id - ) - - results.append(SettlementResponse( - message_id=result.message_id, - status=result.status.value, - transaction_hash=result.transaction_hash, - bridge_name=result.transaction_hash and await get_bridge_from_tx(result.transaction_hash), - estimated_completion=estimate_completion_time(result.status), - error_message=result.error_message - )) - - except Exception as e: - results.append(SettlementResponse( - message_id="", - status="failed", - transaction_hash=None, - bridge_name="", - estimated_completion=None, - error_message=str(e) - )) - - return results - - except HTTPException: - raise - except Exception as e: - raise HTTPException(status_code=500, detail=f"Batch settlement failed: {str(e)}") - - -@router.get("/bridges", response_model=Dict[str, Any]) -async def list_supported_bridges( - settlement_hook: SettlementHook = Depends(get_settlement_hook) -): - """List all supported bridges and their capabilities""" - try: - return await settlement_hook.list_supported_bridges() - except Exception as e: - raise HTTPException(status_code=500, detail=f"Failed to list bridges: {str(e)}") - - -@router.get("/chains", response_model=Dict[str, List[int]]) -async def list_supported_chains( - settlement_hook: SettlementHook = Depends(get_settlement_hook) -): - """List all supported chains by bridge""" - try: - return await settlement_hook.list_supported_chains() - except Exception as e: - raise HTTPException(status_code=500, detail=f"Failed to list chains: {str(e)}") - - -@router.post("/{message_id}/refund") -async def refund_settlement( - message_id: str, - bridge_manager: BridgeManager = Depends(get_bridge_manager) -): - """Attempt to refund a failed settlement""" - try: - result = await bridge_manager.refund_failed_settlement(message_id) - - return { - "message_id": message_id, - "status": result.status.value, - "refund_transaction": result.transaction_hash, - "error_message": result.error_message - } - - except ValueError as e: - raise HTTPException(status_code=404, detail=str(e)) - except Exception as e: - raise HTTPException(status_code=500, detail=f"Refund failed: {str(e)}") - - -@router.get("/job/{job_id}/settlements") -async def get_job_settlements( - job_id: str, - bridge_manager: BridgeManager = Depends(get_bridge_manager) -): - """Get all cross-chain settlements for a job""" - try: - # Validate job exists - job = await Job.get(job_id) - if not job: - raise HTTPException(status_code=404, detail="Job not found") - - # Get settlements from storage - settlements = await bridge_manager.storage.get_settlements_by_job(job_id) - - return { - "job_id": job_id, - "settlements": settlements, - "total_count": len(settlements) - } - - except HTTPException: - raise - except Exception as e: - raise HTTPException(status_code=500, detail=f"Failed to get settlements: {str(e)}") - - -# Helper functions -async def monitor_settlement_completion(message_id: str, job_id: str): - """Background task to monitor settlement completion""" - settlement_hook = get_settlement_hook() - - # Monitor for up to 1 hour - max_wait = 3600 - start_time = asyncio.get_event_loop().time() - - while asyncio.get_event_loop().time() - start_time < max_wait: - result = await settlement_hook.get_settlement_status(message_id) - - # Update job status - job = await Job.get(job_id) - if job: - job.cross_chain_settlement_status = result.status.value - await job.save() - - # If completed or failed, stop monitoring - if result.status.value in ['completed', 'failed']: - break - - # Wait before checking again - await asyncio.sleep(30) - - -def estimate_completion_time(status) -> Optional[str]: - """Estimate completion time based on status""" - if status.value == 'completed': - return None - elif status.value == 'pending': - return "5-10 minutes" - elif status.value == 'in_progress': - return "2-5 minutes" - else: - return None - - -async def get_bridge_from_tx(tx_hash: str) -> str: - """Get bridge name from transaction hash""" - # This would look up the bridge from the transaction - # For now, return placeholder - return "layerzero" - - -async def get_bridge_from_job(job_id: str) -> str: - """Get bridge name from job""" - # This would look up the bridge from the job - # For now, return placeholder - return "layerzero" - - -async def get_job_id_from_settlement(message_id: str) -> Optional[str]: - """Get job ID from settlement message ID""" - # This would look up the job ID from storage - # For now, return None - return None diff --git a/apps/coordinator-api/aitbc/logging.py b/apps/coordinator-api/aitbc/logging.py deleted file mode 100755 index b6f4f973..00000000 --- a/apps/coordinator-api/aitbc/logging.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -Logging utilities for AITBC coordinator API -""" - -import logging -import sys -from typing import Optional - -def setup_logger( - name: str, - level: str = "INFO", - format_string: Optional[str] = None -) -> logging.Logger: - """Setup a logger with consistent formatting""" - if format_string is None: - format_string = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" - - logger = logging.getLogger(name) - logger.setLevel(getattr(logging, level.upper())) - - if not logger.handlers: - handler = logging.StreamHandler(sys.stdout) - formatter = logging.Formatter(format_string) - handler.setFormatter(formatter) - logger.addHandler(handler) - - return logger - -def get_logger(name: str) -> logging.Logger: - """Get a logger instance""" - return logging.getLogger(name) diff --git a/apps/coordinator-api/src/app/logging.py b/apps/coordinator-api/src/app/logging.py index f570a3bc..b6f4f973 100755 --- a/apps/coordinator-api/src/app/logging.py +++ b/apps/coordinator-api/src/app/logging.py @@ -1,3 +1,31 @@ -from aitbc.logging import get_logger, setup_logger +""" +Logging utilities for AITBC coordinator API +""" -__all__ = ["get_logger", "setup_logger"] +import logging +import sys +from typing import Optional + +def setup_logger( + name: str, + level: str = "INFO", + format_string: Optional[str] = None +) -> logging.Logger: + """Setup a logger with consistent formatting""" + if format_string is None: + format_string = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + + logger = logging.getLogger(name) + logger.setLevel(getattr(logging, level.upper())) + + if not logger.handlers: + handler = logging.StreamHandler(sys.stdout) + formatter = logging.Formatter(format_string) + handler.setFormatter(formatter) + logger.addHandler(handler) + + return logger + +def get_logger(name: str) -> logging.Logger: + """Get a logger instance""" + return logging.getLogger(name) diff --git a/apps/coordinator-api/src/app/routers/settlement.py b/apps/coordinator-api/src/app/routers/settlement.py new file mode 100644 index 00000000..eac91a97 --- /dev/null +++ b/apps/coordinator-api/src/app/routers/settlement.py @@ -0,0 +1,151 @@ +""" +Settlement router for cross-chain settlements +""" + +from typing import Dict, Any, Optional, List +from fastapi import APIRouter, HTTPException, Depends, BackgroundTasks +from pydantic import BaseModel, Field +import asyncio + +from .settlement.hooks import SettlementHook +from .settlement.manager import BridgeManager +from .settlement.bridges.base import SettlementResult +from ..auth import get_api_key +from ..models.job import Job + +router = APIRouter(prefix="/settlement", tags=["settlement"]) + + +class CrossChainSettlementRequest(BaseModel): + """Request model for cross-chain settlement""" + source_chain_id: str = Field(..., description="Source blockchain ID") + target_chain_id: str = Field(..., description="Target blockchain ID") + amount: float = Field(..., gt=0, description="Amount to settle") + asset_type: str = Field(..., description="Asset type (e.g., 'AITBC', 'ETH')") + recipient_address: str = Field(..., description="Recipient address on target chain") + gas_limit: Optional[int] = Field(None, description="Gas limit for transaction") + gas_price: Optional[float] = Field(None, description="Gas price in Gwei") + + +class CrossChainSettlementResponse(BaseModel): + """Response model for cross-chain settlement""" + settlement_id: str = Field(..., description="Unique settlement identifier") + status: str = Field(..., description="Settlement status") + transaction_hash: Optional[str] = Field(None, description="Transaction hash on target chain") + estimated_completion: Optional[str] = Field(None, description="Estimated completion time") + created_at: str = Field(..., description="Creation timestamp") + + +@router.post("/cross-chain", response_model=CrossChainSettlementResponse) +async def initiate_cross_chain_settlement( + request: CrossChainSettlementRequest, + background_tasks: BackgroundTasks, + api_key: str = Depends(get_api_key) +): + """Initiate a cross-chain settlement""" + try: + # Initialize settlement manager + manager = BridgeManager() + + # Create settlement + settlement_id = await manager.create_settlement( + source_chain_id=request.source_chain_id, + target_chain_id=request.target_chain_id, + amount=request.amount, + asset_type=request.asset_type, + recipient_address=request.recipient_address, + gas_limit=request.gas_limit, + gas_price=request.gas_price + ) + + # Add background task to process settlement + background_tasks.add_task( + manager.process_settlement, + settlement_id, + api_key + ) + + return CrossChainSettlementResponse( + settlement_id=settlement_id, + status="pending", + estimated_completion="~5 minutes", + created_at=asyncio.get_event_loop().time() + ) + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Settlement failed: {str(e)}") + + +@router.get("/cross-chain/{settlement_id}") +async def get_settlement_status( + settlement_id: str, + api_key: str = Depends(get_api_key) +): + """Get settlement status""" + try: + manager = BridgeManager() + settlement = await manager.get_settlement(settlement_id) + + if not settlement: + raise HTTPException(status_code=404, detail="Settlement not found") + + return { + "settlement_id": settlement.id, + "status": settlement.status, + "transaction_hash": settlement.tx_hash, + "created_at": settlement.created_at, + "completed_at": settlement.completed_at, + "error_message": settlement.error_message + } + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to get settlement: {str(e)}") + + +@router.get("/cross-chain") +async def list_settlements( + api_key: str = Depends(get_api_key), + limit: int = 50, + offset: int = 0 +): + """List settlements with pagination""" + try: + manager = BridgeManager() + settlements = await manager.list_settlements( + api_key=api_key, + limit=limit, + offset=offset + ) + + return { + "settlements": settlements, + "total": len(settlements), + "limit": limit, + "offset": offset + } + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to list settlements: {str(e)}") + + +@router.delete("/cross-chain/{settlement_id}") +async def cancel_settlement( + settlement_id: str, + api_key: str = Depends(get_api_key) +): + """Cancel a pending settlement""" + try: + manager = BridgeManager() + success = await manager.cancel_settlement(settlement_id, api_key) + + if not success: + raise HTTPException(status_code=400, detail="Cannot cancel settlement") + + return {"message": "Settlement cancelled successfully"} + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to cancel settlement: {str(e)}") diff --git a/apps/coordinator-api/aitbc/settlement/__init__.py b/apps/coordinator-api/src/app/settlement/__init__.py similarity index 100% rename from apps/coordinator-api/aitbc/settlement/__init__.py rename to apps/coordinator-api/src/app/settlement/__init__.py diff --git a/apps/coordinator-api/aitbc/settlement/bridges/__init__.py b/apps/coordinator-api/src/app/settlement/bridges/__init__.py similarity index 100% rename from apps/coordinator-api/aitbc/settlement/bridges/__init__.py rename to apps/coordinator-api/src/app/settlement/bridges/__init__.py diff --git a/apps/coordinator-api/aitbc/settlement/bridges/base.py b/apps/coordinator-api/src/app/settlement/bridges/base.py similarity index 100% rename from apps/coordinator-api/aitbc/settlement/bridges/base.py rename to apps/coordinator-api/src/app/settlement/bridges/base.py diff --git a/apps/coordinator-api/aitbc/settlement/bridges/layerzero.py b/apps/coordinator-api/src/app/settlement/bridges/layerzero.py similarity index 100% rename from apps/coordinator-api/aitbc/settlement/bridges/layerzero.py rename to apps/coordinator-api/src/app/settlement/bridges/layerzero.py diff --git a/apps/coordinator-api/aitbc/settlement/hooks.py b/apps/coordinator-api/src/app/settlement/hooks.py similarity index 100% rename from apps/coordinator-api/aitbc/settlement/hooks.py rename to apps/coordinator-api/src/app/settlement/hooks.py diff --git a/apps/coordinator-api/aitbc/settlement/manager.py b/apps/coordinator-api/src/app/settlement/manager.py similarity index 100% rename from apps/coordinator-api/aitbc/settlement/manager.py rename to apps/coordinator-api/src/app/settlement/manager.py diff --git a/apps/coordinator-api/aitbc/settlement/storage.py b/apps/coordinator-api/src/app/settlement/storage.py similarity index 100% rename from apps/coordinator-api/aitbc/settlement/storage.py rename to apps/coordinator-api/src/app/settlement/storage.py diff --git a/packages/py/aitbc-core/src/aitbc/__init__.py b/packages/py/aitbc-core/src/__init__.py similarity index 100% rename from packages/py/aitbc-core/src/aitbc/__init__.py rename to packages/py/aitbc-core/src/__init__.py diff --git a/packages/py/aitbc-core/src/aitbc/logging/__init__.py b/packages/py/aitbc-core/src/logging/__init__.py similarity index 100% rename from packages/py/aitbc-core/src/aitbc/logging/__init__.py rename to packages/py/aitbc-core/src/logging/__init__.py