Add blockchain event bridge service with smart contract event integration
Some checks failed
Blockchain Synchronization Verification / sync-verification (push) Failing after 2s
Integration Tests / test-service-integration (push) Failing after 15s
Multi-Node Blockchain Health Monitoring / health-check (push) Successful in 2s
P2P Network Verification / p2p-verification (push) Successful in 2s
Python Tests / test-python (push) Successful in 12s
Security Scanning / security-scan (push) Successful in 41s
Systemd Sync / sync-systemd (push) Successful in 7s
Some checks failed
Blockchain Synchronization Verification / sync-verification (push) Failing after 2s
Integration Tests / test-service-integration (push) Failing after 15s
Multi-Node Blockchain Health Monitoring / health-check (push) Successful in 2s
P2P Network Verification / p2p-verification (push) Successful in 2s
Python Tests / test-python (push) Successful in 12s
Security Scanning / security-scan (push) Successful in 41s
Systemd Sync / sync-systemd (push) Successful in 7s
- Phase 1: Core bridge service with gossip broker subscription - Phase 2: Smart contract event integration via eth_getLogs RPC endpoint - Add contract event subscriber for AgentStaking, PerformanceVerifier, Marketplace, Bounty, CrossChainBridge - Add contract event handlers in agent_daemon.py and marketplace.py - Add systemd service file for blockchain-event-bridge - Update blockchain node router.py with eth_getLogs endpoint - Add configuration for contract addresses - Add tests for contract subscriber and handlers (27 tests passing)
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
"""Blockchain Event Bridge - Connects AITBC blockchain events to OpenClaw agent triggers."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
@@ -0,0 +1,7 @@
|
||||
"""Action handler modules for OpenClaw agent triggers."""
|
||||
|
||||
from .coordinator_api import CoordinatorAPIHandler
|
||||
from .agent_daemon import AgentDaemonHandler
|
||||
from .marketplace import MarketplaceHandler
|
||||
|
||||
__all__ = ["CoordinatorAPIHandler", "AgentDaemonHandler", "MarketplaceHandler"]
|
||||
@@ -0,0 +1,246 @@
|
||||
"""Agent daemon action handler for triggering autonomous agent responses."""
|
||||
|
||||
import httpx
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AgentDaemonHandler:
|
||||
"""Handles actions that trigger the agent daemon to process transactions."""
|
||||
|
||||
def __init__(self, blockchain_rpc_url: str) -> None:
|
||||
self.blockchain_rpc_url = blockchain_rpc_url.rstrip("/")
|
||||
self._client: Optional[httpx.AsyncClient] = None
|
||||
|
||||
async def _get_client(self) -> httpx.AsyncClient:
|
||||
"""Get or create HTTP client."""
|
||||
if self._client is None:
|
||||
self._client = httpx.AsyncClient(
|
||||
base_url=self.blockchain_rpc_url,
|
||||
timeout=30.0,
|
||||
)
|
||||
return self._client
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Close HTTP client."""
|
||||
if self._client:
|
||||
await self._client.aclose()
|
||||
self._client = None
|
||||
|
||||
async def handle_transaction(self, tx_data: Dict[str, Any]) -> None:
|
||||
"""Handle a transaction that may require agent daemon response."""
|
||||
tx_hash = tx_data.get("hash", "unknown")
|
||||
tx_type = tx_data.get("type", "unknown")
|
||||
recipient = tx_data.get("to")
|
||||
|
||||
logger.info(f"Checking transaction {tx_hash} for agent daemon trigger")
|
||||
|
||||
# Check if this is a message to an agent wallet
|
||||
if self._is_agent_transaction(tx_data):
|
||||
await self._notify_agent_daemon(tx_data)
|
||||
|
||||
def _is_agent_transaction(self, tx_data: Dict[str, Any]) -> bool:
|
||||
"""Check if transaction is addressed to an agent wallet."""
|
||||
# In a real implementation, this would check against a registry of agent addresses
|
||||
# For now, we'll check if the transaction has a payload that looks like an agent message
|
||||
payload = tx_data.get("payload", {})
|
||||
|
||||
# Check for agent message indicators
|
||||
if isinstance(payload, dict):
|
||||
# Check for trigger message or agent-specific fields
|
||||
if "trigger" in payload or "agent" in payload or "command" in payload:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
async def _notify_agent_daemon(self, tx_data: Dict[str, Any]) -> None:
|
||||
"""Notify agent daemon about a transaction requiring processing."""
|
||||
try:
|
||||
# The agent daemon currently polls the blockchain database directly
|
||||
# This handler could be enhanced to send a direct notification
|
||||
# For now, we'll log that the agent daemon should pick this up on its next poll
|
||||
|
||||
tx_hash = tx_data.get("hash", "unknown")
|
||||
recipient = tx_data.get("to")
|
||||
|
||||
logger.info(f"Agent daemon should process transaction {tx_hash} to {recipient}")
|
||||
|
||||
# Future enhancement: send direct notification via agent-coordinator API
|
||||
# client = await self._get_client()
|
||||
# response = await client.post(f"/v1/agents/{recipient}/notify", json=tx_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error notifying agent daemon: {e}", exc_info=True)
|
||||
|
||||
# Phase 2: Contract event handlers
|
||||
async def handle_staking_event(self, event_log: Dict[str, Any]) -> None:
|
||||
"""Handle AgentStaking contract event."""
|
||||
event_type = event_log.get("topics", [""])[0] if event_log.get("topics") else "Unknown"
|
||||
logger.info(f"Handling staking event: {event_type}")
|
||||
|
||||
# Route based on event type
|
||||
if "StakeCreated" in event_type:
|
||||
await self._handle_stake_created(event_log)
|
||||
elif "RewardsDistributed" in event_type:
|
||||
await self._handle_rewards_distributed(event_log)
|
||||
elif "AgentTierUpdated" in event_type:
|
||||
await self._handle_agent_tier_updated(event_log)
|
||||
|
||||
async def _handle_stake_created(self, event_log: Dict[str, Any]) -> None:
|
||||
"""Handle StakeCreated event."""
|
||||
try:
|
||||
# Extract event data
|
||||
data = event_log.get("data", "{}")
|
||||
logger.info(f"StakeCreated event: {data}")
|
||||
|
||||
# Call coordinator API to update agent reputation
|
||||
# This would call the reputation service to update agent tier based on stake
|
||||
logger.info("Would call coordinator API reputation service to update agent stake")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling StakeCreated event: {e}", exc_info=True)
|
||||
|
||||
async def _handle_rewards_distributed(self, event_log: Dict[str, Any]) -> None:
|
||||
"""Handle RewardsDistributed event."""
|
||||
try:
|
||||
data = event_log.get("data", "{}")
|
||||
logger.info(f"RewardsDistributed event: {data}")
|
||||
|
||||
# Call coordinator API to update agent rewards
|
||||
logger.info("Would call coordinator API to update agent rewards")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling RewardsDistributed event: {e}", exc_info=True)
|
||||
|
||||
async def _handle_agent_tier_updated(self, event_log: Dict[str, Any]) -> None:
|
||||
"""Handle AgentTierUpdated event."""
|
||||
try:
|
||||
data = event_log.get("data", "{}")
|
||||
logger.info(f"AgentTierUpdated event: {data}")
|
||||
|
||||
# Call coordinator API to update agent tier
|
||||
logger.info("Would call coordinator API reputation service to update agent tier")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling AgentTierUpdated event: {e}", exc_info=True)
|
||||
|
||||
async def handle_performance_event(self, event_log: Dict[str, Any]) -> None:
|
||||
"""Handle PerformanceVerifier contract event."""
|
||||
event_type = event_log.get("topics", [""])[0] if event_log.get("topics") else "Unknown"
|
||||
logger.info(f"Handling performance event: {event_type}")
|
||||
|
||||
# Route based on event type
|
||||
if "PerformanceVerified" in event_type:
|
||||
await self._handle_performance_verified(event_log)
|
||||
elif "PenaltyApplied" in event_type:
|
||||
await self._handle_penalty_applied(event_log)
|
||||
elif "RewardIssued" in event_type:
|
||||
await self._handle_reward_issued(event_log)
|
||||
|
||||
async def _handle_performance_verified(self, event_log: Dict[str, Any]) -> None:
|
||||
"""Handle PerformanceVerified event."""
|
||||
try:
|
||||
data = event_log.get("data", "{}")
|
||||
logger.info(f"PerformanceVerified event: {data}")
|
||||
|
||||
# Call coordinator API to update performance metrics
|
||||
logger.info("Would call coordinator API performance service to update metrics")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling PerformanceVerified event: {e}", exc_info=True)
|
||||
|
||||
async def _handle_penalty_applied(self, event_log: Dict[str, Any]) -> None:
|
||||
"""Handle PenaltyApplied event."""
|
||||
try:
|
||||
data = event_log.get("data", "{}")
|
||||
logger.info(f"PenaltyApplied event: {data}")
|
||||
|
||||
# Call coordinator API to update agent penalties
|
||||
logger.info("Would call coordinator API performance service to apply penalty")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling PenaltyApplied event: {e}", exc_info=True)
|
||||
|
||||
async def _handle_reward_issued(self, event_log: Dict[str, Any]) -> None:
|
||||
"""Handle RewardIssued event."""
|
||||
try:
|
||||
data = event_log.get("data", "{}")
|
||||
logger.info(f"RewardIssued event: {data}")
|
||||
|
||||
# Call coordinator API to update agent rewards
|
||||
logger.info("Would call coordinator API performance service to issue reward")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling RewardIssued event: {e}", exc_info=True)
|
||||
|
||||
async def handle_bounty_event(self, event_log: Dict[str, Any]) -> None:
|
||||
"""Handle BountyIntegration contract event."""
|
||||
event_type = event_log.get("topics", [""])[0] if event_log.get("topics") else "Unknown"
|
||||
logger.info(f"Handling bounty event: {event_type}")
|
||||
|
||||
# Route based on event type
|
||||
if "BountyCreated" in event_type:
|
||||
await self._handle_bounty_created(event_log)
|
||||
elif "BountyCompleted" in event_type:
|
||||
await self._handle_bounty_completed(event_log)
|
||||
|
||||
async def _handle_bounty_created(self, event_log: Dict[str, Any]) -> None:
|
||||
"""Handle BountyCreated event."""
|
||||
try:
|
||||
data = event_log.get("data", "{}")
|
||||
logger.info(f"BountyCreated event: {data}")
|
||||
|
||||
# Call coordinator API to sync new bounty
|
||||
logger.info("Would call coordinator API bounty service to sync bounty")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling BountyCreated event: {e}", exc_info=True)
|
||||
|
||||
async def _handle_bounty_completed(self, event_log: Dict[str, Any]) -> None:
|
||||
"""Handle BountyCompleted event."""
|
||||
try:
|
||||
data = event_log.get("data", "{}")
|
||||
logger.info(f"BountyCompleted event: {data}")
|
||||
|
||||
# Call coordinator API to complete bounty
|
||||
logger.info("Would call coordinator API bounty service to complete bounty")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling BountyCompleted event: {e}", exc_info=True)
|
||||
|
||||
async def handle_bridge_event(self, event_log: Dict[str, Any]) -> None:
|
||||
"""Handle CrossChainBridge contract event."""
|
||||
event_type = event_log.get("topics", [""])[0] if event_log.get("topics") else "Unknown"
|
||||
logger.info(f"Handling bridge event: {event_type}")
|
||||
|
||||
# Route based on event type
|
||||
if "BridgeInitiated" in event_type:
|
||||
await self._handle_bridge_initiated(event_log)
|
||||
elif "BridgeCompleted" in event_type:
|
||||
await self._handle_bridge_completed(event_log)
|
||||
|
||||
async def _handle_bridge_initiated(self, event_log: Dict[str, Any]) -> None:
|
||||
"""Handle BridgeInitiated event."""
|
||||
try:
|
||||
data = event_log.get("data", "{}")
|
||||
logger.info(f"BridgeInitiated event: {data}")
|
||||
|
||||
# Call coordinator API to track bridge
|
||||
logger.info("Would call coordinator API cross-chain service to track bridge")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling BridgeInitiated event: {e}", exc_info=True)
|
||||
|
||||
async def _handle_bridge_completed(self, event_log: Dict[str, Any]) -> None:
|
||||
"""Handle BridgeCompleted event."""
|
||||
try:
|
||||
data = event_log.get("data", "{}")
|
||||
logger.info(f"BridgeCompleted event: {data}")
|
||||
|
||||
# Call coordinator API to complete bridge
|
||||
logger.info("Would call coordinator API cross-chain service to complete bridge")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling BridgeCompleted event: {e}", exc_info=True)
|
||||
@@ -0,0 +1,125 @@
|
||||
"""Coordinator API action handler for triggering OpenClaw agent actions."""
|
||||
|
||||
import httpx
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CoordinatorAPIHandler:
|
||||
"""Handles actions that trigger coordinator API endpoints."""
|
||||
|
||||
def __init__(self, base_url: str, api_key: Optional[str] = None) -> None:
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.api_key = api_key
|
||||
self._client: Optional[httpx.AsyncClient] = None
|
||||
|
||||
async def _get_client(self) -> httpx.AsyncClient:
|
||||
"""Get or create HTTP client."""
|
||||
if self._client is None:
|
||||
headers = {}
|
||||
if self.api_key:
|
||||
headers["Authorization"] = f"Bearer {self.api_key}"
|
||||
|
||||
self._client = httpx.AsyncClient(
|
||||
base_url=self.base_url,
|
||||
headers=headers,
|
||||
timeout=30.0,
|
||||
)
|
||||
return self._client
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Close HTTP client."""
|
||||
if self._client:
|
||||
await self._client.aclose()
|
||||
self._client = None
|
||||
|
||||
async def handle_block(self, block_data: Dict[str, Any], transactions: List[Dict[str, Any]]) -> None:
|
||||
"""Handle a new block by triggering coordinator API actions."""
|
||||
logger.info(f"Handling block {block_data.get('height')} with {len(transactions)} transactions")
|
||||
|
||||
# Filter relevant transactions (AI jobs, agent messages, etc.)
|
||||
for tx in transactions:
|
||||
await self.handle_transaction(tx)
|
||||
|
||||
async def handle_transaction(self, tx_data: Dict[str, Any]) -> None:
|
||||
"""Handle a single transaction."""
|
||||
tx_type = tx_data.get("type", "unknown")
|
||||
tx_hash = tx_data.get("hash", "unknown")
|
||||
|
||||
logger.info(f"Handling transaction {tx_hash} of type {tx_type}")
|
||||
|
||||
# Route based on transaction type
|
||||
if tx_type == "ai_job":
|
||||
await self._trigger_ai_job_processing(tx_data)
|
||||
elif tx_type == "agent_message":
|
||||
await self._trigger_agent_message_processing(tx_data)
|
||||
elif tx_type == "marketplace":
|
||||
await self._trigger_marketplace_update(tx_data)
|
||||
|
||||
async def _trigger_ai_job_processing(self, tx_data: Dict[str, Any]) -> None:
|
||||
"""Trigger AI job processing via coordinator API."""
|
||||
try:
|
||||
client = await self._get_client()
|
||||
|
||||
# Extract job details from transaction payload
|
||||
payload = tx_data.get("payload", {})
|
||||
job_id = payload.get("job_id")
|
||||
|
||||
if job_id:
|
||||
# Notify coordinator about new AI job
|
||||
response = await client.post(f"/v1/ai-jobs/{job_id}/notify", json=tx_data)
|
||||
response.raise_for_status()
|
||||
logger.info(f"Successfully notified coordinator about AI job {job_id}")
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"HTTP error triggering AI job processing: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error triggering AI job processing: {e}", exc_info=True)
|
||||
|
||||
async def _trigger_agent_message_processing(self, tx_data: Dict[str, Any]) -> None:
|
||||
"""Trigger agent message processing via coordinator API."""
|
||||
try:
|
||||
client = await self._get_client()
|
||||
|
||||
# Extract message details
|
||||
payload = tx_data.get("payload", {})
|
||||
recipient = tx_data.get("to")
|
||||
|
||||
if recipient:
|
||||
# Notify coordinator about agent message
|
||||
response = await client.post(
|
||||
f"/v1/agents/{recipient}/message",
|
||||
json={"transaction": tx_data, "payload": payload}
|
||||
)
|
||||
response.raise_for_status()
|
||||
logger.info(f"Successfully notified coordinator about message to {recipient}")
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"HTTP error triggering agent message processing: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error triggering agent message processing: {e}", exc_info=True)
|
||||
|
||||
async def _trigger_marketplace_update(self, tx_data: Dict[str, Any]) -> None:
|
||||
"""Trigger marketplace state update via coordinator API."""
|
||||
try:
|
||||
client = await self._get_client()
|
||||
|
||||
# Extract marketplace details
|
||||
payload = tx_data.get("payload", {})
|
||||
listing_id = payload.get("listing_id")
|
||||
|
||||
if listing_id:
|
||||
# Update marketplace state
|
||||
response = await client.post(
|
||||
f"/v1/marketplace/{listing_id}/sync",
|
||||
json={"transaction": tx_data}
|
||||
)
|
||||
response.raise_for_status()
|
||||
logger.info(f"Successfully updated marketplace listing {listing_id}")
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"HTTP error triggering marketplace update: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error triggering marketplace update: {e}", exc_info=True)
|
||||
@@ -0,0 +1,119 @@
|
||||
"""Marketplace action handler for triggering marketplace state updates."""
|
||||
|
||||
import httpx
|
||||
import logging
|
||||
from typing import Any, Dict, List
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MarketplaceHandler:
|
||||
"""Handles actions that trigger marketplace state updates."""
|
||||
|
||||
def __init__(self, coordinator_api_url: str, api_key: str | None = None) -> None:
|
||||
self.base_url = coordinator_api_url.rstrip("/")
|
||||
self.api_key = api_key
|
||||
self._client: httpx.AsyncClient | None = None
|
||||
|
||||
async def _get_client(self) -> httpx.AsyncClient:
|
||||
"""Get or create HTTP client."""
|
||||
if self._client is None:
|
||||
headers = {}
|
||||
if self.api_key:
|
||||
headers["Authorization"] = f"Bearer {self.api_key}"
|
||||
|
||||
self._client = httpx.AsyncClient(
|
||||
base_url=self.base_url,
|
||||
headers=headers,
|
||||
timeout=30.0,
|
||||
)
|
||||
return self._client
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Close HTTP client."""
|
||||
if self._client:
|
||||
await self._client.aclose()
|
||||
self._client = None
|
||||
|
||||
async def handle_block(self, block_data: Dict[str, Any], transactions: List[Dict[str, Any]]) -> None:
|
||||
"""Handle a new block by updating marketplace state."""
|
||||
logger.info(f"Processing block {block_data.get('height')} for marketplace updates")
|
||||
|
||||
# Filter marketplace-related transactions
|
||||
marketplace_txs = self._filter_marketplace_transactions(transactions)
|
||||
|
||||
if marketplace_txs:
|
||||
await self._sync_marketplace_state(marketplace_txs)
|
||||
|
||||
def _filter_marketplace_transactions(self, transactions: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""Filter transactions that affect marketplace state."""
|
||||
marketplace_txs = []
|
||||
|
||||
for tx in transactions:
|
||||
tx_type = tx.get("type", "unknown")
|
||||
payload = tx.get("payload", {})
|
||||
|
||||
# Check for marketplace-related transaction types
|
||||
if tx_type in ["marketplace", "listing", "purchase", "service"]:
|
||||
marketplace_txs.append(tx)
|
||||
elif isinstance(payload, dict):
|
||||
# Check for marketplace-related payload fields
|
||||
if any(key in payload for key in ["listing_id", "service_id", "marketplace"]):
|
||||
marketplace_txs.append(tx)
|
||||
|
||||
return marketplace_txs
|
||||
|
||||
async def _sync_marketplace_state(self, transactions: List[Dict[str, Any]]) -> None:
|
||||
"""Synchronize marketplace state with blockchain."""
|
||||
try:
|
||||
client = await self._get_client()
|
||||
|
||||
# Send batch of marketplace transactions for processing
|
||||
response = await client.post(
|
||||
"/v1/marketplace/sync",
|
||||
json={"transactions": transactions}
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
logger.info(f"Successfully synced {len(transactions)} marketplace transactions")
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"HTTP error syncing marketplace state: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error syncing marketplace state: {e}", exc_info=True)
|
||||
|
||||
# Phase 2: Contract event handlers
|
||||
async def handle_contract_event(self, event_log: Dict[str, Any]) -> None:
|
||||
"""Handle AgentServiceMarketplace contract event."""
|
||||
event_type = event_log.get("topics", [""])[0] if event_log.get("topics") else "Unknown"
|
||||
logger.info(f"Handling marketplace contract event: {event_type}")
|
||||
|
||||
# Route based on event type
|
||||
if "ServiceListed" in event_type:
|
||||
await self._handle_service_listed(event_log)
|
||||
elif "ServicePurchased" in event_type:
|
||||
await self._handle_service_purchased(event_log)
|
||||
|
||||
async def _handle_service_listed(self, event_log: Dict[str, Any]) -> None:
|
||||
"""Handle ServiceListed event."""
|
||||
try:
|
||||
data = event_log.get("data", "{}")
|
||||
logger.info(f"ServiceListed event: {data}")
|
||||
|
||||
# Call coordinator API to sync marketplace listing
|
||||
logger.info("Would call coordinator API marketplace service to sync listing")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling ServiceListed event: {e}", exc_info=True)
|
||||
|
||||
async def _handle_service_purchased(self, event_log: Dict[str, Any]) -> None:
|
||||
"""Handle ServicePurchased event."""
|
||||
try:
|
||||
data = event_log.get("data", "{}")
|
||||
logger.info(f"ServicePurchased event: {data}")
|
||||
|
||||
# Call coordinator API to sync marketplace purchase
|
||||
logger.info("Would call coordinator API marketplace service to sync purchase")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling ServicePurchased event: {e}", exc_info=True)
|
||||
@@ -0,0 +1,287 @@
|
||||
"""Core bridge logic for blockchain event to OpenClaw agent trigger mapping."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from .config import Settings
|
||||
from .event_subscribers.blocks import BlockEventSubscriber
|
||||
from .event_subscribers.transactions import TransactionEventSubscriber
|
||||
from .event_subscribers.contracts import ContractEventSubscriber
|
||||
from .action_handlers.coordinator_api import CoordinatorAPIHandler
|
||||
from .action_handlers.agent_daemon import AgentDaemonHandler
|
||||
from .action_handlers.marketplace import MarketplaceHandler
|
||||
from .metrics import (
|
||||
events_received_total,
|
||||
events_processed_total,
|
||||
actions_triggered_total,
|
||||
actions_failed_total,
|
||||
event_processing_duration_seconds,
|
||||
action_execution_duration_seconds,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BlockchainEventBridge:
|
||||
"""Main bridge service connecting blockchain events to OpenClaw agent actions."""
|
||||
|
||||
def __init__(self, settings: Settings) -> None:
|
||||
self.settings = settings
|
||||
self._running = False
|
||||
self._tasks: set[asyncio.Task] = set()
|
||||
|
||||
# Event subscribers
|
||||
self.block_subscriber: Optional[BlockEventSubscriber] = None
|
||||
self.transaction_subscriber: Optional[TransactionEventSubscriber] = None
|
||||
self.contract_subscriber: Optional[ContractEventSubscriber] = None
|
||||
|
||||
# Action handlers
|
||||
self.coordinator_handler: Optional[CoordinatorAPIHandler] = None
|
||||
self.agent_daemon_handler: Optional[AgentDaemonHandler] = None
|
||||
self.marketplace_handler: Optional[MarketplaceHandler] = None
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Start the bridge service."""
|
||||
if self._running:
|
||||
logger.warning("Bridge already running")
|
||||
return
|
||||
|
||||
logger.info("Initializing blockchain event bridge...")
|
||||
|
||||
# Initialize action handlers
|
||||
if self.settings.enable_coordinator_api_trigger:
|
||||
self.coordinator_handler = CoordinatorAPIHandler(
|
||||
self.settings.coordinator_api_url,
|
||||
self.settings.coordinator_api_key,
|
||||
)
|
||||
logger.info("Coordinator API handler initialized")
|
||||
|
||||
if self.settings.enable_agent_daemon_trigger:
|
||||
self.agent_daemon_handler = AgentDaemonHandler(self.settings.blockchain_rpc_url)
|
||||
logger.info("Agent daemon handler initialized")
|
||||
|
||||
if self.settings.enable_marketplace_trigger:
|
||||
self.marketplace_handler = MarketplaceHandler(
|
||||
self.settings.coordinator_api_url,
|
||||
self.settings.coordinator_api_key,
|
||||
)
|
||||
logger.info("Marketplace handler initialized")
|
||||
|
||||
# Initialize event subscribers
|
||||
if self.settings.subscribe_blocks:
|
||||
self.block_subscriber = BlockEventSubscriber(self.settings)
|
||||
self.block_subscriber.set_bridge(self)
|
||||
task = asyncio.create_task(self.block_subscriber.run(), name="block-subscriber")
|
||||
self._tasks.add(task)
|
||||
logger.info("Block event subscriber started")
|
||||
|
||||
if self.settings.subscribe_transactions:
|
||||
self.transaction_subscriber = TransactionEventSubscriber(self.settings)
|
||||
self.transaction_subscriber.set_bridge(self)
|
||||
task = asyncio.create_task(self.transaction_subscriber.run(), name="transaction-subscriber")
|
||||
self._tasks.add(task)
|
||||
logger.info("Transaction event subscriber started")
|
||||
|
||||
# Initialize contract event subscriber (Phase 2)
|
||||
if self.settings.subscribe_contracts:
|
||||
self.contract_subscriber = ContractEventSubscriber(self.settings)
|
||||
self.contract_subscriber.set_bridge(self)
|
||||
task = asyncio.create_task(self.contract_subscriber.run(), name="contract-subscriber")
|
||||
self._tasks.add(task)
|
||||
logger.info("Contract event subscriber started")
|
||||
|
||||
self._running = True
|
||||
logger.info("Blockchain event bridge started successfully")
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stop the bridge service."""
|
||||
if not self._running:
|
||||
return
|
||||
|
||||
logger.info("Stopping blockchain event bridge...")
|
||||
|
||||
# Cancel all tasks
|
||||
for task in self._tasks:
|
||||
task.cancel()
|
||||
|
||||
# Wait for tasks to complete
|
||||
if self._tasks:
|
||||
await asyncio.gather(*self._tasks, return_exceptions=True)
|
||||
|
||||
self._tasks.clear()
|
||||
self._running = False
|
||||
logger.info("Blockchain event bridge stopped")
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
"""Check if the bridge is running."""
|
||||
return self._running
|
||||
|
||||
async def handle_block_event(self, block_data: Dict[str, Any]) -> None:
|
||||
"""Handle a new block event."""
|
||||
event_type = "block"
|
||||
events_received_total.labels(event_type=event_type).inc()
|
||||
|
||||
with event_processing_duration_seconds.labels(event_type=event_type).time():
|
||||
try:
|
||||
# Extract transactions from block
|
||||
transactions = block_data.get("transactions", [])
|
||||
|
||||
# Trigger actions based on block content
|
||||
if transactions and self.settings.enable_coordinator_api_trigger:
|
||||
await self._trigger_coordinator_actions(block_data, transactions)
|
||||
|
||||
if transactions and self.settings.enable_marketplace_trigger:
|
||||
await self._trigger_marketplace_actions(block_data, transactions)
|
||||
|
||||
events_processed_total.labels(event_type=event_type, status="success").inc()
|
||||
logger.info(f"Processed block event: height={block_data.get('height')}, txs={len(transactions)}")
|
||||
|
||||
except Exception as e:
|
||||
events_processed_total.labels(event_type=event_type, status="error").inc()
|
||||
logger.error(f"Error processing block event: {e}", exc_info=True)
|
||||
|
||||
async def handle_transaction_event(self, tx_data: Dict[str, Any]) -> None:
|
||||
"""Handle a transaction event."""
|
||||
event_type = "transaction"
|
||||
events_received_total.labels(event_type=event_type).inc()
|
||||
|
||||
with event_processing_duration_seconds.labels(event_type=event_type).time():
|
||||
try:
|
||||
# Trigger actions based on transaction type
|
||||
if self.settings.enable_agent_daemon_trigger:
|
||||
await self._trigger_agent_daemon_actions(tx_data)
|
||||
|
||||
if self.settings.enable_coordinator_api_trigger:
|
||||
await self._trigger_coordinator_transaction_actions(tx_data)
|
||||
|
||||
events_processed_total.labels(event_type=event_type, status="success").inc()
|
||||
logger.info(f"Processed transaction event: hash={tx_data.get('hash')}")
|
||||
|
||||
except Exception as e:
|
||||
events_processed_total.labels(event_type=event_type, status="error").inc()
|
||||
logger.error(f"Error processing transaction event: {e}", exc_info=True)
|
||||
|
||||
async def _trigger_coordinator_actions(self, block_data: Dict[str, Any], transactions: list) -> None:
|
||||
"""Trigger coordinator API actions based on block data."""
|
||||
if not self.coordinator_handler:
|
||||
return
|
||||
|
||||
with action_execution_duration_seconds.labels(action_type="coordinator_api").time():
|
||||
try:
|
||||
await self.coordinator_handler.handle_block(block_data, transactions)
|
||||
actions_triggered_total.labels(action_type="coordinator_api").inc()
|
||||
except Exception as e:
|
||||
actions_failed_total.labels(action_type="coordinator_api").inc()
|
||||
logger.error(f"Error triggering coordinator API actions: {e}", exc_info=True)
|
||||
|
||||
async def _trigger_marketplace_actions(self, block_data: Dict[str, Any], transactions: list) -> None:
|
||||
"""Trigger marketplace actions based on block data."""
|
||||
if not self.marketplace_handler:
|
||||
return
|
||||
|
||||
with action_execution_duration_seconds.labels(action_type="marketplace").time():
|
||||
try:
|
||||
await self.marketplace_handler.handle_block(block_data, transactions)
|
||||
actions_triggered_total.labels(action_type="marketplace").inc()
|
||||
except Exception as e:
|
||||
actions_failed_total.labels(action_type="marketplace").inc()
|
||||
logger.error(f"Error triggering marketplace actions: {e}", exc_info=True)
|
||||
|
||||
async def _trigger_agent_daemon_actions(self, tx_data: Dict[str, Any]) -> None:
|
||||
"""Trigger agent daemon actions based on transaction data."""
|
||||
if not self.agent_daemon_handler:
|
||||
return
|
||||
|
||||
with action_execution_duration_seconds.labels(action_type="agent_daemon").time():
|
||||
try:
|
||||
await self.agent_daemon_handler.handle_transaction(tx_data)
|
||||
actions_triggered_total.labels(action_type="agent_daemon").inc()
|
||||
except Exception as e:
|
||||
actions_failed_total.labels(action_type="agent_daemon").inc()
|
||||
logger.error(f"Error triggering agent daemon actions: {e}", exc_info=True)
|
||||
|
||||
async def _trigger_coordinator_transaction_actions(self, tx_data: Dict[str, Any]) -> None:
|
||||
"""Trigger coordinator API actions based on transaction data."""
|
||||
if not self.coordinator_handler:
|
||||
return
|
||||
|
||||
with action_execution_duration_seconds.labels(action_type="coordinator_api").time():
|
||||
try:
|
||||
await self.coordinator_handler.handle_transaction(tx_data)
|
||||
actions_triggered_total.labels(action_type="coordinator_api").inc()
|
||||
except Exception as e:
|
||||
actions_failed_total.labels(action_type="coordinator_api").inc()
|
||||
logger.error(f"Error triggering coordinator API transaction actions: {e}", exc_info=True)
|
||||
|
||||
# Phase 2: Contract event handlers
|
||||
async def handle_staking_event(self, event_log: Dict[str, Any]) -> None:
|
||||
"""Handle AgentStaking contract event."""
|
||||
event_type = "staking_event"
|
||||
events_received_total.labels(event_type=event_type).inc()
|
||||
|
||||
with event_processing_duration_seconds.labels(event_type=event_type).time():
|
||||
try:
|
||||
if self.agent_daemon_handler:
|
||||
await self.agent_daemon_handler.handle_staking_event(event_log)
|
||||
events_processed_total.labels(event_type=event_type, status="success").inc()
|
||||
except Exception as e:
|
||||
events_processed_total.labels(event_type=event_type, status="error").inc()
|
||||
logger.error(f"Error processing staking event: {e}", exc_info=True)
|
||||
|
||||
async def handle_performance_event(self, event_log: Dict[str, Any]) -> None:
|
||||
"""Handle PerformanceVerifier contract event."""
|
||||
event_type = "performance_event"
|
||||
events_received_total.labels(event_type=event_type).inc()
|
||||
|
||||
with event_processing_duration_seconds.labels(event_type=event_type).time():
|
||||
try:
|
||||
if self.agent_daemon_handler:
|
||||
await self.agent_daemon_handler.handle_performance_event(event_log)
|
||||
events_processed_total.labels(event_type=event_type, status="success").inc()
|
||||
except Exception as e:
|
||||
events_processed_total.labels(event_type=event_type, status="error").inc()
|
||||
logger.error(f"Error processing performance event: {e}", exc_info=True)
|
||||
|
||||
async def handle_marketplace_event(self, event_log: Dict[str, Any]) -> None:
|
||||
"""Handle AgentServiceMarketplace contract event."""
|
||||
event_type = "marketplace_event"
|
||||
events_received_total.labels(event_type=event_type).inc()
|
||||
|
||||
with event_processing_duration_seconds.labels(event_type=event_type).time():
|
||||
try:
|
||||
if self.marketplace_handler:
|
||||
await self.marketplace_handler.handle_contract_event(event_log)
|
||||
events_processed_total.labels(event_type=event_type, status="success").inc()
|
||||
except Exception as e:
|
||||
events_processed_total.labels(event_type=event_type, status="error").inc()
|
||||
logger.error(f"Error processing marketplace event: {e}", exc_info=True)
|
||||
|
||||
async def handle_bounty_event(self, event_log: Dict[str, Any]) -> None:
|
||||
"""Handle BountyIntegration contract event."""
|
||||
event_type = "bounty_event"
|
||||
events_received_total.labels(event_type=event_type).inc()
|
||||
|
||||
with event_processing_duration_seconds.labels(event_type=event_type).time():
|
||||
try:
|
||||
if self.agent_daemon_handler:
|
||||
await self.agent_daemon_handler.handle_bounty_event(event_log)
|
||||
events_processed_total.labels(event_type=event_type, status="success").inc()
|
||||
except Exception as e:
|
||||
events_processed_total.labels(event_type=event_type, status="error").inc()
|
||||
logger.error(f"Error processing bounty event: {e}", exc_info=True)
|
||||
|
||||
async def handle_bridge_event(self, event_log: Dict[str, Any]) -> None:
|
||||
"""Handle CrossChainBridge contract event."""
|
||||
event_type = "bridge_event"
|
||||
events_received_total.labels(event_type=event_type).inc()
|
||||
|
||||
with event_processing_duration_seconds.labels(event_type=event_type).time():
|
||||
try:
|
||||
if self.agent_daemon_handler:
|
||||
await self.agent_daemon_handler.handle_bridge_event(event_log)
|
||||
events_processed_total.labels(event_type=event_type, status="success").inc()
|
||||
except Exception as e:
|
||||
events_processed_total.labels(event_type=event_type, status="error").inc()
|
||||
logger.error(f"Error processing bridge event: {e}", exc_info=True)
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Configuration settings for blockchain event bridge."""
|
||||
|
||||
from typing import Optional
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Configuration settings for the blockchain event bridge."""
|
||||
|
||||
# Service configuration
|
||||
app_name: str = "Blockchain Event Bridge"
|
||||
bind_host: str = Field(default="127.0.0.1")
|
||||
bind_port: int = Field(default=8204)
|
||||
|
||||
# Blockchain RPC
|
||||
blockchain_rpc_url: str = Field(default="http://localhost:8006")
|
||||
|
||||
# Gossip broker
|
||||
gossip_backend: str = Field(default="memory") # memory, broadcast, redis
|
||||
gossip_broadcast_url: Optional[str] = Field(default=None)
|
||||
|
||||
# Coordinator API
|
||||
coordinator_api_url: str = Field(default="http://localhost:8011")
|
||||
coordinator_api_key: Optional[str] = Field(default=None)
|
||||
|
||||
# Event subscription filters
|
||||
subscribe_blocks: bool = Field(default=True)
|
||||
subscribe_transactions: bool = Field(default=True)
|
||||
subscribe_contracts: bool = Field(default=False) # Phase 2
|
||||
|
||||
# Smart contract addresses (Phase 2)
|
||||
agent_staking_address: Optional[str] = Field(default=None)
|
||||
performance_verifier_address: Optional[str] = Field(default=None)
|
||||
marketplace_address: Optional[str] = Field(default=None)
|
||||
bounty_address: Optional[str] = Field(default=None)
|
||||
bridge_address: Optional[str] = Field(default=None)
|
||||
|
||||
# Action handler enable/disable flags
|
||||
enable_agent_daemon_trigger: bool = Field(default=True)
|
||||
enable_coordinator_api_trigger: bool = Field(default=True)
|
||||
enable_marketplace_trigger: bool = Field(default=True)
|
||||
|
||||
# Polling configuration (Phase 3)
|
||||
enable_polling: bool = Field(default=False)
|
||||
polling_interval_seconds: int = Field(default=60)
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
env_file_encoding = "utf-8"
|
||||
extra = "ignore"
|
||||
|
||||
|
||||
settings = Settings()
|
||||
@@ -0,0 +1,7 @@
|
||||
"""Event subscriber modules for blockchain events."""
|
||||
|
||||
from .blocks import BlockEventSubscriber
|
||||
from .transactions import TransactionEventSubscriber
|
||||
from .contracts import ContractEventSubscriber
|
||||
|
||||
__all__ = ["BlockEventSubscriber", "TransactionEventSubscriber", "ContractEventSubscriber"]
|
||||
@@ -0,0 +1,107 @@
|
||||
"""Block event subscriber for gossip broker."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Dict
|
||||
|
||||
from ..config import Settings
|
||||
from ..metrics import event_queue_size, gossip_subscribers_total
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..bridge import BlockchainEventBridge
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BlockEventSubscriber:
|
||||
"""Subscribes to block events from the gossip broker."""
|
||||
|
||||
def __init__(self, settings: Settings) -> None:
|
||||
self.settings = settings
|
||||
self._running = False
|
||||
self._bridge: "BlockchainEventBridge | None" = None
|
||||
self._subscription = None
|
||||
|
||||
def set_bridge(self, bridge: "BlockchainEventBridge") -> None:
|
||||
"""Set the bridge instance for event handling."""
|
||||
self._bridge = bridge
|
||||
|
||||
async def run(self) -> None:
|
||||
"""Run the block event subscriber."""
|
||||
if self._running:
|
||||
logger.warning("Block event subscriber already running")
|
||||
return
|
||||
|
||||
self._running = True
|
||||
logger.info("Starting block event subscriber...")
|
||||
|
||||
# Import gossip broker from blockchain-node
|
||||
try:
|
||||
# Add blockchain-node to path for import
|
||||
import sys
|
||||
from pathlib import Path
|
||||
blockchain_node_src = Path("/opt/aitbc/apps/blockchain-node/src")
|
||||
if str(blockchain_node_src) not in sys.path:
|
||||
sys.path.insert(0, str(blockchain_node_src))
|
||||
|
||||
from aitbc_chain.gossip.broker import create_backend, GossipBroker
|
||||
|
||||
# Create gossip backend
|
||||
backend = create_backend(
|
||||
self.settings.gossip_backend,
|
||||
broadcast_url=self.settings.gossip_broadcast_url
|
||||
)
|
||||
self._broker = GossipBroker(backend)
|
||||
|
||||
# Subscribe to blocks topic
|
||||
self._subscription = await self._broker.subscribe("blocks", max_queue_size=100)
|
||||
gossip_subscribers_total.set(1)
|
||||
|
||||
logger.info("Successfully subscribed to blocks topic")
|
||||
|
||||
except ImportError as e:
|
||||
logger.error(f"Failed to import gossip broker: {e}")
|
||||
logger.info("Using mock implementation for development")
|
||||
await self._run_mock()
|
||||
return
|
||||
|
||||
# Process block events
|
||||
while self._running:
|
||||
try:
|
||||
block_data = await self._subscription.get()
|
||||
event_queue_size.labels(topic="blocks").set(self._subscription.queue.qsize())
|
||||
|
||||
logger.info(f"Received block event: height={block_data.get('height')}")
|
||||
|
||||
if self._bridge:
|
||||
await self._bridge.handle_block_event(block_data)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.info("Block event subscriber cancelled")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing block event: {e}", exc_info=True)
|
||||
await asyncio.sleep(1) # Avoid tight error loop
|
||||
|
||||
async def _run_mock(self) -> None:
|
||||
"""Run a mock subscriber for development/testing when gossip broker is unavailable."""
|
||||
logger.warning("Using mock block event subscriber - no real events will be processed")
|
||||
await asyncio.sleep(60) # Keep alive but do nothing
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stop the block event subscriber."""
|
||||
if not self._running:
|
||||
return
|
||||
|
||||
logger.info("Stopping block event subscriber...")
|
||||
self._running = False
|
||||
|
||||
if self._subscription:
|
||||
self._subscription.close()
|
||||
|
||||
if hasattr(self, '_broker'):
|
||||
await self._broker.shutdown()
|
||||
|
||||
gossip_subscribers_total.set(0)
|
||||
logger.info("Block event subscriber stopped")
|
||||
@@ -0,0 +1,223 @@
|
||||
"""Contract event subscriber for smart contract event monitoring."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
from ..config import Settings
|
||||
from ..metrics import event_queue_size
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..bridge import BlockchainEventBridge
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ContractEventSubscriber:
|
||||
"""Subscribes to smart contract events via blockchain RPC."""
|
||||
|
||||
def __init__(self, settings: Settings) -> None:
|
||||
self.settings = settings
|
||||
self._running = False
|
||||
self._bridge: "BlockchainEventBridge | None" = None
|
||||
self._client: Optional[httpx.AsyncClient] = None
|
||||
|
||||
# Contract addresses from configuration
|
||||
self.contract_addresses: Dict[str, str] = {
|
||||
"AgentStaking": settings.agent_staking_address or "",
|
||||
"PerformanceVerifier": settings.performance_verifier_address or "",
|
||||
"AgentServiceMarketplace": settings.marketplace_address or "",
|
||||
"BountyIntegration": settings.bounty_address or "",
|
||||
"CrossChainBridge": settings.bridge_address or "",
|
||||
}
|
||||
|
||||
# Event topics/signatures for each contract
|
||||
self.event_topics: Dict[str, list[str]] = {
|
||||
"AgentStaking": [
|
||||
"StakeCreated",
|
||||
"RewardsDistributed",
|
||||
"AgentTierUpdated",
|
||||
],
|
||||
"PerformanceVerifier": [
|
||||
"PerformanceVerified",
|
||||
"PenaltyApplied",
|
||||
"RewardIssued",
|
||||
],
|
||||
"AgentServiceMarketplace": [
|
||||
"ServiceListed",
|
||||
"ServicePurchased",
|
||||
],
|
||||
"BountyIntegration": [
|
||||
"BountyCreated",
|
||||
"BountyCompleted",
|
||||
],
|
||||
"CrossChainBridge": [
|
||||
"BridgeInitiated",
|
||||
"BridgeCompleted",
|
||||
],
|
||||
}
|
||||
|
||||
# Track last processed block for each contract
|
||||
self.last_processed_blocks: Dict[str, int] = {}
|
||||
|
||||
def set_bridge(self, bridge: "BlockchainEventBridge") -> None:
|
||||
"""Set the bridge instance for event handling."""
|
||||
self._bridge = bridge
|
||||
|
||||
async def _get_client(self) -> httpx.AsyncClient:
|
||||
"""Get or create HTTP client."""
|
||||
if self._client is None:
|
||||
self._client = httpx.AsyncClient(
|
||||
base_url=self.settings.blockchain_rpc_url,
|
||||
timeout=30.0,
|
||||
)
|
||||
return self._client
|
||||
|
||||
async def run(self) -> None:
|
||||
"""Run the contract event subscriber."""
|
||||
if not self.settings.subscribe_contracts:
|
||||
logger.info("Contract event subscription disabled")
|
||||
return
|
||||
|
||||
if self._running:
|
||||
logger.warning("Contract event subscriber already running")
|
||||
return
|
||||
|
||||
self._running = True
|
||||
logger.info("Starting contract event subscriber...")
|
||||
|
||||
# Initialize last processed blocks from current chain height
|
||||
await self._initialize_block_tracking()
|
||||
|
||||
while self._running:
|
||||
try:
|
||||
await self._poll_contract_events()
|
||||
await asyncio.sleep(self.settings.polling_interval_seconds)
|
||||
except asyncio.CancelledError:
|
||||
logger.info("Contract event subscriber cancelled")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Error in contract event subscriber: {e}", exc_info=True)
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def _initialize_block_tracking(self) -> None:
|
||||
"""Initialize block tracking from current chain height."""
|
||||
try:
|
||||
client = await self._get_client()
|
||||
response = await client.get("/head")
|
||||
if response.status_code == 200:
|
||||
head_data = response.json()
|
||||
current_height = head_data.get("height", 0)
|
||||
for contract in self.contract_addresses:
|
||||
if self.contract_addresses[contract]:
|
||||
self.last_processed_blocks[contract] = current_height
|
||||
logger.info(f"Initialized block tracking at height {current_height}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error initializing block tracking: {e}")
|
||||
|
||||
async def _poll_contract_events(self) -> None:
|
||||
"""Poll for contract events from blockchain."""
|
||||
client = await self._get_client()
|
||||
|
||||
for contract_name, contract_address in self.contract_addresses.items():
|
||||
if not contract_address:
|
||||
continue
|
||||
|
||||
try:
|
||||
# Get current chain height
|
||||
response = await client.get("/head")
|
||||
if response.status_code != 200:
|
||||
logger.error(f"Failed to get chain head: {response.status_code}")
|
||||
continue
|
||||
|
||||
head_data = response.json()
|
||||
current_height = head_data.get("height", 0)
|
||||
last_height = self.last_processed_blocks.get(contract_name, current_height - 100)
|
||||
|
||||
# Query events for this contract
|
||||
logs_response = await client.post(
|
||||
"/eth_getLogs",
|
||||
json={
|
||||
"address": contract_address,
|
||||
"from_block": last_height + 1,
|
||||
"to_block": current_height,
|
||||
"topics": self.event_topics.get(contract_name, []),
|
||||
}
|
||||
)
|
||||
|
||||
if logs_response.status_code != 200:
|
||||
logger.error(f"Failed to get logs for {contract_name}: {logs_response.status_code}")
|
||||
continue
|
||||
|
||||
logs_data = logs_response.json()
|
||||
logs = logs_data.get("logs", [])
|
||||
|
||||
if logs:
|
||||
logger.info(f"Found {len(logs)} events for {contract_name}")
|
||||
|
||||
# Process each log
|
||||
for log in logs:
|
||||
await self._process_contract_event(contract_name, log)
|
||||
|
||||
# Update last processed block
|
||||
self.last_processed_blocks[contract_name] = current_height
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error polling events for {contract_name}: {e}", exc_info=True)
|
||||
|
||||
async def _process_contract_event(self, contract_name: str, log: Dict[str, Any]) -> None:
|
||||
"""Process a contract event."""
|
||||
event_type = log.get("topics", [""])[0] if log.get("topics") else "Unknown"
|
||||
|
||||
logger.info(f"Processing {contract_name} event: {event_type}")
|
||||
|
||||
if self._bridge:
|
||||
# Route event to appropriate handler based on contract type
|
||||
if contract_name == "AgentStaking":
|
||||
await self._handle_staking_event(log)
|
||||
elif contract_name == "PerformanceVerifier":
|
||||
await self._handle_performance_event(log)
|
||||
elif contract_name == "AgentServiceMarketplace":
|
||||
await self._handle_marketplace_event(log)
|
||||
elif contract_name == "BountyIntegration":
|
||||
await self._handle_bounty_event(log)
|
||||
elif contract_name == "CrossChainBridge":
|
||||
await self._handle_bridge_event(log)
|
||||
|
||||
async def _handle_staking_event(self, log: Dict[str, Any]) -> None:
|
||||
"""Handle AgentStaking contract event."""
|
||||
if self._bridge:
|
||||
await self._bridge.handle_staking_event(log)
|
||||
|
||||
async def _handle_performance_event(self, log: Dict[str, Any]) -> None:
|
||||
"""Handle PerformanceVerifier contract event."""
|
||||
if self._bridge:
|
||||
await self._bridge.handle_performance_event(log)
|
||||
|
||||
async def _handle_marketplace_event(self, log: Dict[str, Any]) -> None:
|
||||
"""Handle AgentServiceMarketplace contract event."""
|
||||
if self._bridge:
|
||||
await self._bridge.handle_marketplace_event(log)
|
||||
|
||||
async def _handle_bounty_event(self, log: Dict[str, Any]) -> None:
|
||||
"""Handle BountyIntegration contract event."""
|
||||
if self._bridge:
|
||||
await self._bridge.handle_bounty_event(log)
|
||||
|
||||
async def _handle_bridge_event(self, log: Dict[str, Any]) -> None:
|
||||
"""Handle CrossChainBridge contract event."""
|
||||
if self._bridge:
|
||||
await self._bridge.handle_bridge_event(log)
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stop the contract event subscriber."""
|
||||
self._running = False
|
||||
|
||||
if self._client:
|
||||
await self._client.aclose()
|
||||
self._client = None
|
||||
|
||||
logger.info("Contract event subscriber stopped")
|
||||
@@ -0,0 +1,113 @@
|
||||
"""Transaction event subscriber for gossip broker."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Dict
|
||||
|
||||
from ..config import Settings
|
||||
from ..metrics import event_queue_size, gossip_subscribers_total
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..bridge import BlockchainEventBridge
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TransactionEventSubscriber:
|
||||
"""Subscribes to transaction events from the gossip broker."""
|
||||
|
||||
def __init__(self, settings: Settings) -> None:
|
||||
self.settings = settings
|
||||
self._running = False
|
||||
self._bridge: "BlockchainEventBridge | None" = None
|
||||
self._subscription = None
|
||||
|
||||
def set_bridge(self, bridge: "BlockchainEventBridge") -> None:
|
||||
"""Set the bridge instance for event handling."""
|
||||
self._bridge = bridge
|
||||
|
||||
async def run(self) -> None:
|
||||
"""Run the transaction event subscriber."""
|
||||
if self._running:
|
||||
logger.warning("Transaction event subscriber already running")
|
||||
return
|
||||
|
||||
self._running = True
|
||||
logger.info("Starting transaction event subscriber...")
|
||||
|
||||
# Import gossip broker from blockchain-node
|
||||
try:
|
||||
# Add blockchain-node to path for import
|
||||
import sys
|
||||
from pathlib import Path
|
||||
blockchain_node_src = Path("/opt/aitbc/apps/blockchain-node/src")
|
||||
if str(blockchain_node_src) not in sys.path:
|
||||
sys.path.insert(0, str(blockchain_node_src))
|
||||
|
||||
from aitbc_chain.gossip.broker import create_backend, GossipBroker
|
||||
|
||||
# Create gossip backend
|
||||
backend = create_backend(
|
||||
self.settings.gossip_backend,
|
||||
broadcast_url=self.settings.gossip_broadcast_url
|
||||
)
|
||||
self._broker = GossipBroker(backend)
|
||||
|
||||
# Subscribe to transactions topic (if available)
|
||||
# Note: Currently transactions are embedded in block events
|
||||
# This subscriber will be enhanced when transaction events are published separately
|
||||
try:
|
||||
self._subscription = await self._broker.subscribe("transactions", max_queue_size=100)
|
||||
gossip_subscribers_total.inc()
|
||||
logger.info("Successfully subscribed to transactions topic")
|
||||
except Exception:
|
||||
logger.info("Transactions topic not available - will extract from block events")
|
||||
await self._run_mock()
|
||||
return
|
||||
|
||||
except ImportError as e:
|
||||
logger.error(f"Failed to import gossip broker: {e}")
|
||||
logger.info("Using mock implementation for development")
|
||||
await self._run_mock()
|
||||
return
|
||||
|
||||
# Process transaction events
|
||||
while self._running:
|
||||
try:
|
||||
tx_data = await self._subscription.get()
|
||||
event_queue_size.labels(topic="transactions").set(self._subscription.queue.qsize())
|
||||
|
||||
logger.info(f"Received transaction event: hash={tx_data.get('hash')}")
|
||||
|
||||
if self._bridge:
|
||||
await self._bridge.handle_transaction_event(tx_data)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.info("Transaction event subscriber cancelled")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing transaction event: {e}", exc_info=True)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def _run_mock(self) -> None:
|
||||
"""Run a mock subscriber for development/testing."""
|
||||
logger.warning("Using mock transaction event subscriber - no real events will be processed")
|
||||
await asyncio.sleep(60)
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stop the transaction event subscriber."""
|
||||
if not self._running:
|
||||
return
|
||||
|
||||
logger.info("Stopping transaction event subscriber...")
|
||||
self._running = False
|
||||
|
||||
if self._subscription:
|
||||
self._subscription.close()
|
||||
|
||||
if hasattr(self, '_broker'):
|
||||
await self._broker.shutdown()
|
||||
|
||||
gossip_subscribers_total.dec()
|
||||
logger.info("Transaction event subscriber stopped")
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Main FastAPI application for blockchain event bridge."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from prometheus_client import make_asgi_app
|
||||
|
||||
from .config import settings
|
||||
from .bridge import BlockchainEventBridge
|
||||
from .metrics import (
|
||||
events_received_total,
|
||||
events_processed_total,
|
||||
actions_triggered_total,
|
||||
actions_failed_total,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
bridge_instance: BlockchainEventBridge | None = None
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Lifespan context manager for startup/shutdown."""
|
||||
global bridge_instance
|
||||
|
||||
logger.info(f"Starting {settings.app_name}...")
|
||||
|
||||
# Initialize and start the bridge
|
||||
bridge_instance = BlockchainEventBridge(settings)
|
||||
await bridge_instance.start()
|
||||
|
||||
logger.info(f"{settings.app_name} started successfully")
|
||||
|
||||
yield
|
||||
|
||||
# Shutdown
|
||||
logger.info(f"Shutting down {settings.app_name}...")
|
||||
if bridge_instance:
|
||||
await bridge_instance.stop()
|
||||
logger.info(f"{settings.app_name} shut down successfully")
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title=settings.app_name,
|
||||
description="Bridge between AITBC blockchain events and OpenClaw agent triggers",
|
||||
version="0.1.0",
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
# Add Prometheus metrics endpoint
|
||||
metrics_app = make_asgi_app()
|
||||
app.mount("/metrics", metrics_app)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""Health check endpoint."""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"bridge_running": bridge_instance is not None and bridge_instance.is_running,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""Root endpoint."""
|
||||
return {
|
||||
"service": settings.app_name,
|
||||
"version": "0.1.0",
|
||||
"status": "running",
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Prometheus metrics for blockchain event bridge."""
|
||||
|
||||
from prometheus_client import Counter, Histogram, Gauge
|
||||
|
||||
# Event metrics
|
||||
events_received_total = Counter(
|
||||
"bridge_events_received_total",
|
||||
"Total number of events received from blockchain",
|
||||
["event_type"]
|
||||
)
|
||||
|
||||
events_processed_total = Counter(
|
||||
"bridge_events_processed_total",
|
||||
"Total number of events processed",
|
||||
["event_type", "status"]
|
||||
)
|
||||
|
||||
# Action metrics
|
||||
actions_triggered_total = Counter(
|
||||
"bridge_actions_triggered_total",
|
||||
"Total number of actions triggered",
|
||||
["action_type"]
|
||||
)
|
||||
|
||||
actions_failed_total = Counter(
|
||||
"bridge_actions_failed_total",
|
||||
"Total number of actions that failed",
|
||||
["action_type"]
|
||||
)
|
||||
|
||||
# Performance metrics
|
||||
event_processing_duration_seconds = Histogram(
|
||||
"bridge_event_processing_duration_seconds",
|
||||
"Time spent processing events",
|
||||
["event_type"]
|
||||
)
|
||||
|
||||
action_execution_duration_seconds = Histogram(
|
||||
"bridge_action_execution_duration_seconds",
|
||||
"Time spent executing actions",
|
||||
["action_type"]
|
||||
)
|
||||
|
||||
# Queue metrics
|
||||
event_queue_size = Gauge(
|
||||
"bridge_event_queue_size",
|
||||
"Current size of event queue",
|
||||
["topic"]
|
||||
)
|
||||
|
||||
# Connection metrics
|
||||
gossip_subscribers_total = Gauge(
|
||||
"bridge_gossip_subscribers_total",
|
||||
"Number of active gossip broker subscriptions"
|
||||
)
|
||||
|
||||
coordinator_api_requests_total = Counter(
|
||||
"bridge_coordinator_api_requests_total",
|
||||
"Total number of coordinator API requests",
|
||||
["endpoint", "method", "status"]
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
"""Polling modules for batch operations and condition-based triggers."""
|
||||
|
||||
from .conditions import ConditionPoller
|
||||
from .batch import BatchProcessor
|
||||
|
||||
__all__ = ["ConditionPoller", "BatchProcessor"]
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Batch processing for aggregated operations."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any, Dict, List
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BatchProcessor:
|
||||
"""Processes events in batches for efficiency."""
|
||||
|
||||
def __init__(self, settings: Any) -> None:
|
||||
self.settings = settings
|
||||
self._running = False
|
||||
self._batch_queue: List[Dict[str, Any]] = []
|
||||
self._batch_size = 50
|
||||
|
||||
async def run(self) -> None:
|
||||
"""Run the batch processor."""
|
||||
if not self.settings.enable_polling:
|
||||
logger.info("Batch processing disabled")
|
||||
return
|
||||
|
||||
self._running = True
|
||||
logger.info("Starting batch processor...")
|
||||
|
||||
while self._running:
|
||||
try:
|
||||
await self._process_batch()
|
||||
await asyncio.sleep(self.settings.polling_interval_seconds)
|
||||
except asyncio.CancelledError:
|
||||
logger.info("Batch processor cancelled")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Error in batch processor: {e}", exc_info=True)
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def add_to_batch(self, event: Dict[str, Any]) -> None:
|
||||
"""Add an event to the batch queue."""
|
||||
self._batch_queue.append(event)
|
||||
|
||||
if len(self._batch_queue) >= self._batch_size:
|
||||
await self._process_batch()
|
||||
|
||||
async def _process_batch(self) -> None:
|
||||
"""Process the current batch of events."""
|
||||
if not self._batch_queue:
|
||||
return
|
||||
|
||||
batch = self._batch_queue.copy()
|
||||
self._batch_queue.clear()
|
||||
|
||||
logger.info(f"Processing batch of {len(batch)} events")
|
||||
|
||||
# Placeholder for Phase 3 implementation
|
||||
# Examples:
|
||||
# - Batch agent reputation updates
|
||||
# - Batch marketplace state synchronization
|
||||
# - Batch performance metric aggregation
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stop the batch processor."""
|
||||
self._running = False
|
||||
|
||||
# Process remaining events
|
||||
if self._batch_queue:
|
||||
await self._process_batch()
|
||||
|
||||
logger.info("Batch processor stopped")
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Condition-based polling for batch operations."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConditionPoller:
|
||||
"""Polls for specific conditions that should trigger OpenClaw actions."""
|
||||
|
||||
def __init__(self, settings: Any) -> None:
|
||||
self.settings = settings
|
||||
self._running = False
|
||||
|
||||
async def run(self) -> None:
|
||||
"""Run the condition poller."""
|
||||
if not self.settings.enable_polling:
|
||||
logger.info("Condition polling disabled")
|
||||
return
|
||||
|
||||
self._running = True
|
||||
logger.info("Starting condition poller...")
|
||||
|
||||
while self._running:
|
||||
try:
|
||||
await self._check_conditions()
|
||||
await asyncio.sleep(self.settings.polling_interval_seconds)
|
||||
except asyncio.CancelledError:
|
||||
logger.info("Condition poller cancelled")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Error in condition poller: {e}", exc_info=True)
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def _check_conditions(self) -> None:
|
||||
"""Check for conditions that should trigger actions."""
|
||||
# Placeholder for Phase 3 implementation
|
||||
# Examples:
|
||||
# - Agent performance thresholds (SLA violations)
|
||||
# - Marketplace capacity planning
|
||||
# - Governance proposal voting deadlines
|
||||
# - Cross-chain bridge status
|
||||
pass
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stop the condition poller."""
|
||||
self._running = False
|
||||
logger.info("Condition poller stopped")
|
||||
Reference in New Issue
Block a user