feat: add marketplace metrics, privacy features, and service registry endpoints
- Add Prometheus metrics for marketplace API throughput and error rates with new dashboard panels - Implement confidential transaction models with encryption support and access control - Add key management system with registration, rotation, and audit logging - Create services and registry routers for service discovery and management - Integrate ZK proof generation for privacy-preserving receipts - Add metrics instru
This commit is contained in:
406
apps/coordinator-api/aitbc/api/v1/settlement.py
Normal file
406
apps/coordinator-api/aitbc/api/v1/settlement.py
Normal file
@@ -0,0 +1,406 @@
|
||||
"""
|
||||
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
|
||||
21
apps/coordinator-api/aitbc/settlement/__init__.py
Normal file
21
apps/coordinator-api/aitbc/settlement/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""
|
||||
Cross-chain settlement module for AITBC
|
||||
"""
|
||||
|
||||
from .manager import BridgeManager
|
||||
from .hooks import SettlementHook, BatchSettlementHook, SettlementMonitor
|
||||
from .storage import SettlementStorage, InMemorySettlementStorage
|
||||
from .bridges.base import BridgeAdapter, BridgeConfig, SettlementMessage, SettlementResult
|
||||
|
||||
__all__ = [
|
||||
"BridgeManager",
|
||||
"SettlementHook",
|
||||
"BatchSettlementHook",
|
||||
"SettlementMonitor",
|
||||
"SettlementStorage",
|
||||
"InMemorySettlementStorage",
|
||||
"BridgeAdapter",
|
||||
"BridgeConfig",
|
||||
"SettlementMessage",
|
||||
"SettlementResult",
|
||||
]
|
||||
23
apps/coordinator-api/aitbc/settlement/bridges/__init__.py
Normal file
23
apps/coordinator-api/aitbc/settlement/bridges/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
Bridge adapters for cross-chain settlements
|
||||
"""
|
||||
|
||||
from .base import (
|
||||
BridgeAdapter,
|
||||
BridgeConfig,
|
||||
SettlementMessage,
|
||||
SettlementResult,
|
||||
BridgeStatus,
|
||||
BridgeError
|
||||
)
|
||||
from .layerzero import LayerZeroAdapter
|
||||
|
||||
__all__ = [
|
||||
"BridgeAdapter",
|
||||
"BridgeConfig",
|
||||
"SettlementMessage",
|
||||
"SettlementResult",
|
||||
"BridgeStatus",
|
||||
"BridgeError",
|
||||
"LayerZeroAdapter",
|
||||
]
|
||||
172
apps/coordinator-api/aitbc/settlement/bridges/base.py
Normal file
172
apps/coordinator-api/aitbc/settlement/bridges/base.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""
|
||||
Base interfaces for cross-chain settlement bridges
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Any, List, Optional
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class BridgeStatus(Enum):
|
||||
"""Bridge operation status"""
|
||||
PENDING = "pending"
|
||||
IN_PROGRESS = "in_progress"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
REFUNDED = "refunded"
|
||||
|
||||
|
||||
@dataclass
|
||||
class BridgeConfig:
|
||||
"""Bridge configuration"""
|
||||
name: str
|
||||
enabled: bool
|
||||
endpoint_address: str
|
||||
supported_chains: List[int]
|
||||
default_fee: str
|
||||
max_message_size: int
|
||||
timeout: int = 3600
|
||||
|
||||
|
||||
@dataclass
|
||||
class SettlementMessage:
|
||||
"""Message to be settled across chains"""
|
||||
source_chain_id: int
|
||||
target_chain_id: int
|
||||
job_id: str
|
||||
receipt_hash: str
|
||||
proof_data: Dict[str, Any]
|
||||
payment_amount: int
|
||||
payment_token: str
|
||||
nonce: int
|
||||
signature: str
|
||||
gas_limit: Optional[int] = None
|
||||
created_at: datetime = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.created_at is None:
|
||||
self.created_at = datetime.utcnow()
|
||||
|
||||
|
||||
@dataclass
|
||||
class SettlementResult:
|
||||
"""Result of settlement operation"""
|
||||
message_id: str
|
||||
status: BridgeStatus
|
||||
transaction_hash: Optional[str] = None
|
||||
error_message: Optional[str] = None
|
||||
gas_used: Optional[int] = None
|
||||
fee_paid: Optional[int] = None
|
||||
created_at: datetime = None
|
||||
completed_at: Optional[datetime] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.created_at is None:
|
||||
self.created_at = datetime.utcnow()
|
||||
|
||||
|
||||
class BridgeAdapter(ABC):
|
||||
"""Abstract interface for bridge adapters"""
|
||||
|
||||
def __init__(self, config: BridgeConfig):
|
||||
self.config = config
|
||||
self.name = config.name
|
||||
|
||||
@abstractmethod
|
||||
async def initialize(self) -> None:
|
||||
"""Initialize the bridge adapter"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def send_message(self, message: SettlementMessage) -> SettlementResult:
|
||||
"""Send message to target chain"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def verify_delivery(self, message_id: str) -> bool:
|
||||
"""Verify message was delivered"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_message_status(self, message_id: str) -> SettlementResult:
|
||||
"""Get current status of message"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def estimate_cost(self, message: SettlementMessage) -> Dict[str, int]:
|
||||
"""Estimate bridge fees"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def refund_failed_message(self, message_id: str) -> SettlementResult:
|
||||
"""Refund failed message if supported"""
|
||||
pass
|
||||
|
||||
def get_supported_chains(self) -> List[int]:
|
||||
"""Get list of supported target chains"""
|
||||
return self.config.supported_chains
|
||||
|
||||
def get_max_message_size(self) -> int:
|
||||
"""Get maximum message size in bytes"""
|
||||
return self.config.max_message_size
|
||||
|
||||
async def validate_message(self, message: SettlementMessage) -> bool:
|
||||
"""Validate message before sending"""
|
||||
# Check if target chain is supported
|
||||
if message.target_chain_id not in self.get_supported_chains():
|
||||
raise ValueError(f"Chain {message.target_chain_id} not supported")
|
||||
|
||||
# Check message size
|
||||
message_size = len(json.dumps(message.proof_data).encode())
|
||||
if message_size > self.get_max_message_size():
|
||||
raise ValueError(f"Message too large: {message_size} > {self.get_max_message_size()}")
|
||||
|
||||
# Validate signature
|
||||
if not await self._verify_signature(message):
|
||||
raise ValueError("Invalid signature")
|
||||
|
||||
return True
|
||||
|
||||
async def _verify_signature(self, message: SettlementMessage) -> bool:
|
||||
"""Verify message signature - to be implemented by subclass"""
|
||||
# This would verify the cryptographic signature
|
||||
# Implementation depends on the signature scheme used
|
||||
return True
|
||||
|
||||
def _encode_payload(self, message: SettlementMessage) -> bytes:
|
||||
"""Encode message payload - to be implemented by subclass"""
|
||||
# Each bridge may have different encoding requirements
|
||||
raise NotImplementedError("Subclass must implement _encode_payload")
|
||||
|
||||
async def _get_gas_estimate(self, message: SettlementMessage) -> int:
|
||||
"""Get gas estimate for message - to be implemented by subclass"""
|
||||
# Each bridge has different gas requirements
|
||||
raise NotImplementedError("Subclass must implement _get_gas_estimate")
|
||||
|
||||
|
||||
class BridgeError(Exception):
|
||||
"""Base exception for bridge errors"""
|
||||
pass
|
||||
|
||||
|
||||
class BridgeNotSupportedError(BridgeError):
|
||||
"""Raised when operation is not supported by bridge"""
|
||||
pass
|
||||
|
||||
|
||||
class BridgeTimeoutError(BridgeError):
|
||||
"""Raised when bridge operation times out"""
|
||||
pass
|
||||
|
||||
|
||||
class BridgeInsufficientFundsError(BridgeError):
|
||||
"""Raised when insufficient funds for bridge operation"""
|
||||
pass
|
||||
|
||||
|
||||
class BridgeMessageTooLargeError(BridgeError):
|
||||
"""Raised when message exceeds bridge limits"""
|
||||
pass
|
||||
288
apps/coordinator-api/aitbc/settlement/bridges/layerzero.py
Normal file
288
apps/coordinator-api/aitbc/settlement/bridges/layerzero.py
Normal file
@@ -0,0 +1,288 @@
|
||||
"""
|
||||
LayerZero bridge adapter implementation
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List, Optional
|
||||
import json
|
||||
import asyncio
|
||||
from web3 import Web3
|
||||
from web3.contract import Contract
|
||||
from eth_utils import to_checksum_address, encode_hex
|
||||
|
||||
from .base import (
|
||||
BridgeAdapter,
|
||||
BridgeConfig,
|
||||
SettlementMessage,
|
||||
SettlementResult,
|
||||
BridgeStatus,
|
||||
BridgeError,
|
||||
BridgeTimeoutError,
|
||||
BridgeInsufficientFundsError
|
||||
)
|
||||
|
||||
|
||||
class LayerZeroAdapter(BridgeAdapter):
|
||||
"""LayerZero bridge adapter for cross-chain settlements"""
|
||||
|
||||
# LayerZero chain IDs
|
||||
CHAIN_IDS = {
|
||||
1: 101, # Ethereum
|
||||
137: 109, # Polygon
|
||||
56: 102, # BSC
|
||||
42161: 110, # Arbitrum
|
||||
10: 111, # Optimism
|
||||
43114: 106 # Avalanche
|
||||
}
|
||||
|
||||
def __init__(self, config: BridgeConfig, web3: Web3):
|
||||
super().__init__(config)
|
||||
self.web3 = web3
|
||||
self.endpoint: Optional[Contract] = None
|
||||
self.ultra_light_node: Optional[Contract] = None
|
||||
|
||||
async def initialize(self) -> None:
|
||||
"""Initialize LayerZero contracts"""
|
||||
# Load LayerZero endpoint ABI
|
||||
endpoint_abi = await self._load_abi("LayerZeroEndpoint")
|
||||
self.endpoint = self.web3.eth.contract(
|
||||
address=to_checksum_address(self.config.endpoint_address),
|
||||
abi=endpoint_abi
|
||||
)
|
||||
|
||||
# Load Ultra Light Node ABI for fee estimation
|
||||
uln_abi = await self._load_abi("UltraLightNode")
|
||||
uln_address = await self.endpoint.functions.ultraLightNode().call()
|
||||
self.ultra_light_node = self.web3.eth.contract(
|
||||
address=to_checksum_address(uln_address),
|
||||
abi=uln_abi
|
||||
)
|
||||
|
||||
async def send_message(self, message: SettlementMessage) -> SettlementResult:
|
||||
"""Send message via LayerZero"""
|
||||
try:
|
||||
# Validate message
|
||||
await self.validate_message(message)
|
||||
|
||||
# Get target address on destination chain
|
||||
target_address = await self._get_target_address(message.target_chain_id)
|
||||
|
||||
# Encode payload
|
||||
payload = self._encode_payload(message)
|
||||
|
||||
# Estimate fees
|
||||
fees = await self.estimate_cost(message)
|
||||
|
||||
# Get gas limit
|
||||
gas_limit = message.gas_limit or await self._get_gas_estimate(message)
|
||||
|
||||
# Build transaction
|
||||
tx_params = {
|
||||
'from': await self._get_signer_address(),
|
||||
'gas': gas_limit,
|
||||
'value': fees['layerZeroFee'],
|
||||
'nonce': await self.web3.eth.get_transaction_count(
|
||||
await self._get_signer_address()
|
||||
)
|
||||
}
|
||||
|
||||
# Send transaction
|
||||
tx_hash = await self.endpoint.functions.send(
|
||||
self.CHAIN_IDS[message.target_chain_id], # dstChainId
|
||||
target_address, # destination address
|
||||
payload, # payload
|
||||
message.payment_amount, # value (optional)
|
||||
[0, 0, 0], # address and parameters for adapterParams
|
||||
message.nonce # refund address
|
||||
).transact(tx_params)
|
||||
|
||||
# Wait for confirmation
|
||||
receipt = await self.web3.eth.wait_for_transaction_receipt(tx_hash)
|
||||
|
||||
return SettlementResult(
|
||||
message_id=tx_hash.hex(),
|
||||
status=BridgeStatus.IN_PROGRESS,
|
||||
transaction_hash=tx_hash.hex(),
|
||||
gas_used=receipt.gasUsed,
|
||||
fee_paid=fees['layerZeroFee']
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return SettlementResult(
|
||||
message_id="",
|
||||
status=BridgeStatus.FAILED,
|
||||
error_message=str(e)
|
||||
)
|
||||
|
||||
async def verify_delivery(self, message_id: str) -> bool:
|
||||
"""Verify message was delivered"""
|
||||
try:
|
||||
# Get transaction receipt
|
||||
receipt = await self.web3.eth.get_transaction_receipt(message_id)
|
||||
|
||||
# Check for Delivered event
|
||||
delivered_logs = self.endpoint.events.Delivered().processReceipt(receipt)
|
||||
return len(delivered_logs) > 0
|
||||
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
async def get_message_status(self, message_id: str) -> SettlementResult:
|
||||
"""Get current status of message"""
|
||||
try:
|
||||
# Get transaction receipt
|
||||
receipt = await self.web3.eth.get_transaction_receipt(message_id)
|
||||
|
||||
if receipt.status == 0:
|
||||
return SettlementResult(
|
||||
message_id=message_id,
|
||||
status=BridgeStatus.FAILED,
|
||||
transaction_hash=message_id,
|
||||
completed_at=receipt['blockTimestamp']
|
||||
)
|
||||
|
||||
# Check if delivered
|
||||
if await self.verify_delivery(message_id):
|
||||
return SettlementResult(
|
||||
message_id=message_id,
|
||||
status=BridgeStatus.COMPLETED,
|
||||
transaction_hash=message_id,
|
||||
completed_at=receipt['blockTimestamp']
|
||||
)
|
||||
|
||||
# Still in progress
|
||||
return SettlementResult(
|
||||
message_id=message_id,
|
||||
status=BridgeStatus.IN_PROGRESS,
|
||||
transaction_hash=message_id
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return SettlementResult(
|
||||
message_id=message_id,
|
||||
status=BridgeStatus.FAILED,
|
||||
error_message=str(e)
|
||||
)
|
||||
|
||||
async def estimate_cost(self, message: SettlementMessage) -> Dict[str, int]:
|
||||
"""Estimate LayerZero fees"""
|
||||
try:
|
||||
# Get destination chain ID
|
||||
dst_chain_id = self.CHAIN_IDS[message.target_chain_id]
|
||||
|
||||
# Get target address
|
||||
target_address = await self._get_target_address(message.target_chain_id)
|
||||
|
||||
# Encode payload
|
||||
payload = self._encode_payload(message)
|
||||
|
||||
# Estimate fee using LayerZero endpoint
|
||||
(native_fee, zro_fee) = await self.endpoint.functions.estimateFees(
|
||||
dst_chain_id,
|
||||
target_address,
|
||||
payload,
|
||||
False, # payInZRO
|
||||
[0, 0, 0] # adapterParams
|
||||
).call()
|
||||
|
||||
return {
|
||||
'layerZeroFee': native_fee,
|
||||
'zroFee': zro_fee,
|
||||
'total': native_fee + zro_fee
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise BridgeError(f"Failed to estimate fees: {str(e)}")
|
||||
|
||||
async def refund_failed_message(self, message_id: str) -> SettlementResult:
|
||||
"""LayerZero doesn't support direct refunds"""
|
||||
raise BridgeNotSupportedError("LayerZero does not support message refunds")
|
||||
|
||||
def _encode_payload(self, message: SettlementMessage) -> bytes:
|
||||
"""Encode settlement message for LayerZero"""
|
||||
# Use ABI encoding for structured data
|
||||
from web3 import Web3
|
||||
|
||||
# Define the payload structure
|
||||
payload_types = [
|
||||
'uint256', # job_id
|
||||
'bytes32', # receipt_hash
|
||||
'bytes', # proof_data (JSON)
|
||||
'uint256', # payment_amount
|
||||
'address', # payment_token
|
||||
'uint256', # nonce
|
||||
'bytes' # signature
|
||||
]
|
||||
|
||||
payload_values = [
|
||||
int(message.job_id),
|
||||
bytes.fromhex(message.receipt_hash),
|
||||
json.dumps(message.proof_data).encode(),
|
||||
message.payment_amount,
|
||||
to_checksum_address(message.payment_token),
|
||||
message.nonce,
|
||||
bytes.fromhex(message.signature)
|
||||
]
|
||||
|
||||
# Encode the payload
|
||||
encoded = Web3().codec.encode(payload_types, payload_values)
|
||||
return encoded
|
||||
|
||||
async def _get_target_address(self, target_chain_id: int) -> str:
|
||||
"""Get target contract address on destination chain"""
|
||||
# This would look up the target address from configuration
|
||||
# For now, return a placeholder
|
||||
target_addresses = {
|
||||
1: "0x...", # Ethereum
|
||||
137: "0x...", # Polygon
|
||||
56: "0x...", # BSC
|
||||
42161: "0x..." # Arbitrum
|
||||
}
|
||||
|
||||
if target_chain_id not in target_addresses:
|
||||
raise ValueError(f"No target address configured for chain {target_chain_id}")
|
||||
|
||||
return target_addresses[target_chain_id]
|
||||
|
||||
async def _get_gas_estimate(self, message: SettlementMessage) -> int:
|
||||
"""Estimate gas for LayerZero transaction"""
|
||||
try:
|
||||
# Get target address
|
||||
target_address = await self._get_target_address(message.target_chain_id)
|
||||
|
||||
# Encode payload
|
||||
payload = self._encode_payload(message)
|
||||
|
||||
# Estimate gas
|
||||
gas_estimate = await self.endpoint.functions.send(
|
||||
self.CHAIN_IDS[message.target_chain_id],
|
||||
target_address,
|
||||
payload,
|
||||
message.payment_amount,
|
||||
[0, 0, 0],
|
||||
message.nonce
|
||||
).estimateGas({'from': await self._get_signer_address()})
|
||||
|
||||
# Add 20% buffer
|
||||
return int(gas_estimate * 1.2)
|
||||
|
||||
except Exception:
|
||||
# Return default estimate
|
||||
return 300000
|
||||
|
||||
async def _get_signer_address(self) -> str:
|
||||
"""Get the signer address for transactions"""
|
||||
# This would get the address from the wallet/key management system
|
||||
# For now, return a placeholder
|
||||
return "0x..."
|
||||
|
||||
async def _load_abi(self, contract_name: str) -> List[Dict]:
|
||||
"""Load contract ABI from file or registry"""
|
||||
# This would load the ABI from a file or contract registry
|
||||
# For now, return empty list
|
||||
return []
|
||||
|
||||
async def _verify_signature(self, message: SettlementMessage) -> bool:
|
||||
"""Verify LayerZero message signature"""
|
||||
# Implement signature verification specific to LayerZero
|
||||
# This would verify the message signature using the appropriate scheme
|
||||
return True
|
||||
327
apps/coordinator-api/aitbc/settlement/hooks.py
Normal file
327
apps/coordinator-api/aitbc/settlement/hooks.py
Normal file
@@ -0,0 +1,327 @@
|
||||
"""
|
||||
Settlement hooks for coordinator API integration
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional, List
|
||||
from datetime import datetime
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from .manager import BridgeManager
|
||||
from .bridges.base import (
|
||||
SettlementMessage,
|
||||
SettlementResult,
|
||||
BridgeStatus
|
||||
)
|
||||
from ..models.job import Job
|
||||
from ..models.receipt import Receipt
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SettlementHook:
|
||||
"""Settlement hook for coordinator to handle cross-chain settlements"""
|
||||
|
||||
def __init__(self, bridge_manager: BridgeManager):
|
||||
self.bridge_manager = bridge_manager
|
||||
self._enabled = True
|
||||
|
||||
async def on_job_completed(self, job: Job) -> None:
|
||||
"""Called when a job completes successfully"""
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
try:
|
||||
# Check if cross-chain settlement is required
|
||||
if await self._requires_cross_chain_settlement(job):
|
||||
await self._initiate_settlement(job)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to handle job completion for {job.id}: {e}")
|
||||
# Don't fail the job, just log the error
|
||||
await self._handle_settlement_error(job, e)
|
||||
|
||||
async def on_job_failed(self, job: Job, error: Exception) -> None:
|
||||
"""Called when a job fails"""
|
||||
# For failed jobs, we might want to refund any cross-chain payments
|
||||
if job.cross_chain_payment_id:
|
||||
try:
|
||||
await self._refund_cross_chain_payment(job)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to refund cross-chain payment for {job.id}: {e}")
|
||||
|
||||
async def initiate_manual_settlement(
|
||||
self,
|
||||
job_id: str,
|
||||
target_chain_id: int,
|
||||
bridge_name: Optional[str] = None,
|
||||
options: Optional[Dict[str, Any]] = None
|
||||
) -> SettlementResult:
|
||||
"""Manually initiate cross-chain settlement for a job"""
|
||||
# Get job
|
||||
job = await Job.get(job_id)
|
||||
if not job:
|
||||
raise ValueError(f"Job {job_id} not found")
|
||||
|
||||
if not job.completed:
|
||||
raise ValueError(f"Job {job_id} is not completed")
|
||||
|
||||
# Override target chain if specified
|
||||
if target_chain_id:
|
||||
job.target_chain = target_chain_id
|
||||
|
||||
# Create settlement message
|
||||
message = await self._create_settlement_message(job, options)
|
||||
|
||||
# Send settlement
|
||||
result = await self.bridge_manager.settle_cross_chain(
|
||||
message,
|
||||
bridge_name=bridge_name
|
||||
)
|
||||
|
||||
# Update job with settlement info
|
||||
job.cross_chain_settlement_id = result.message_id
|
||||
job.cross_chain_bridge = bridge_name or self.bridge_manager.default_adapter
|
||||
await job.save()
|
||||
|
||||
return result
|
||||
|
||||
async def get_settlement_status(self, settlement_id: str) -> SettlementResult:
|
||||
"""Get status of a cross-chain settlement"""
|
||||
return await self.bridge_manager.get_settlement_status(settlement_id)
|
||||
|
||||
async def estimate_settlement_cost(
|
||||
self,
|
||||
job_id: str,
|
||||
target_chain_id: int,
|
||||
bridge_name: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Estimate cost for cross-chain settlement"""
|
||||
# Get job
|
||||
job = await Job.get(job_id)
|
||||
if not job:
|
||||
raise ValueError(f"Job {job_id} not found")
|
||||
|
||||
# Create mock settlement message for estimation
|
||||
message = SettlementMessage(
|
||||
source_chain_id=await self._get_current_chain_id(),
|
||||
target_chain_id=target_chain_id,
|
||||
job_id=job.id,
|
||||
receipt_hash=job.receipt.hash if job.receipt else "",
|
||||
proof_data=job.receipt.proof if job.receipt else {},
|
||||
payment_amount=job.payment_amount or 0,
|
||||
payment_token=job.payment_token or "AITBC",
|
||||
nonce=await self._generate_nonce(),
|
||||
signature="" # Not needed for estimation
|
||||
)
|
||||
|
||||
return await self.bridge_manager.estimate_settlement_cost(
|
||||
message,
|
||||
bridge_name=bridge_name
|
||||
)
|
||||
|
||||
async def list_supported_bridges(self) -> Dict[str, Any]:
|
||||
"""List all supported bridges and their capabilities"""
|
||||
return self.bridge_manager.get_bridge_info()
|
||||
|
||||
async def list_supported_chains(self) -> Dict[str, List[int]]:
|
||||
"""List all supported chains by bridge"""
|
||||
return self.bridge_manager.get_supported_chains()
|
||||
|
||||
async def enable(self) -> None:
|
||||
"""Enable settlement hooks"""
|
||||
self._enabled = True
|
||||
logger.info("Settlement hooks enabled")
|
||||
|
||||
async def disable(self) -> None:
|
||||
"""Disable settlement hooks"""
|
||||
self._enabled = False
|
||||
logger.info("Settlement hooks disabled")
|
||||
|
||||
async def _requires_cross_chain_settlement(self, job: Job) -> bool:
|
||||
"""Check if job requires cross-chain settlement"""
|
||||
# Check if job has target chain different from current
|
||||
if job.target_chain and job.target_chain != await self._get_current_chain_id():
|
||||
return True
|
||||
|
||||
# Check if job explicitly requests cross-chain settlement
|
||||
if job.requires_cross_chain_settlement:
|
||||
return True
|
||||
|
||||
# Check if payment is on different chain
|
||||
if job.payment_chain and job.payment_chain != await self._get_current_chain_id():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
async def _initiate_settlement(self, job: Job) -> None:
|
||||
"""Initiate cross-chain settlement for a job"""
|
||||
try:
|
||||
# Create settlement message
|
||||
message = await self._create_settlement_message(job)
|
||||
|
||||
# Get optimal bridge if not specified
|
||||
bridge_name = job.preferred_bridge or await self.bridge_manager.get_optimal_bridge(
|
||||
message,
|
||||
priority=job.settlement_priority or 'cost'
|
||||
)
|
||||
|
||||
# Send settlement
|
||||
result = await self.bridge_manager.settle_cross_chain(
|
||||
message,
|
||||
bridge_name=bridge_name
|
||||
)
|
||||
|
||||
# Update job with settlement info
|
||||
job.cross_chain_settlement_id = result.message_id
|
||||
job.cross_chain_bridge = bridge_name
|
||||
job.cross_chain_settlement_status = result.status.value
|
||||
await job.save()
|
||||
|
||||
logger.info(f"Initiated cross-chain settlement for job {job.id}: {result.message_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initiate settlement for job {job.id}: {e}")
|
||||
await self._handle_settlement_error(job, e)
|
||||
|
||||
async def _create_settlement_message(self, job: Job, options: Optional[Dict[str, Any]] = None) -> SettlementMessage:
|
||||
"""Create settlement message from job"""
|
||||
# Get current chain ID
|
||||
source_chain_id = await self._get_current_chain_id()
|
||||
|
||||
# Get receipt data
|
||||
receipt_hash = ""
|
||||
proof_data = {}
|
||||
zk_proof = None
|
||||
|
||||
if job.receipt:
|
||||
receipt_hash = job.receipt.hash
|
||||
proof_data = job.receipt.proof or {}
|
||||
|
||||
# Check if ZK proof is included in receipt
|
||||
if options and options.get("use_zk_proof"):
|
||||
zk_proof = job.receipt.payload.get("zk_proof")
|
||||
if not zk_proof:
|
||||
logger.warning(f"ZK proof requested but not found in receipt for job {job.id}")
|
||||
|
||||
# Sign the settlement message
|
||||
signature = await self._sign_settlement_message(job)
|
||||
|
||||
return SettlementMessage(
|
||||
source_chain_id=source_chain_id,
|
||||
target_chain_id=job.target_chain or source_chain_id,
|
||||
job_id=job.id,
|
||||
receipt_hash=receipt_hash,
|
||||
proof_data=proof_data,
|
||||
zk_proof=zk_proof,
|
||||
payment_amount=job.payment_amount or 0,
|
||||
payment_token=job.payment_token or "AITBC",
|
||||
nonce=await self._generate_nonce(),
|
||||
signature=signature,
|
||||
gas_limit=job.settlement_gas_limit,
|
||||
privacy_level=options.get("privacy_level") if options else None
|
||||
)
|
||||
|
||||
async def _get_current_chain_id(self) -> int:
|
||||
"""Get the current blockchain chain ID"""
|
||||
# This would get the chain ID from the blockchain node
|
||||
# For now, return a placeholder
|
||||
return 1 # Ethereum mainnet
|
||||
|
||||
async def _generate_nonce(self) -> int:
|
||||
"""Generate a unique nonce for settlement"""
|
||||
# This would generate a unique nonce
|
||||
# For now, use timestamp
|
||||
return int(datetime.utcnow().timestamp())
|
||||
|
||||
async def _sign_settlement_message(self, job: Job) -> str:
|
||||
"""Sign the settlement message"""
|
||||
# This would sign the message with the appropriate key
|
||||
# For now, return a placeholder
|
||||
return "0x..." * 20
|
||||
|
||||
async def _handle_settlement_error(self, job: Job, error: Exception) -> None:
|
||||
"""Handle settlement errors"""
|
||||
# Update job with error info
|
||||
job.cross_chain_settlement_error = str(error)
|
||||
job.cross_chain_settlement_status = BridgeStatus.FAILED.value
|
||||
await job.save()
|
||||
|
||||
# Notify monitoring system
|
||||
await self._notify_settlement_failure(job, error)
|
||||
|
||||
async def _refund_cross_chain_payment(self, job: Job) -> None:
|
||||
"""Refund a cross-chain payment if possible"""
|
||||
if not job.cross_chain_payment_id:
|
||||
return
|
||||
|
||||
try:
|
||||
result = await self.bridge_manager.refund_failed_settlement(
|
||||
job.cross_chain_payment_id
|
||||
)
|
||||
|
||||
# Update job with refund info
|
||||
job.cross_chain_refund_id = result.message_id
|
||||
job.cross_chain_refund_status = result.status.value
|
||||
await job.save()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to refund cross-chain payment for {job.id}: {e}")
|
||||
|
||||
async def _notify_settlement_failure(self, job: Job, error: Exception) -> None:
|
||||
"""Notify monitoring system of settlement failure"""
|
||||
# This would send alerts to the monitoring system
|
||||
logger.error(f"Settlement failure for job {job.id}: {error}")
|
||||
|
||||
|
||||
class BatchSettlementHook:
|
||||
"""Hook for handling batch settlements"""
|
||||
|
||||
def __init__(self, bridge_manager: BridgeManager):
|
||||
self.bridge_manager = bridge_manager
|
||||
self.batch_size = 10
|
||||
self.batch_timeout = 300 # 5 minutes
|
||||
|
||||
async def add_to_batch(self, job: Job) -> None:
|
||||
"""Add job to batch settlement queue"""
|
||||
# This would add the job to a batch queue
|
||||
pass
|
||||
|
||||
async def process_batch(self) -> List[SettlementResult]:
|
||||
"""Process a batch of settlements"""
|
||||
# This would process queued jobs in batches
|
||||
# For now, return empty list
|
||||
return []
|
||||
|
||||
|
||||
class SettlementMonitor:
|
||||
"""Monitor for cross-chain settlements"""
|
||||
|
||||
def __init__(self, bridge_manager: BridgeManager):
|
||||
self.bridge_manager = bridge_manager
|
||||
self._monitoring = False
|
||||
|
||||
async def start_monitoring(self) -> None:
|
||||
"""Start monitoring settlements"""
|
||||
self._monitoring = True
|
||||
|
||||
while self._monitoring:
|
||||
try:
|
||||
# Get pending settlements
|
||||
pending = await self.bridge_manager.storage.get_pending_settlements()
|
||||
|
||||
# Check status of each
|
||||
for settlement in pending:
|
||||
await self.bridge_manager.get_settlement_status(
|
||||
settlement['message_id']
|
||||
)
|
||||
|
||||
# Wait before next check
|
||||
await asyncio.sleep(30)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in settlement monitoring: {e}")
|
||||
await asyncio.sleep(60)
|
||||
|
||||
async def stop_monitoring(self) -> None:
|
||||
"""Stop monitoring settlements"""
|
||||
self._monitoring = False
|
||||
337
apps/coordinator-api/aitbc/settlement/manager.py
Normal file
337
apps/coordinator-api/aitbc/settlement/manager.py
Normal file
@@ -0,0 +1,337 @@
|
||||
"""
|
||||
Bridge manager for cross-chain settlements
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List, Optional, Type
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from dataclasses import asdict
|
||||
|
||||
from .bridges.base import (
|
||||
BridgeAdapter,
|
||||
BridgeConfig,
|
||||
SettlementMessage,
|
||||
SettlementResult,
|
||||
BridgeStatus,
|
||||
BridgeError
|
||||
)
|
||||
from .bridges.layerzero import LayerZeroAdapter
|
||||
from .storage import SettlementStorage
|
||||
|
||||
|
||||
class BridgeManager:
|
||||
"""Manages multiple bridge adapters for cross-chain settlements"""
|
||||
|
||||
def __init__(self, storage: SettlementStorage):
|
||||
self.adapters: Dict[str, BridgeAdapter] = {}
|
||||
self.default_adapter: Optional[str] = None
|
||||
self.storage = storage
|
||||
self._initialized = False
|
||||
|
||||
async def initialize(self, configs: Dict[str, BridgeConfig]) -> None:
|
||||
"""Initialize all bridge adapters"""
|
||||
for name, config in configs.items():
|
||||
if config.enabled:
|
||||
adapter = await self._create_adapter(config)
|
||||
await adapter.initialize()
|
||||
self.adapters[name] = adapter
|
||||
|
||||
# Set first enabled adapter as default
|
||||
if self.default_adapter is None:
|
||||
self.default_adapter = name
|
||||
|
||||
self._initialized = True
|
||||
|
||||
async def register_adapter(self, name: str, adapter: BridgeAdapter) -> None:
|
||||
"""Register a bridge adapter"""
|
||||
await adapter.initialize()
|
||||
self.adapters[name] = adapter
|
||||
|
||||
if self.default_adapter is None:
|
||||
self.default_adapter = name
|
||||
|
||||
async def settle_cross_chain(
|
||||
self,
|
||||
message: SettlementMessage,
|
||||
bridge_name: Optional[str] = None,
|
||||
retry_on_failure: bool = True
|
||||
) -> SettlementResult:
|
||||
"""Settle message across chains"""
|
||||
if not self._initialized:
|
||||
raise BridgeError("Bridge manager not initialized")
|
||||
|
||||
# Get adapter
|
||||
adapter = self._get_adapter(bridge_name)
|
||||
|
||||
# Validate message
|
||||
await adapter.validate_message(message)
|
||||
|
||||
# Store initial settlement record
|
||||
await self.storage.store_settlement(
|
||||
message_id="pending",
|
||||
message=message,
|
||||
bridge_name=adapter.name,
|
||||
status=BridgeStatus.PENDING
|
||||
)
|
||||
|
||||
# Attempt settlement with retries
|
||||
max_retries = 3 if retry_on_failure else 1
|
||||
last_error = None
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
# Send message
|
||||
result = await adapter.send_message(message)
|
||||
|
||||
# Update storage with result
|
||||
await self.storage.update_settlement(
|
||||
message_id=result.message_id,
|
||||
status=result.status,
|
||||
transaction_hash=result.transaction_hash,
|
||||
error_message=result.error_message
|
||||
)
|
||||
|
||||
# Start monitoring for completion
|
||||
asyncio.create_task(self._monitor_settlement(result.message_id))
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
if attempt < max_retries - 1:
|
||||
# Wait before retry
|
||||
await asyncio.sleep(2 ** attempt) # Exponential backoff
|
||||
continue
|
||||
else:
|
||||
# Final attempt failed
|
||||
result = SettlementResult(
|
||||
message_id="",
|
||||
status=BridgeStatus.FAILED,
|
||||
error_message=str(e)
|
||||
)
|
||||
|
||||
await self.storage.update_settlement(
|
||||
message_id="",
|
||||
status=BridgeStatus.FAILED,
|
||||
error_message=str(e)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
async def get_settlement_status(self, message_id: str) -> SettlementResult:
|
||||
"""Get current status of settlement"""
|
||||
# Get from storage first
|
||||
stored = await self.storage.get_settlement(message_id)
|
||||
|
||||
if not stored:
|
||||
raise ValueError(f"Settlement {message_id} not found")
|
||||
|
||||
# If completed or failed, return stored result
|
||||
if stored['status'] in [BridgeStatus.COMPLETED, BridgeStatus.FAILED]:
|
||||
return SettlementResult(**stored)
|
||||
|
||||
# Otherwise check with bridge
|
||||
adapter = self.adapters.get(stored['bridge_name'])
|
||||
if not adapter:
|
||||
raise BridgeError(f"Bridge {stored['bridge_name']} not found")
|
||||
|
||||
# Get current status from bridge
|
||||
result = await adapter.get_message_status(message_id)
|
||||
|
||||
# Update storage if status changed
|
||||
if result.status != stored['status']:
|
||||
await self.storage.update_settlement(
|
||||
message_id=message_id,
|
||||
status=result.status,
|
||||
completed_at=result.completed_at
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
async def estimate_settlement_cost(
|
||||
self,
|
||||
message: SettlementMessage,
|
||||
bridge_name: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Estimate cost for settlement across different bridges"""
|
||||
results = {}
|
||||
|
||||
if bridge_name:
|
||||
# Estimate for specific bridge
|
||||
adapter = self._get_adapter(bridge_name)
|
||||
results[bridge_name] = await adapter.estimate_cost(message)
|
||||
else:
|
||||
# Estimate for all bridges
|
||||
for name, adapter in self.adapters.items():
|
||||
try:
|
||||
await adapter.validate_message(message)
|
||||
results[name] = await adapter.estimate_cost(message)
|
||||
except Exception as e:
|
||||
results[name] = {'error': str(e)}
|
||||
|
||||
return results
|
||||
|
||||
async def get_optimal_bridge(
|
||||
self,
|
||||
message: SettlementMessage,
|
||||
priority: str = 'cost' # 'cost' or 'speed'
|
||||
) -> str:
|
||||
"""Get optimal bridge for settlement"""
|
||||
if len(self.adapters) == 1:
|
||||
return list(self.adapters.keys())[0]
|
||||
|
||||
# Get estimates for all bridges
|
||||
estimates = await self.estimate_settlement_cost(message)
|
||||
|
||||
# Filter out failed estimates
|
||||
valid_estimates = {
|
||||
name: est for name, est in estimates.items()
|
||||
if 'error' not in est
|
||||
}
|
||||
|
||||
if not valid_estimates:
|
||||
raise BridgeError("No bridges available for settlement")
|
||||
|
||||
# Select based on priority
|
||||
if priority == 'cost':
|
||||
# Select cheapest
|
||||
optimal = min(valid_estimates.items(), key=lambda x: x[1]['total'])
|
||||
else:
|
||||
# Select fastest (based on historical data)
|
||||
# For now, return default
|
||||
optimal = (self.default_adapter, valid_estimates[self.default_adapter])
|
||||
|
||||
return optimal[0]
|
||||
|
||||
async def batch_settle(
|
||||
self,
|
||||
messages: List[SettlementMessage],
|
||||
bridge_name: Optional[str] = None
|
||||
) -> List[SettlementResult]:
|
||||
"""Settle multiple messages"""
|
||||
results = []
|
||||
|
||||
# Process in parallel with rate limiting
|
||||
semaphore = asyncio.Semaphore(5) # Max 5 concurrent settlements
|
||||
|
||||
async def settle_single(message):
|
||||
async with semaphore:
|
||||
return await self.settle_cross_chain(message, bridge_name)
|
||||
|
||||
tasks = [settle_single(msg) for msg in messages]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
# Convert exceptions to failed results
|
||||
processed_results = []
|
||||
for result in results:
|
||||
if isinstance(result, Exception):
|
||||
processed_results.append(SettlementResult(
|
||||
message_id="",
|
||||
status=BridgeStatus.FAILED,
|
||||
error_message=str(result)
|
||||
))
|
||||
else:
|
||||
processed_results.append(result)
|
||||
|
||||
return processed_results
|
||||
|
||||
async def refund_failed_settlement(self, message_id: str) -> SettlementResult:
|
||||
"""Attempt to refund a failed settlement"""
|
||||
# Get settlement details
|
||||
stored = await self.storage.get_settlement(message_id)
|
||||
|
||||
if not stored:
|
||||
raise ValueError(f"Settlement {message_id} not found")
|
||||
|
||||
# Check if it's actually failed
|
||||
if stored['status'] != BridgeStatus.FAILED:
|
||||
raise ValueError(f"Settlement {message_id} is not in failed state")
|
||||
|
||||
# Get adapter
|
||||
adapter = self.adapters.get(stored['bridge_name'])
|
||||
if not adapter:
|
||||
raise BridgeError(f"Bridge {stored['bridge_name']} not found")
|
||||
|
||||
# Attempt refund
|
||||
result = await adapter.refund_failed_message(message_id)
|
||||
|
||||
# Update storage
|
||||
await self.storage.update_settlement(
|
||||
message_id=message_id,
|
||||
status=result.status,
|
||||
error_message=result.error_message
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def get_supported_chains(self) -> Dict[str, List[int]]:
|
||||
"""Get all supported chains by bridge"""
|
||||
chains = {}
|
||||
for name, adapter in self.adapters.items():
|
||||
chains[name] = adapter.get_supported_chains()
|
||||
return chains
|
||||
|
||||
def get_bridge_info(self) -> Dict[str, Dict[str, Any]]:
|
||||
"""Get information about all bridges"""
|
||||
info = {}
|
||||
for name, adapter in self.adapters.items():
|
||||
info[name] = {
|
||||
'name': adapter.name,
|
||||
'supported_chains': adapter.get_supported_chains(),
|
||||
'max_message_size': adapter.get_max_message_size(),
|
||||
'config': asdict(adapter.config)
|
||||
}
|
||||
return info
|
||||
|
||||
async def _monitor_settlement(self, message_id: str) -> None:
|
||||
"""Monitor settlement until completion"""
|
||||
max_wait_time = timedelta(hours=1)
|
||||
start_time = datetime.utcnow()
|
||||
|
||||
while datetime.utcnow() - start_time < max_wait_time:
|
||||
# Check status
|
||||
result = await self.get_settlement_status(message_id)
|
||||
|
||||
# If completed or failed, stop monitoring
|
||||
if result.status in [BridgeStatus.COMPLETED, BridgeStatus.FAILED]:
|
||||
break
|
||||
|
||||
# Wait before checking again
|
||||
await asyncio.sleep(30) # Check every 30 seconds
|
||||
|
||||
# If still pending after timeout, mark as failed
|
||||
if result.status == BridgeStatus.IN_PROGRESS:
|
||||
await self.storage.update_settlement(
|
||||
message_id=message_id,
|
||||
status=BridgeStatus.FAILED,
|
||||
error_message="Settlement timed out"
|
||||
)
|
||||
|
||||
def _get_adapter(self, bridge_name: Optional[str] = None) -> BridgeAdapter:
|
||||
"""Get bridge adapter"""
|
||||
if bridge_name:
|
||||
if bridge_name not in self.adapters:
|
||||
raise BridgeError(f"Bridge {bridge_name} not found")
|
||||
return self.adapters[bridge_name]
|
||||
|
||||
if self.default_adapter is None:
|
||||
raise BridgeError("No default bridge configured")
|
||||
|
||||
return self.adapters[self.default_adapter]
|
||||
|
||||
async def _create_adapter(self, config: BridgeConfig) -> BridgeAdapter:
|
||||
"""Create adapter instance based on config"""
|
||||
# Import web3 here to avoid circular imports
|
||||
from web3 import Web3
|
||||
|
||||
# Get web3 instance (this would be injected or configured)
|
||||
web3 = Web3() # Placeholder
|
||||
|
||||
if config.name == "layerzero":
|
||||
return LayerZeroAdapter(config, web3)
|
||||
# Add other adapters as they're implemented
|
||||
# elif config.name == "chainlink_ccip":
|
||||
# return ChainlinkCCIPAdapter(config, web3)
|
||||
else:
|
||||
raise BridgeError(f"Unknown bridge type: {config.name}")
|
||||
367
apps/coordinator-api/aitbc/settlement/storage.py
Normal file
367
apps/coordinator-api/aitbc/settlement/storage.py
Normal file
@@ -0,0 +1,367 @@
|
||||
"""
|
||||
Storage layer for cross-chain settlements
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional, List
|
||||
from datetime import datetime
|
||||
import json
|
||||
import asyncio
|
||||
from dataclasses import asdict
|
||||
|
||||
from .bridges.base import (
|
||||
SettlementMessage,
|
||||
SettlementResult,
|
||||
BridgeStatus
|
||||
)
|
||||
|
||||
|
||||
class SettlementStorage:
|
||||
"""Storage interface for settlement data"""
|
||||
|
||||
def __init__(self, db_connection):
|
||||
self.db = db_connection
|
||||
|
||||
async def store_settlement(
|
||||
self,
|
||||
message_id: str,
|
||||
message: SettlementMessage,
|
||||
bridge_name: str,
|
||||
status: BridgeStatus
|
||||
) -> None:
|
||||
"""Store a new settlement record"""
|
||||
query = """
|
||||
INSERT INTO settlements (
|
||||
message_id, job_id, source_chain_id, target_chain_id,
|
||||
receipt_hash, proof_data, payment_amount, payment_token,
|
||||
nonce, signature, bridge_name, status, created_at
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13
|
||||
)
|
||||
"""
|
||||
|
||||
await self.db.execute(query, (
|
||||
message_id,
|
||||
message.job_id,
|
||||
message.source_chain_id,
|
||||
message.target_chain_id,
|
||||
message.receipt_hash,
|
||||
json.dumps(message.proof_data),
|
||||
message.payment_amount,
|
||||
message.payment_token,
|
||||
message.nonce,
|
||||
message.signature,
|
||||
bridge_name,
|
||||
status.value,
|
||||
message.created_at or datetime.utcnow()
|
||||
))
|
||||
|
||||
async def update_settlement(
|
||||
self,
|
||||
message_id: str,
|
||||
status: Optional[BridgeStatus] = None,
|
||||
transaction_hash: Optional[str] = None,
|
||||
error_message: Optional[str] = None,
|
||||
completed_at: Optional[datetime] = None
|
||||
) -> None:
|
||||
"""Update settlement record"""
|
||||
updates = []
|
||||
params = []
|
||||
param_count = 1
|
||||
|
||||
if status is not None:
|
||||
updates.append(f"status = ${param_count}")
|
||||
params.append(status.value)
|
||||
param_count += 1
|
||||
|
||||
if transaction_hash is not None:
|
||||
updates.append(f"transaction_hash = ${param_count}")
|
||||
params.append(transaction_hash)
|
||||
param_count += 1
|
||||
|
||||
if error_message is not None:
|
||||
updates.append(f"error_message = ${param_count}")
|
||||
params.append(error_message)
|
||||
param_count += 1
|
||||
|
||||
if completed_at is not None:
|
||||
updates.append(f"completed_at = ${param_count}")
|
||||
params.append(completed_at)
|
||||
param_count += 1
|
||||
|
||||
if not updates:
|
||||
return
|
||||
|
||||
updates.append(f"updated_at = ${param_count}")
|
||||
params.append(datetime.utcnow())
|
||||
param_count += 1
|
||||
|
||||
params.append(message_id)
|
||||
|
||||
query = f"""
|
||||
UPDATE settlements
|
||||
SET {', '.join(updates)}
|
||||
WHERE message_id = ${param_count}
|
||||
"""
|
||||
|
||||
await self.db.execute(query, params)
|
||||
|
||||
async def get_settlement(self, message_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get settlement by message ID"""
|
||||
query = """
|
||||
SELECT * FROM settlements WHERE message_id = $1
|
||||
"""
|
||||
|
||||
result = await self.db.fetchrow(query, message_id)
|
||||
|
||||
if not result:
|
||||
return None
|
||||
|
||||
# Convert to dict
|
||||
settlement = dict(result)
|
||||
|
||||
# Parse JSON fields
|
||||
if settlement['proof_data']:
|
||||
settlement['proof_data'] = json.loads(settlement['proof_data'])
|
||||
|
||||
return settlement
|
||||
|
||||
async def get_settlements_by_job(self, job_id: str) -> List[Dict[str, Any]]:
|
||||
"""Get all settlements for a job"""
|
||||
query = """
|
||||
SELECT * FROM settlements
|
||||
WHERE job_id = $1
|
||||
ORDER BY created_at DESC
|
||||
"""
|
||||
|
||||
results = await self.db.fetch(query, job_id)
|
||||
|
||||
settlements = []
|
||||
for result in results:
|
||||
settlement = dict(result)
|
||||
if settlement['proof_data']:
|
||||
settlement['proof_data'] = json.loads(settlement['proof_data'])
|
||||
settlements.append(settlement)
|
||||
|
||||
return settlements
|
||||
|
||||
async def get_pending_settlements(self, bridge_name: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
"""Get all pending settlements"""
|
||||
query = """
|
||||
SELECT * FROM settlements
|
||||
WHERE status = 'pending' OR status = 'in_progress'
|
||||
"""
|
||||
params = []
|
||||
|
||||
if bridge_name:
|
||||
query += " AND bridge_name = $1"
|
||||
params.append(bridge_name)
|
||||
|
||||
query += " ORDER BY created_at ASC"
|
||||
|
||||
results = await self.db.fetch(query, *params)
|
||||
|
||||
settlements = []
|
||||
for result in results:
|
||||
settlement = dict(result)
|
||||
if settlement['proof_data']:
|
||||
settlement['proof_data'] = json.loads(settlement['proof_data'])
|
||||
settlements.append(settlement)
|
||||
|
||||
return settlements
|
||||
|
||||
async def get_settlement_stats(
|
||||
self,
|
||||
bridge_name: Optional[str] = None,
|
||||
time_range: Optional[int] = None # hours
|
||||
) -> Dict[str, Any]:
|
||||
"""Get settlement statistics"""
|
||||
conditions = []
|
||||
params = []
|
||||
param_count = 1
|
||||
|
||||
if bridge_name:
|
||||
conditions.append(f"bridge_name = ${param_count}")
|
||||
params.append(bridge_name)
|
||||
param_count += 1
|
||||
|
||||
if time_range:
|
||||
conditions.append(f"created_at > NOW() - INTERVAL '${param_count} hours'")
|
||||
params.append(time_range)
|
||||
param_count += 1
|
||||
|
||||
where_clause = "WHERE " + " AND ".join(conditions) if conditions else ""
|
||||
|
||||
query = f"""
|
||||
SELECT
|
||||
bridge_name,
|
||||
status,
|
||||
COUNT(*) as count,
|
||||
AVG(payment_amount) as avg_amount,
|
||||
SUM(payment_amount) as total_amount
|
||||
FROM settlements
|
||||
{where_clause}
|
||||
GROUP BY bridge_name, status
|
||||
"""
|
||||
|
||||
results = await self.db.fetch(query, *params)
|
||||
|
||||
stats = {}
|
||||
for result in results:
|
||||
bridge = result['bridge_name']
|
||||
if bridge not in stats:
|
||||
stats[bridge] = {}
|
||||
|
||||
stats[bridge][result['status']] = {
|
||||
'count': result['count'],
|
||||
'avg_amount': float(result['avg_amount']) if result['avg_amount'] else 0,
|
||||
'total_amount': float(result['total_amount']) if result['total_amount'] else 0
|
||||
}
|
||||
|
||||
return stats
|
||||
|
||||
async def cleanup_old_settlements(self, days: int = 30) -> int:
|
||||
"""Clean up old completed settlements"""
|
||||
query = """
|
||||
DELETE FROM settlements
|
||||
WHERE status IN ('completed', 'failed')
|
||||
AND created_at < NOW() - INTERVAL $1 days
|
||||
"""
|
||||
|
||||
result = await self.db.execute(query, days)
|
||||
return result.split()[-1] # Return number of deleted rows
|
||||
|
||||
|
||||
# In-memory implementation for testing
|
||||
class InMemorySettlementStorage(SettlementStorage):
|
||||
"""In-memory storage implementation for testing"""
|
||||
|
||||
def __init__(self):
|
||||
self.settlements: Dict[str, Dict[str, Any]] = {}
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
async def store_settlement(
|
||||
self,
|
||||
message_id: str,
|
||||
message: SettlementMessage,
|
||||
bridge_name: str,
|
||||
status: BridgeStatus
|
||||
) -> None:
|
||||
async with self._lock:
|
||||
self.settlements[message_id] = {
|
||||
'message_id': message_id,
|
||||
'job_id': message.job_id,
|
||||
'source_chain_id': message.source_chain_id,
|
||||
'target_chain_id': message.target_chain_id,
|
||||
'receipt_hash': message.receipt_hash,
|
||||
'proof_data': message.proof_data,
|
||||
'payment_amount': message.payment_amount,
|
||||
'payment_token': message.payment_token,
|
||||
'nonce': message.nonce,
|
||||
'signature': message.signature,
|
||||
'bridge_name': bridge_name,
|
||||
'status': status.value,
|
||||
'created_at': message.created_at or datetime.utcnow(),
|
||||
'updated_at': datetime.utcnow()
|
||||
}
|
||||
|
||||
async def update_settlement(
|
||||
self,
|
||||
message_id: str,
|
||||
status: Optional[BridgeStatus] = None,
|
||||
transaction_hash: Optional[str] = None,
|
||||
error_message: Optional[str] = None,
|
||||
completed_at: Optional[datetime] = None
|
||||
) -> None:
|
||||
async with self._lock:
|
||||
if message_id not in self.settlements:
|
||||
return
|
||||
|
||||
settlement = self.settlements[message_id]
|
||||
|
||||
if status is not None:
|
||||
settlement['status'] = status.value
|
||||
if transaction_hash is not None:
|
||||
settlement['transaction_hash'] = transaction_hash
|
||||
if error_message is not None:
|
||||
settlement['error_message'] = error_message
|
||||
if completed_at is not None:
|
||||
settlement['completed_at'] = completed_at
|
||||
|
||||
settlement['updated_at'] = datetime.utcnow()
|
||||
|
||||
async def get_settlement(self, message_id: str) -> Optional[Dict[str, Any]]:
|
||||
async with self._lock:
|
||||
return self.settlements.get(message_id)
|
||||
|
||||
async def get_settlements_by_job(self, job_id: str) -> List[Dict[str, Any]]:
|
||||
async with self._lock:
|
||||
return [
|
||||
s for s in self.settlements.values()
|
||||
if s['job_id'] == job_id
|
||||
]
|
||||
|
||||
async def get_pending_settlements(self, bridge_name: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
async with self._lock:
|
||||
pending = [
|
||||
s for s in self.settlements.values()
|
||||
if s['status'] in ['pending', 'in_progress']
|
||||
]
|
||||
|
||||
if bridge_name:
|
||||
pending = [s for s in pending if s['bridge_name'] == bridge_name]
|
||||
|
||||
return pending
|
||||
|
||||
async def get_settlement_stats(
|
||||
self,
|
||||
bridge_name: Optional[str] = None,
|
||||
time_range: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
async with self._lock:
|
||||
stats = {}
|
||||
|
||||
for settlement in self.settlements.values():
|
||||
if bridge_name and settlement['bridge_name'] != bridge_name:
|
||||
continue
|
||||
|
||||
# TODO: Implement time range filtering
|
||||
|
||||
bridge = settlement['bridge_name']
|
||||
if bridge not in stats:
|
||||
stats[bridge] = {}
|
||||
|
||||
status = settlement['status']
|
||||
if status not in stats[bridge]:
|
||||
stats[bridge][status] = {
|
||||
'count': 0,
|
||||
'avg_amount': 0,
|
||||
'total_amount': 0
|
||||
}
|
||||
|
||||
stats[bridge][status]['count'] += 1
|
||||
stats[bridge][status]['total_amount'] += settlement['payment_amount']
|
||||
|
||||
# Calculate averages
|
||||
for bridge_data in stats.values():
|
||||
for status_data in bridge_data.values():
|
||||
if status_data['count'] > 0:
|
||||
status_data['avg_amount'] = status_data['total_amount'] / status_data['count']
|
||||
|
||||
return stats
|
||||
|
||||
async def cleanup_old_settlements(self, days: int = 30) -> int:
|
||||
async with self._lock:
|
||||
cutoff = datetime.utcnow() - timedelta(days=days)
|
||||
|
||||
to_delete = [
|
||||
msg_id for msg_id, settlement in self.settlements.items()
|
||||
if (
|
||||
settlement['status'] in ['completed', 'failed'] and
|
||||
settlement['created_at'] < cutoff
|
||||
)
|
||||
]
|
||||
|
||||
for msg_id in to_delete:
|
||||
del self.settlements[msg_id]
|
||||
|
||||
return len(to_delete)
|
||||
Reference in New Issue
Block a user