fix: resolve 'box in a box' nesting issues in codebase

ISSUES RESOLVED:
1. Coordinator API unnecessary nesting
   BEFORE: /apps/coordinator-api/aitbc/api/v1/settlement.py
   AFTER:  /apps/coordinator-api/src/app/routers/settlement.py

   - Moved settlement code to proper router location
   - Moved logging.py to main app directory
   - Integrated settlement functionality into main FastAPI app
   - Removed duplicate /aitbc/ directory

2. AITBC Core package structure
   ANALYZED: /packages/py/aitbc-core/src/aitbc/
   STATUS:    Kept as-is (proper Python packaging)

   - src/aitbc/ is standard Python package structure
   - No unnecessary nesting detected
   - Follows Poetry best practices

LEGITIMATE DIRECTORIES (NO CHANGES):
- /cli/debian/etc/aitbc (Debian package structure)
- /cli/debian/usr/share/aitbc (Debian package structure)
- Node modules and virtual environments

BENEFITS:
- Eliminated duplicate code locations
- Integrated settlement functionality into main app
- Cleaner coordinator-api structure
- Reduced confusion in codebase organization
- Maintained proper Python packaging standards

VERIFICATION:
 No more problematic 'aitbc' directories
 All code properly organized
 Standard package structures maintained
 No functionality lost in refactoring
This commit is contained in:
2026-03-26 09:18:13 +01:00
parent 394ecb49b9
commit 23e4816077
13 changed files with 181 additions and 439 deletions

View File

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

View File

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

View 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",
]

View 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",
]

View 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

View 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

View File

@@ -0,0 +1,327 @@
"""
Settlement hooks for coordinator API integration
"""
from typing import Dict, Any, Optional, List
from datetime import datetime
import asyncio
from aitbc.logging import get_logger
from .manager import BridgeManager
from .bridges.base import (
SettlementMessage,
SettlementResult,
BridgeStatus
)
from ..models.job import Job
from ..models.receipt import Receipt
logger = get_logger(__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

View 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}")

View File

@@ -0,0 +1,377 @@
"""
Storage layer for cross-chain settlements
"""
from typing import Dict, Any, Optional, List
from datetime import datetime, timedelta
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
# Time range filtering
if time_range is not None:
cutoff = datetime.utcnow() - timedelta(hours=time_range)
if settlement["created_at"] < cutoff:
continue
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)