""" Node client for multi-chain operations """ import asyncio import httpx import json import os import logging from typing import Dict, List, Optional, Any from core.config import NodeConfig from models.chain import ChainInfo, ChainType, ChainStatus, ConsensusAlgorithm logger = logging.getLogger(__name__) class NodeClient: """Client for communicating with AITBC nodes""" def __init__(self, node_config: NodeConfig): self.config = node_config self._client: Optional[httpx.AsyncClient] = None self._session_id: Optional[str] = None self._mock_fallback_count = 0 self._dev_mocks_enabled = os.getenv("DEV_MOCKS_ENABLED", "false").lower() == "true" async def __aenter__(self): """Async context manager entry""" self._client = httpx.AsyncClient( timeout=httpx.Timeout(self.config.timeout), limits=httpx.Limits(max_connections=self.config.max_connections) ) await self._authenticate() return self async def __aexit__(self, exc_type, exc_val, exc_tb): """Async context manager exit""" if self._client: await self._client.aclose() async def _authenticate(self): """Authenticate with the node""" try: # For now, we'll use a simple authentication # In production, this would use proper authentication response = await self._client.post( f"{self.config.endpoint}/api/auth", json={"action": "authenticate"} ) if response.status_code == 200: data = response.json() self._session_id = data.get("session_id") except Exception as e: # For development, we'll continue without authentication if self._dev_mocks_enabled: logger.warning(f"[DEV_MODE] Authentication failed for node {self.config.id}: {e}") else: logger.error(f"Authentication failed for node {self.config.id}: {e}") raise async def get_node_info(self) -> Dict[str, Any]: """Get node information""" try: response = await self._client.get(f"{self.config.endpoint}/api/node/info") if response.status_code == 200: return response.json() else: raise Exception(f"Node info request failed: {response.status_code}") except Exception as e: # Return mock data for development if self._dev_mocks_enabled: self._mock_fallback_count += 1 logger.warning(f"[DEV_MODE] Using mock node info for {self.config.id} (fallback #{self._mock_fallback_count})") return self._get_mock_node_info() else: logger.error(f"Failed to get node info for {self.config.id}: {e}") raise async def get_hosted_chains(self) -> List[ChainInfo]: """Get all chains hosted by this node""" try: health_url = f"{self.config.endpoint}/health" if "/rpc" in self.config.endpoint: health_url = self.config.endpoint.replace("/rpc", "/health") response = await self._client.get(health_url) if response.status_code == 200: health_data = response.json() chains = health_data.get("supported_chains", ["ait-devnet"]) result = [] for cid in chains: # Try to fetch real block height block_height = 0 try: head_url = f"{self.config.endpoint}/rpc/head?chain_id={cid}" if "/rpc" in self.config.endpoint: head_url = f"{self.config.endpoint}/head?chain_id={cid}" head_resp = await self._client.get(head_url, timeout=2.0) if head_resp.status_code == 200: head_data = head_resp.json() block_height = head_data.get("height", 0) except Exception: pass result.append(self._parse_chain_info({ "id": cid, "name": f"AITBC {cid.split('-')[-1].capitalize()} Chain", "type": "topic" if "health" in cid else "main", "purpose": "specialized" if "health" in cid else "general", "status": "active", "size_mb": 50.5, "nodes": 3, "smart_contracts": 5, "active_clients": 25, "active_miners": 8, "block_height": block_height, "privacy": {"visibility": "public"} })) return result else: return self._get_mock_chains() except Exception as e: return self._get_mock_chains() async def get_chain_info(self, chain_id: str) -> Optional[ChainInfo]: """Get specific chain information""" try: # Re-use the health endpoint logic health_url = f"{self.config.endpoint}/health" if "/rpc" in self.config.endpoint: health_url = self.config.endpoint.replace("/rpc", "/health") response = await self._client.get(health_url) if response.status_code == 200: health_data = response.json() chains = health_data.get("supported_chains", ["ait-devnet"]) if chain_id in chains: block_height = 0 try: head_url = f"{self.config.endpoint}/rpc/head?chain_id={chain_id}" if "/rpc" in self.config.endpoint: head_url = f"{self.config.endpoint}/head?chain_id={chain_id}" head_resp = await self._client.get(head_url, timeout=2.0) if head_resp.status_code == 200: head_data = head_resp.json() block_height = head_data.get("height", 0) except Exception: pass return self._parse_chain_info({ "id": chain_id, "name": f"AITBC {chain_id.split('-')[-1].capitalize()} Chain", "type": "topic" if "health" in chain_id else "main", "purpose": "specialized" if "health" in chain_id else "general", "status": "active", "size_mb": 50.5, "nodes": 3, "smart_contracts": 5, "active_clients": 25, "active_miners": 8, "block_height": block_height, "privacy": {"visibility": "public"} }) return None except Exception as e: # Fallback to pure mock chains = self._get_mock_chains() for chain in chains: if chain.id == chain_id: return chain return None async def create_chain(self, genesis_block: Dict[str, Any]) -> str: """Create a new chain on this node""" try: response = await self._client.post( f"{self.config.endpoint}/api/chains", json=genesis_block ) if response.status_code == 201: data = response.json() return data["chain_id"] else: raise Exception(f"Chain creation failed: {response.status_code}") except Exception as e: # Mock chain creation for development chain_id = genesis_block.get("chain_id", f"MOCK-CHAIN-{hash(str(genesis_block)) % 10000}") logger.info(f"Mock created chain {chain_id} on node {self.config.id}") return chain_id async def delete_chain(self, chain_id: str) -> bool: """Delete a chain from this node""" try: response = await self._client.delete(f"{self.config.endpoint}/api/chains/{chain_id}") if response.status_code == 200: return True else: raise Exception(f"Chain deletion failed: {response.status_code}") except Exception as e: # Mock chain deletion for development logger.info(f"Mock deleted chain {chain_id} from node {self.config.id}") return True async def get_chain_stats(self, chain_id: str) -> Dict[str, Any]: """Get chain statistics""" try: response = await self._client.get(f"{self.config.endpoint}/api/chains/{chain_id}/stats") if response.status_code == 200: return response.json() else: raise Exception(f"Chain stats request failed: {response.status_code}") except Exception as e: # Return mock stats for development return self._get_mock_chain_stats(chain_id) async def backup_chain(self, chain_id: str, backup_path: str) -> Dict[str, Any]: """Backup a chain""" try: response = await self._client.post( f"{self.config.endpoint}/api/chains/{chain_id}/backup", json={"backup_path": backup_path} ) if response.status_code == 200: return response.json() else: raise Exception(f"Chain backup failed: {response.status_code}") except Exception as e: # Mock backup for development backup_info = { "chain_id": chain_id, "backup_file": f"{backup_path}/{chain_id}_backup.tar.gz", "original_size_mb": 100.0, "backup_size_mb": 50.0, "checksum": "mock_checksum_12345" } logger.info(f"Mock backed up chain {chain_id} to {backup_info['backup_file']}") return backup_info async def restore_chain(self, backup_file: str, chain_id: Optional[str] = None) -> Dict[str, Any]: """Restore a chain from backup""" try: response = await self._client.post( f"{self.config.endpoint}/api/chains/restore", json={"backup_file": backup_file, "chain_id": chain_id} ) if response.status_code == 200: return response.json() else: raise Exception(f"Chain restore failed: {response.status_code}") except Exception as e: # Mock restore for development restore_info = { "chain_id": chain_id or "RESTORED-MOCK-CHAIN", "blocks_restored": 1000, "verification_passed": True } logger.info(f"Mock restored chain from {backup_file}") return restore_info def _parse_chain_info(self, chain_data: Dict[str, Any]) -> ChainInfo: """Parse chain data from node response""" from datetime import datetime from models.chain import PrivacyConfig return ChainInfo( id=chain_data.get("chain_id", chain_data.get("id", "unknown")), type=ChainType(chain_data.get("chain_type", "topic")), purpose=chain_data.get("purpose", "unknown"), name=chain_data.get("name", "Unnamed Chain"), description=chain_data.get("description"), status=ChainStatus(chain_data.get("status", "active")), created_at=datetime.fromisoformat(chain_data.get("created_at", "2024-01-01T00:00:00")), block_height=chain_data.get("block_height", 0), size_mb=chain_data.get("size_mb", 0.0), node_count=chain_data.get("node_count", 1), active_nodes=chain_data.get("active_nodes", 1), contract_count=chain_data.get("contract_count", 0), client_count=chain_data.get("client_count", 0), miner_count=chain_data.get("miner_count", 0), agent_count=chain_data.get("agent_count", 0), consensus_algorithm=ConsensusAlgorithm(chain_data.get("consensus_algorithm", "pos")), block_time=chain_data.get("block_time", 5), tps=chain_data.get("tps", 0.0), avg_block_time=chain_data.get("avg_block_time", 5.0), avg_gas_used=chain_data.get("avg_gas_used", 0), growth_rate_mb_per_day=chain_data.get("growth_rate_mb_per_day", 0.0), gas_price=chain_data.get("gas_price", 20000000000), memory_usage_mb=chain_data.get("memory_usage_mb", 0.0), disk_usage_mb=chain_data.get("disk_usage_mb", 0.0), privacy=PrivacyConfig( visibility=chain_data.get("privacy", {}).get("visibility", "public"), access_control=chain_data.get("privacy", {}).get("access_control", "open") ) ) def _get_mock_node_info(self) -> Dict[str, Any]: """Get mock node information for development""" return { "node_id": self.config.id, "type": "full", "status": "active", "version": "1.0.0", "uptime_days": 30, "uptime_hours": 720, "hosted_chains": {}, "cpu_usage": 25.5, "memory_usage_mb": 1024.0, "disk_usage_mb": 10240.0, "network_in_mb": 10.5, "network_out_mb": 8.2 } def _get_mock_chains(self) -> List[ChainInfo]: """Get mock chains for development""" from datetime import datetime from models.chain import PrivacyConfig return [ ChainInfo( id="AITBC-TOPIC-HEALTHCARE-001", type=ChainType.TOPIC, purpose="healthcare", name="Healthcare AI Chain", description="A specialized chain for healthcare AI applications", status=ChainStatus.ACTIVE, created_at=datetime.now(), block_height=1000, size_mb=50.5, node_count=3, active_nodes=3, contract_count=5, client_count=25, miner_count=8, agent_count=12, consensus_algorithm=ConsensusAlgorithm.POS, block_time=3, tps=15.5, avg_block_time=3.2, avg_gas_used=5000000, growth_rate_mb_per_day=2.1, gas_price=20000000000, memory_usage_mb=256.0, disk_usage_mb=512.0, privacy=PrivacyConfig(visibility="public", access_control="open") ), ChainInfo( id="AITBC-PRIVATE-COLLAB-001", type=ChainType.PRIVATE, purpose="collaboration", name="Private Research Chain", description="A private chain for trusted agent collaboration", status=ChainStatus.ACTIVE, created_at=datetime.now(), block_height=500, size_mb=25.2, node_count=2, active_nodes=2, contract_count=3, client_count=8, miner_count=4, agent_count=6, consensus_algorithm=ConsensusAlgorithm.POA, block_time=5, tps=8.0, avg_block_time=5.1, avg_gas_used=3000000, growth_rate_mb_per_day=1.0, gas_price=15000000000, memory_usage_mb=128.0, disk_usage_mb=256.0, privacy=PrivacyConfig(visibility="private", access_control="invite_only") ) ] def _get_mock_chain_stats(self, chain_id: str) -> Dict[str, Any]: """Get mock chain statistics for development""" return { "chain_id": chain_id, "block_height": 1000, "tps": 15.5, "avg_block_time": 3.2, "gas_price": 20000000000, "memory_usage_mb": 256.0, "disk_usage_mb": 512.0, "active_nodes": 3, "client_count": 25, "miner_count": 8, "agent_count": 12, "last_block_time": "2024-03-02T10:00:00Z" }