docs: add code quality and type checking workflows to master index
Some checks failed
Documentation Validation / validate-docs (push) Has been cancelled
Python Tests / test-python (push) Has been cancelled
API Endpoint Tests / test-api-endpoints (push) Has been cancelled
CLI Tests / test-cli (push) Has been cancelled
Integration Tests / test-service-integration (push) Has been cancelled
Package Tests / test-python-packages (map[name:aitbc-agent-sdk path:packages/py/aitbc-agent-sdk]) (push) Has been cancelled
Package Tests / test-python-packages (map[name:aitbc-core path:packages/py/aitbc-core]) (push) Has been cancelled
Package Tests / test-python-packages (map[name:aitbc-crypto path:packages/py/aitbc-crypto]) (push) Has been cancelled
Package Tests / test-python-packages (map[name:aitbc-sdk path:packages/py/aitbc-sdk]) (push) Has been cancelled
Package Tests / test-javascript-packages (map[name:aitbc-sdk-js path:packages/js/aitbc-sdk]) (push) Has been cancelled
Package Tests / test-javascript-packages (map[name:aitbc-token path:packages/solidity/aitbc-token]) (push) Has been cancelled
Security Scanning / security-scan (push) Has been cancelled
Systemd Sync / sync-systemd (push) Has been cancelled

- Add Code Quality Module section with pre-commit hooks and quality checks
- Add Type Checking CI/CD Module section with MyPy workflow and coverage
- Update README with code quality achievements and project structure
- Migrate FastAPI apps from deprecated on_event to lifespan context manager
- Update pyproject.toml files to reference consolidated dependencies
- Remove unused app.py import in coordinator-api
- Add type hints to agent
This commit is contained in:
aitbc
2026-03-31 21:45:43 +02:00
parent 26592ddf55
commit 9db720add8
308 changed files with 34194 additions and 34575 deletions

View File

@@ -2,10 +2,10 @@
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
from .hooks import BatchSettlementHook, SettlementHook, SettlementMonitor
from .manager import BridgeManager
from .storage import InMemorySettlementStorage, SettlementStorage
__all__ = [
"BridgeManager",

View File

@@ -2,14 +2,7 @@
Bridge adapters for cross-chain settlements
"""
from .base import (
BridgeAdapter,
BridgeConfig,
SettlementMessage,
SettlementResult,
BridgeStatus,
BridgeError
)
from .base import BridgeAdapter, BridgeConfig, BridgeError, BridgeStatus, SettlementMessage, SettlementResult
from .layerzero import LayerZeroAdapter
__all__ = [

View File

@@ -2,16 +2,17 @@
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 abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from typing import Any
class BridgeStatus(Enum):
"""Bridge operation status"""
PENDING = "pending"
IN_PROGRESS = "in_progress"
COMPLETED = "completed"
@@ -22,10 +23,11 @@ class BridgeStatus(Enum):
@dataclass
class BridgeConfig:
"""Bridge configuration"""
name: str
enabled: bool
endpoint_address: str
supported_chains: List[int]
supported_chains: list[int]
default_fee: str
max_message_size: int
timeout: int = 3600
@@ -34,18 +36,19 @@ class BridgeConfig:
@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]
proof_data: dict[str, Any]
payment_amount: int
payment_token: str
nonce: int
signature: str
gas_limit: Optional[int] = None
gas_limit: int | None = None
created_at: datetime = None
def __post_init__(self):
if self.created_at is None:
self.created_at = datetime.utcnow()
@@ -54,15 +57,16 @@ class SettlementMessage:
@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
transaction_hash: str | None = None
error_message: str | None = None
gas_used: int | None = None
fee_paid: int | None = None
created_at: datetime = None
completed_at: Optional[datetime] = None
completed_at: datetime | None = None
def __post_init__(self):
if self.created_at is None:
self.created_at = datetime.utcnow()
@@ -70,77 +74,77 @@ class SettlementResult:
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]:
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]:
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
@@ -149,24 +153,29 @@ class BridgeAdapter(ABC):
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

@@ -2,217 +2,186 @@
LayerZero bridge adapter implementation
"""
from typing import Dict, Any, List, Optional
import json
import asyncio
from eth_utils import to_checksum_address
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,
BridgeAdapter,
BridgeConfig,
BridgeError,
BridgeTimeoutError,
BridgeInsufficientFundsError
BridgeStatus,
SettlementMessage,
SettlementResult,
)
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
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
self.endpoint: Contract | None = None
self.ultra_light_node: Contract | None = 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
)
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
)
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()
)
"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
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']
fee_paid=fees["layerZeroFee"],
)
except Exception as e:
return SettlementResult(
message_id="",
status=BridgeStatus.FAILED,
error_message=str(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']
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']
completed_at=receipt["blockTimestamp"],
)
# Still in progress
return SettlementResult(
message_id=message_id,
status=BridgeStatus.IN_PROGRESS,
transaction_hash=message_id
)
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]:
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
native_fee, zro_fee = await self.endpoint.functions.estimateFees(
dst_chain_id, target_address, payload, False, [0, 0, 0] # payInZRO # adapterParams
).call()
return {
'layerZeroFee': native_fee,
'zroFee': zro_fee,
'total': native_fee + zro_fee
}
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
"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),
@@ -220,38 +189,33 @@ class LayerZeroAdapter(BridgeAdapter):
message.payment_amount,
to_checksum_address(message.payment_token),
message.nonce,
bytes.fromhex(message.signature)
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
}
target_addresses = {1: "0x...", 137: "0x...", 56: "0x...", 42161: "0x..."} # Ethereum # Polygon # BSC # 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],
@@ -259,28 +223,28 @@ class LayerZeroAdapter(BridgeAdapter):
payload,
message.payment_amount,
[0, 0, 0],
message.nonce
).estimateGas({'from': await self._get_signer_address()})
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]:
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

View File

@@ -2,36 +2,30 @@
Settlement hooks for coordinator API integration
"""
from typing import Dict, Any, Optional, List
from datetime import datetime
import asyncio
import logging
from datetime import datetime
from typing import Any
logger = logging.getLogger(__name__)
from .manager import BridgeManager
from .bridges.base import (
SettlementMessage,
SettlementResult,
BridgeStatus
)
from ..models.job import Job
from ..models.receipt import Receipt
from .bridges.base import BridgeStatus, SettlementMessage, SettlementResult
from .manager import BridgeManager
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):
@@ -40,7 +34,7 @@ class SettlementHook:
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
@@ -49,59 +43,49 @@ class SettlementHook:
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
self, job_id: str, target_chain_id: int, bridge_name: str | None = None, options: dict[str, Any] | None = 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
)
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]:
self, job_id: str, target_chain_id: int, bridge_name: str | None = 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(),
@@ -112,101 +96,94 @@ class SettlementHook:
payment_amount=job.payment_amount or 0,
payment_token=job.payment_token or "AITBC",
nonce=await self._generate_nonce(),
signature="" # Not needed for estimation
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]:
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]]:
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'
message, priority=job.settlement_priority or "cost"
)
# Send settlement
result = await self.bridge_manager.settle_cross_chain(
message,
bridge_name=bridge_name
)
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:
async def _create_settlement_message(self, job: Job, options: dict[str, Any] | None = 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,
@@ -219,55 +196,53 @@ class SettlementHook:
nonce=await self._generate_nonce(),
signature=signature,
gas_limit=job.settlement_gas_limit,
privacy_level=options.get("privacy_level") if options else None
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
)
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
@@ -276,18 +251,18 @@ class SettlementHook:
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]:
async def process_batch(self) -> list[SettlementResult]:
"""Process a batch of settlements"""
# This would process queued jobs in batches
# For now, return empty list
@@ -296,33 +271,31 @@ class BatchSettlementHook:
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']
)
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

@@ -2,161 +2,129 @@
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 datetime import datetime, timedelta
from typing import Any
from .bridges.base import (
BridgeAdapter,
BridgeConfig,
SettlementMessage,
SettlementResult,
BridgeStatus,
BridgeError
)
from .bridges.base import BridgeAdapter, BridgeConfig, BridgeError, BridgeStatus, SettlementMessage, SettlementResult
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.adapters: dict[str, BridgeAdapter] = {}
self.default_adapter: str | None = None
self.storage = storage
self._initialized = False
async def initialize(self, configs: Dict[str, BridgeConfig]) -> None:
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
self, message: SettlementMessage, bridge_name: str | None = 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
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
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
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)
)
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]:
if stored["status"] in [BridgeStatus.COMPLETED, BridgeStatus.FAILED]:
return SettlementResult(**stored)
# Otherwise check with bridge
adapter = self.adapters.get(stored['bridge_name'])
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
)
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]:
async def estimate_settlement_cost(self, message: SettlementMessage, bridge_name: str | None = 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)
@@ -168,166 +136,149 @@ class BridgeManager:
await adapter.validate_message(message)
results[name] = await adapter.estimate_cost(message)
except Exception as e:
results[name] = {'error': str(e)}
results[name] = {"error": str(e)}
return results
async def get_optimal_bridge(
self,
message: SettlementMessage,
priority: str = 'cost' # 'cost' or 'speed'
) -> str:
async def get_optimal_bridge(self, message: SettlementMessage, priority: str = "cost") -> str: # 'cost' or 'speed'
"""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
}
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':
if priority == "cost":
# Select cheapest
optimal = min(valid_estimates.items(), key=lambda x: x[1]['total'])
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]:
self, messages: list[SettlementMessage], bridge_name: str | None = 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)
))
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:
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'])
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
)
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]]:
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]]:
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)
"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"
message_id=message_id, status=BridgeStatus.FAILED, error_message="Settlement timed out"
)
def _get_adapter(self, bridge_name: Optional[str] = None) -> BridgeAdapter:
def _get_adapter(self, bridge_name: str | None = 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

View File

@@ -2,13 +2,12 @@
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
import json
from datetime import datetime, timedelta
from typing import Any
from .bridges.base import SettlementMessage, SettlementResult, BridgeStatus
from .bridges.base import BridgeStatus, SettlementMessage
class SettlementStorage:
@@ -57,10 +56,10 @@ class SettlementStorage:
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,
status: BridgeStatus | None = None,
transaction_hash: str | None = None,
error_message: str | None = None,
completed_at: datetime | None = None,
) -> None:
"""Update settlement record"""
updates = []
@@ -97,14 +96,14 @@ class SettlementStorage:
params.append(message_id)
query = f"""
UPDATE settlements
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]]:
async def get_settlement(self, message_id: str) -> dict[str, Any] | None:
"""Get settlement by message ID"""
query = """
SELECT * FROM settlements WHERE message_id = $1
@@ -124,11 +123,11 @@ class SettlementStorage:
return settlement
async def get_settlements_by_job(self, job_id: str) -> List[Dict[str, Any]]:
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
SELECT * FROM settlements
WHERE job_id = $1
ORDER BY created_at DESC
"""
@@ -143,12 +142,10 @@ class SettlementStorage:
return settlements
async def get_pending_settlements(
self, bridge_name: Optional[str] = None
) -> List[Dict[str, Any]]:
async def get_pending_settlements(self, bridge_name: str | None = None) -> list[dict[str, Any]]:
"""Get all pending settlements"""
query = """
SELECT * FROM settlements
SELECT * FROM settlements
WHERE status = 'pending' OR status = 'in_progress'
"""
params = []
@@ -172,9 +169,9 @@ class SettlementStorage:
async def get_settlement_stats(
self,
bridge_name: Optional[str] = None,
time_range: Optional[int] = None, # hours
) -> Dict[str, Any]:
bridge_name: str | None = None,
time_range: int | None = None, # hours
) -> dict[str, Any]:
"""Get settlement statistics"""
conditions = []
params = []
@@ -193,13 +190,13 @@ class SettlementStorage:
where_clause = "WHERE " + " AND ".join(conditions) if conditions else ""
query = f"""
SELECT
SELECT
bridge_name,
status,
COUNT(*) as count,
AVG(payment_amount) as avg_amount,
SUM(payment_amount) as total_amount
FROM settlements
FROM settlements
{where_clause}
GROUP BY bridge_name, status
"""
@@ -214,12 +211,8 @@ class SettlementStorage:
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,
"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
@@ -227,8 +220,8 @@ class SettlementStorage:
async def cleanup_old_settlements(self, days: int = 30) -> int:
"""Clean up old completed settlements"""
query = """
DELETE FROM settlements
WHERE status IN ('completed', 'failed')
DELETE FROM settlements
WHERE status IN ('completed', 'failed')
AND created_at < NOW() - INTERVAL $1 days
"""
@@ -241,7 +234,7 @@ class InMemorySettlementStorage(SettlementStorage):
"""In-memory storage implementation for testing"""
def __init__(self):
self.settlements: Dict[str, Dict[str, Any]] = {}
self.settlements: dict[str, dict[str, Any]] = {}
self._lock = asyncio.Lock()
async def store_settlement(
@@ -272,10 +265,10 @@ class InMemorySettlementStorage(SettlementStorage):
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,
status: BridgeStatus | None = None,
transaction_hash: str | None = None,
error_message: str | None = None,
completed_at: datetime | None = None,
) -> None:
async with self._lock:
if message_id not in self.settlements:
@@ -294,23 +287,17 @@ class InMemorySettlementStorage(SettlementStorage):
settlement["updated_at"] = datetime.utcnow()
async def get_settlement(self, message_id: str) -> Optional[Dict[str, Any]]:
async def get_settlement(self, message_id: str) -> dict[str, Any] | None:
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 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 def get_pending_settlements(self, bridge_name: str | None = None) -> list[dict[str, Any]]:
async with self._lock:
pending = [
s
for s in self.settlements.values()
if s["status"] in ["pending", "in_progress"]
]
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]
@@ -318,8 +305,8 @@ class InMemorySettlementStorage(SettlementStorage):
return pending
async def get_settlement_stats(
self, bridge_name: Optional[str] = None, time_range: Optional[int] = None
) -> Dict[str, Any]:
self, bridge_name: str | None = None, time_range: int | None = None
) -> dict[str, Any]:
async with self._lock:
stats = {}
@@ -352,9 +339,7 @@ class InMemorySettlementStorage(SettlementStorage):
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"]
)
status_data["avg_amount"] = status_data["total_amount"] / status_data["count"]
return stats
@@ -365,10 +350,7 @@ class InMemorySettlementStorage(SettlementStorage):
to_delete = [
msg_id
for msg_id, settlement in self.settlements.items()
if (
settlement["status"] in ["completed", "failed"]
and settlement["created_at"] < cutoff
)
if (settlement["status"] in ["completed", "failed"] and settlement["created_at"] < cutoff)
]
for msg_id in to_delete: