diff --git a/.gitea/workflows/package-tests.yml b/.gitea/workflows/package-tests.yml index eeeb849b..8600db6f 100644 --- a/.gitea/workflows/package-tests.yml +++ b/.gitea/workflows/package-tests.yml @@ -6,6 +6,7 @@ on: paths: - 'packages/**' - 'pyproject.toml' + - 'poetry.lock' - '.gitea/workflows/package-tests.yml' pull_request: branches: [main, develop] diff --git a/.gitea/workflows/python-tests.yml b/.gitea/workflows/python-tests.yml index 16ae314f..0b9a1beb 100644 --- a/.gitea/workflows/python-tests.yml +++ b/.gitea/workflows/python-tests.yml @@ -8,7 +8,7 @@ on: - 'packages/py/**' - 'tests/**' - 'pyproject.toml' - - 'requirements.txt' + - 'poetry.lock' - '.gitea/workflows/python-tests.yml' pull_request: branches: [main, develop] @@ -57,11 +57,6 @@ jobs: --extra-packages "pytest pytest-cov pytest-mock pytest-timeout pytest-asyncio locust pydantic-settings fastapi uvicorn aiohttp>=3.12.14 sqlmodel>=0.0.38 PyJWT" echo "āœ… Python environment ready" - - name: Check requirements.txt sync - run: | - cd "${{ env.WORKSPACE }}/repo" - venv/bin/python scripts/ci/check-requirements-sync.py - - name: Run linting run: | cd "${{ env.WORKSPACE }}/repo" diff --git a/aitbc/dependency_scanner.py b/aitbc/dependency_scanner.py index a647b95a..670ef623 100644 --- a/aitbc/dependency_scanner.py +++ b/aitbc/dependency_scanner.py @@ -39,9 +39,9 @@ class DependencyScanner: Initialize dependency scanner Args: - requirements_file: Path to requirements.txt or pyproject.toml + requirements_file: Path to pyproject.toml (Poetry source of truth) """ - self.requirements_file = requirements_file or Path("requirements.txt") + self.requirements_file = requirements_file or Path("pyproject.toml") self._vulnerabilities: List[VulnerabilityReport] = [] def scan_with_pip_audit(self) -> List[VulnerabilityReport]: diff --git a/apps/marketplace/scripts/deploy_edge_node.py b/apps/marketplace/scripts/deploy_edge_node.py index 612dda81..89778d3a 100755 --- a/apps/marketplace/scripts/deploy_edge_node.py +++ b/apps/marketplace/scripts/deploy_edge_node.py @@ -9,6 +9,7 @@ import subprocess import sys import os import json +import click from datetime import datetime def load_config(config_file): @@ -18,24 +19,24 @@ def load_config(config_file): def deploy_redis_cache(config): """Deploy Redis cache layer""" - print(f"šŸ”§ Deploying Redis cache for {config['edge_node_config']['node_id']}") + click.echo(f"šŸ”§ Deploying Redis cache for {config['edge_node_config']['node_id']}") # Check if Redis is running try: result = subprocess.run(['redis-cli', 'ping'], capture_output=True, text=True) if result.stdout.strip() == 'PONG': - print("āœ… Redis is already running") + click.echo("āœ… Redis is already running") else: - print("āš ļø Redis not responding, attempting to start...") + click.echo("āš ļø Redis not responding, attempting to start...") # Start Redis if not running subprocess.run(['sudo', 'systemctl', 'start', 'redis-server'], check=True) - print("āœ… Redis started") + click.echo("āœ… Redis started") except FileNotFoundError: - print("āŒ Redis not installed, installing...") + click.echo("āŒ Redis not installed, installing...") subprocess.run(['sudo', 'apt-get', 'update'], check=True) subprocess.run(['sudo', 'apt-get', 'install', '-y', 'redis-server'], check=True) subprocess.run(['sudo', 'systemctl', 'start', 'redis-server'], check=True) - print("āœ… Redis installed and started") + click.echo("āœ… Redis installed and started") # Configure Redis redis_config = config['edge_node_config']['caching'] @@ -51,11 +52,11 @@ def deploy_redis_cache(config): try: subprocess.run(['redis-cli', *cmd.split()], check=True, capture_output=True) except subprocess.CalledProcessError: - print(f"āš ļø Could not set Redis config: {cmd}") + click.echo(f"āš ļø Could not set Redis config: {cmd}") def deploy_monitoring(config): """Deploy monitoring agent""" - print(f"šŸ“Š Deploying monitoring for {config['edge_node_config']['node_id']}") + click.echo(f"šŸ“Š Deploying monitoring for {config['edge_node_config']['node_id']}") monitoring_config = config['edge_node_config']['monitoring'] @@ -115,11 +116,11 @@ WantedBy=multi-user.target subprocess.run(['sudo', 'systemctl', 'enable', f'aitbc-edge-monitoring-{config["edge_node_config"]["node_id"]}.service'], check=True) subprocess.run(['sudo', 'systemctl', 'start', f'aitbc-edge-monitoring-{config["edge_node_config"]["node_id"]}.service'], check=True) - print("āœ… Monitoring agent deployed") + click.echo("āœ… Monitoring agent deployed") def optimize_network(config): """Apply network optimizations""" - print(f"🌐 Optimizing network for {config['edge_node_config']['node_id']}") + click.echo(f"🌐 Optimizing network for {config['edge_node_config']['node_id']}") network_config = config['edge_node_config']['network'] @@ -136,13 +137,13 @@ def optimize_network(config): for param, value in tcp_params.items(): try: subprocess.run(['sudo', 'sysctl', '-w', f'{param}={value}'], check=True, capture_output=True) - print(f"āœ… Set {param}={value}") + click.echo(f"āœ… Set {param}={value}") except subprocess.CalledProcessError: - print(f"āš ļø Could not set {param}") + click.echo(f"āš ļø Could not set {param}") def deploy_edge_services(config): """Deploy edge node services""" - print(f"šŸš€ Deploying edge services for {config['edge_node_config']['node_id']}") + click.echo(f"šŸš€ Deploying edge services for {config['edge_node_config']['node_id']}") # Create edge service configuration edge_service_config = { @@ -157,11 +158,11 @@ def deploy_edge_services(config): with open(f'/tmp/aitbc-edge-{config["edge_node_config"]["node_id"]}-config.json', 'w') as f: json.dump(edge_service_config, f, indent=2) - print(f"āœ… Edge services configuration saved") + click.echo(f"āœ… Edge services configuration saved") def validate_deployment(config): """Validate edge node deployment""" - print(f"āœ… Validating deployment for {config['edge_node_config']['node_id']}") + click.echo(f"āœ… Validating deployment for {config['edge_node_config']['node_id']}") validation_results = {} @@ -194,29 +195,29 @@ def validate_deployment(config): except Exception as e: validation_results['monitoring'] = f'error: {str(e)}' - print(f"šŸ“Š Validation Results:") + click.echo(f"šŸ“Š Validation Results:") for service, status in validation_results.items(): - print(f" {service}: {status}") + click.echo(f" {service}: {status}") return validation_results def main(): if len(sys.argv) != 2: - print("Usage: python deploy_edge_node.py ") + click.echo("Usage: python deploy_edge_node.py ") sys.exit(1) config_file = sys.argv[1] if not os.path.exists(config_file): - print(f"āŒ Configuration file {config_file} not found") + click.echo(f"āŒ Configuration file {config_file} not found") sys.exit(1) try: config = load_config(config_file) - print(f"šŸš€ Deploying edge node: {config['edge_node_config']['node_id']}") - print(f"šŸ“ Region: {config['edge_node_config']['region']}") - print(f"šŸŒ Location: {config['edge_node_config']['location']}") + click.echo(f"šŸš€ Deploying edge node: {config['edge_node_config']['node_id']}") + click.echo(f"šŸ“ Region: {config['edge_node_config']['region']}") + click.echo(f"šŸŒ Location: {config['edge_node_config']['location']}") # Deploy components deploy_redis_cache(config) @@ -238,10 +239,10 @@ def main(): with open(f'/tmp/aitbc-edge-{config["edge_node_config"]["node_id"]}-deployment.json', 'w') as f: json.dump(deployment_status, f, indent=2) - print(f"āœ… Edge node deployment completed for {config['edge_node_config']['node_id']}") + click.echo(f"āœ… Edge node deployment completed for {config['edge_node_config']['node_id']}") except Exception as e: - print(f"āŒ Deployment failed: {str(e)}") + click.echo(f"āŒ Deployment failed: {str(e)}") sys.exit(1) if __name__ == "__main__": diff --git a/apps/zk-circuits/zk_cache.py b/apps/zk-circuits/zk_cache.py index cdfff015..69496fad 100755 --- a/apps/zk-circuits/zk_cache.py +++ b/apps/zk-circuits/zk_cache.py @@ -9,10 +9,14 @@ Tracks file dependencies and invalidates cache when source files change. import hashlib import json import os +import logging +import click from pathlib import Path from typing import Dict, List, Optional import time +logger = logging.getLogger(__name__) + class ZKCircuitCache: """Cache system for ZK circuit compilation artifacts""" @@ -123,7 +127,7 @@ class ZKCircuitCache: json.dump(manifest, f, indent=2) except Exception as e: - print(f"Warning: Failed to save cache entry: {e}") + logger.warning(f"Failed to save cache entry: {e}") def get_cached_artifacts(self, circuit_file: Path, output_dir: Path) -> Optional[Dict]: """Retrieve cached artifacts if valid""" @@ -206,14 +210,14 @@ def main(): if args.action == 'stats': stats = cache.get_cache_stats() - print(f"Cache Statistics:") - print(f" Entries: {stats['entries']}") - print(f" Total Size: {stats['total_size_mb']:.2f} MB") - print(f" Cache Directory: {stats['cache_dir']}") + click.echo(f"Cache Statistics:") + click.echo(f" Entries: {stats['entries']}") + click.echo(f" Total Size: {stats['total_size_mb']:.2f} MB") + click.echo(f" Cache Directory: {stats['cache_dir']}") elif args.action == 'clear': cache.clear_cache() - print("Cache cleared successfully") + click.echo("Cache cleared successfully") if __name__ == "__main__": main() diff --git a/cli/pyproject.toml b/cli/pyproject.toml deleted file mode 100644 index 0134a756..00000000 --- a/cli/pyproject.toml +++ /dev/null @@ -1,37 +0,0 @@ -[build-system] -requires = ["setuptools>=61.0", "wheel"] -build-backend = "setuptools.build_meta" - -[project] -name = "aitbc-cli" -version = "0.1.0" -description = "AITBC Command Line Interface" -readme = "README.md" -requires-python = ">=3.11" -dependencies = [ - "click>=8.0", - "rich>=13.0", - "PyYAML", - "requests", - "cryptography", -] - -[project.optional-dependencies] -dev = [ - "pytest>=7.0.0", - "pytest-asyncio>=0.21.0", - "pytest-cov>=4.0.0", - "pytest-mock>=3.10.0", - "black>=22.0.0", - "isort>=5.10.0", - "flake8>=5.0.0", -] - -[project.scripts] -aitbc-cli = "aitbc_cli.main:cli" - -[tool.setuptools] -package-dir = {"" = "src"} - -[tool.setuptools.packages.find] -where = ["src"] diff --git a/cli/setup.py b/cli/setup.py index e1d60ab4..054c7f73 100755 --- a/cli/setup.py +++ b/cli/setup.py @@ -11,10 +11,23 @@ def read_readme(): with open("docs/README.md", "r", encoding="utf-8") as fh: return fh.read() -# Read requirements +# Read requirements from pyproject.toml def read_requirements(): - with open("requirements.txt", "r", encoding="utf-8") as fh: - return [line.strip() for line in fh if line.strip() and not line.startswith("#")] + import tomli + try: + with open("pyproject.toml", "rb") as f: + data = tomli.load(f) + return data.get("project", {}).get("dependencies", []) + except ImportError: + # Fallback to hardcoded list if tomli not available + return [ + "click>=8.0", + "rich>=13.0", + "PyYAML", + "requests", + "cryptography", + "aitbc>=0.6.0", + ] setup( name="aitbc-cli", diff --git a/cli/setup/setup.py b/cli/setup/setup.py index 16cd7a31..6645a554 100755 --- a/cli/setup/setup.py +++ b/cli/setup/setup.py @@ -11,10 +11,23 @@ def read_readme(): with open("README.md", "r", encoding="utf-8") as fh: return fh.read() -# Read requirements +# Read requirements from pyproject.toml def read_requirements(): - with open("requirements.txt", "r", encoding="utf-8") as fh: - return [line.strip() for line in fh if line.strip() and not line.startswith("#")] + import tomli + try: + with open("pyproject.toml", "rb") as f: + data = tomli.load(f) + return data.get("project", {}).get("dependencies", []) + except ImportError: + # Fallback to hardcoded list if tomli not available + return [ + "click>=8.0", + "rich>=13.0", + "PyYAML", + "requests", + "cryptography", + "aitbc>=0.6.0", + ] setup( name="aitbc-cli", diff --git a/cli/src/aitbc_cli/__init__.py b/cli/src/aitbc_cli/__init__.py deleted file mode 100644 index 4b4e8d7a..00000000 --- a/cli/src/aitbc_cli/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""AITBC Command Line Interface.""" - -__version__ = "0.1.0" diff --git a/cli/src/aitbc_cli/commands/__init__.py b/cli/src/aitbc_cli/commands/__init__.py deleted file mode 100644 index 37c0ff70..00000000 --- a/cli/src/aitbc_cli/commands/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Compatibility package for AITBC CLI command modules.""" - -from __future__ import annotations - -from pathlib import Path - -_PACKAGE_DIR = Path(__file__).resolve().parent -_BUILD_COMMANDS_DIR = _PACKAGE_DIR.parents[1] / "build" / "lib" / "aitbc_cli" / "commands" - -if _BUILD_COMMANDS_DIR.exists(): - __path__.append(str(_BUILD_COMMANDS_DIR)) diff --git a/cli/src/aitbc_cli/commands/agent_comm.py b/cli/src/aitbc_cli/commands/agent_comm.py deleted file mode 100755 index e0edd002..00000000 --- a/cli/src/aitbc_cli/commands/agent_comm.py +++ /dev/null @@ -1,496 +0,0 @@ -"""Cross-chain agent communication commands for AITBC CLI""" - -import click -import asyncio -import json -from datetime import datetime, timedelta -from typing import Optional -from ..core.config import load_multichain_config -from ..core.agent_communication import ( - CrossChainAgentCommunication, AgentInfo, AgentMessage, - MessageType, AgentStatus -) -from ..utils import output, error, success - -@click.group() -def agent_comm(): - """Cross-chain agent communication commands""" - pass - -@agent_comm.command() -@click.argument('agent_id') -@click.argument('name') -@click.argument('chain_id') -@click.argument('endpoint') -@click.option('--capabilities', help='Comma-separated list of capabilities') -@click.option('--reputation', default=0.5, help='Initial reputation score') -@click.option('--version', default='1.0.0', help='Agent version') -@click.pass_context -def register(ctx, agent_id, name, chain_id, endpoint, capabilities, reputation, version): - """Register an agent in the cross-chain network""" - try: - config = load_multichain_config() - comm = CrossChainAgentCommunication(config) - - # Parse capabilities - cap_list = capabilities.split(',') if capabilities else [] - - # Create agent info - agent_info = AgentInfo( - agent_id=agent_id, - name=name, - chain_id=chain_id, - node_id="default-node", # Would be determined dynamically - status=AgentStatus.ACTIVE, - capabilities=cap_list, - reputation_score=reputation, - last_seen=datetime.now(), - endpoint=endpoint, - version=version - ) - - # Register agent - success = asyncio.run(comm.register_agent(agent_info)) - - if success: - success(f"Agent {agent_id} registered successfully!") - - agent_data = { - "Agent ID": agent_id, - "Name": name, - "Chain ID": chain_id, - "Status": "active", - "Capabilities": ", ".join(cap_list), - "Reputation": f"{reputation:.2f}", - "Endpoint": endpoint, - "Version": version - } - - output(agent_data, ctx.obj.get('output_format', 'table')) - else: - error(f"Failed to register agent {agent_id}") - raise click.Abort() - - except Exception as e: - error(f"Error registering agent: {str(e)}") - raise click.Abort() - -@agent_comm.command() -@click.option('--chain-id', help='Filter by chain ID') -@click.option('--status', type=click.Choice(['active', 'inactive', 'busy', 'offline']), help='Filter by status') -@click.option('--capabilities', help='Filter by capabilities (comma-separated)') -@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format') -@click.pass_context -def list(ctx, chain_id, status, capabilities, format): - """List registered agents""" - try: - config = load_multichain_config() - comm = CrossChainAgentCommunication(config) - - # Get all agents - agents = list(comm.agents.values()) - - # Apply filters - if chain_id: - agents = [a for a in agents if a.chain_id == chain_id] - - if status: - agents = [a for a in agents if a.status.value == status] - - if capabilities: - required_caps = [cap.strip() for cap in capabilities.split(',')] - agents = [a for a in agents if any(cap in a.capabilities for cap in required_caps)] - - if not agents: - output("No agents found", ctx.obj.get('output_format', 'table')) - return - - # Format output - agent_data = [ - { - "Agent ID": agent.agent_id, - "Name": agent.name, - "Chain ID": agent.chain_id, - "Status": agent.status.value, - "Reputation": f"{agent.reputation_score:.2f}", - "Capabilities": ", ".join(agent.capabilities[:3]), # Show first 3 - "Last Seen": agent.last_seen.strftime("%Y-%m-%d %H:%M:%S") - } - for agent in agents - ] - - output(agent_data, ctx.obj.get('output_format', format), title="Registered Agents") - - except Exception as e: - error(f"Error listing agents: {str(e)}") - raise click.Abort() - -@agent_comm.command() -@click.argument('chain_id') -@click.option('--capabilities', help='Required capabilities (comma-separated)') -@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format') -@click.pass_context -def discover(ctx, chain_id, capabilities, format): - """Discover agents on a specific chain""" - try: - config = load_multichain_config() - comm = CrossChainAgentCommunication(config) - - # Parse capabilities - cap_list = capabilities.split(',') if capabilities else None - - # Discover agents - agents = asyncio.run(comm.discover_agents(chain_id, cap_list)) - - if not agents: - output(f"No agents found on chain {chain_id}", ctx.obj.get('output_format', 'table')) - return - - # Format output - agent_data = [ - { - "Agent ID": agent.agent_id, - "Name": agent.name, - "Status": agent.status.value, - "Reputation": f"{agent.reputation_score:.2f}", - "Capabilities": ", ".join(agent.capabilities), - "Endpoint": agent.endpoint, - "Version": agent.version - } - for agent in agents - ] - - output(agent_data, ctx.obj.get('output_format', format), title=f"Agents on Chain {chain_id}") - - except Exception as e: - error(f"Error discovering agents: {str(e)}") - raise click.Abort() - -@agent_comm.command() -@click.argument('sender_id') -@click.argument('receiver_id') -@click.argument('message_type') -@click.argument('chain_id') -@click.option('--payload', help='Message payload (JSON string)') -@click.option('--target-chain', help='Target chain for cross-chain messages') -@click.option('--priority', default=5, help='Message priority (1-10)') -@click.option('--ttl', default=3600, help='Time to live in seconds') -@click.pass_context -def send(ctx, sender_id, receiver_id, message_type, chain_id, payload, target_chain, priority, ttl): - """Send a message to an agent""" - try: - config = load_multichain_config() - comm = CrossChainAgentCommunication(config) - - # Parse message type - try: - msg_type = MessageType(message_type) - except ValueError: - error(f"Invalid message type: {message_type}") - error(f"Valid types: {[t.value for t in MessageType]}") - raise click.Abort() - - # Parse payload - payload_dict = {} - if payload: - try: - payload_dict = json.loads(payload) - except json.JSONDecodeError: - error("Invalid JSON payload") - raise click.Abort() - - # Create message - message = AgentMessage( - message_id=f"msg_{datetime.now().strftime('%Y%m%d%H%M%S')}_{sender_id}", - sender_id=sender_id, - receiver_id=receiver_id, - message_type=msg_type, - chain_id=chain_id, - target_chain_id=target_chain, - payload=payload_dict, - timestamp=datetime.now(), - signature="auto_generated", # Would be cryptographically signed - priority=priority, - ttl_seconds=ttl - ) - - # Send message - success = asyncio.run(comm.send_message(message)) - - if success: - success(f"Message sent successfully to {receiver_id}") - - message_data = { - "Message ID": message.message_id, - "Sender": sender_id, - "Receiver": receiver_id, - "Type": message_type, - "Chain": chain_id, - "Target Chain": target_chain or "Same", - "Priority": priority, - "TTL": f"{ttl}s", - "Sent": message.timestamp.strftime("%Y-%m-%d %H:%M:%S") - } - - output(message_data, ctx.obj.get('output_format', 'table')) - else: - error(f"Failed to send message to {receiver_id}") - raise click.Abort() - - except Exception as e: - error(f"Error sending message: {str(e)}") - raise click.Abort() - -@agent_comm.command() -@click.argument('agent_ids', nargs=-1, required=True) -@click.argument('collaboration_type') -@click.option('--governance', help='Governance rules (JSON string)') -@click.pass_context -def collaborate(ctx, agent_ids, collaboration_type, governance): - """Create a multi-agent collaboration""" - try: - config = load_multichain_config() - comm = CrossChainAgentCommunication(config) - - # Parse governance rules - governance_dict = {} - if governance: - try: - governance_dict = json.loads(governance) - except json.JSONDecodeError: - error("Invalid JSON governance rules") - raise click.Abort() - - # Create collaboration - collaboration_id = asyncio.run(comm.create_collaboration( - list(agent_ids), collaboration_type, governance_dict - )) - - if collaboration_id: - success(f"Collaboration created: {collaboration_id}") - - collab_data = { - "Collaboration ID": collaboration_id, - "Type": collaboration_type, - "Participants": ", ".join(agent_ids), - "Status": "active", - "Created": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - - output(collab_data, ctx.obj.get('output_format', 'table')) - else: - error("Failed to create collaboration") - raise click.Abort() - - except Exception as e: - error(f"Error creating collaboration: {str(e)}") - raise click.Abort() - -@agent_comm.command() -@click.argument('agent_id') -@click.argument('interaction_result', type=click.Choice(['success', 'failure'])) -@click.option('--feedback', type=float, help='Feedback score (0.0-1.0)') -@click.pass_context -def reputation(ctx, agent_id, interaction_result, feedback): - """Update agent reputation""" - try: - config = load_multichain_config() - comm = CrossChainAgentCommunication(config) - - # Update reputation - success = asyncio.run(comm.update_reputation( - agent_id, interaction_result == 'success', feedback - )) - - if success: - # Get updated reputation - agent_status = asyncio.run(comm.get_agent_status(agent_id)) - - if agent_status and agent_status.get('reputation'): - rep = agent_status['reputation'] - success(f"Reputation updated for {agent_id}") - - rep_data = { - "Agent ID": agent_id, - "Reputation Score": f"{rep['reputation_score']:.3f}", - "Total Interactions": rep['total_interactions'], - "Successful": rep['successful_interactions'], - "Failed": rep['failed_interactions'], - "Success Rate": f"{(rep['successful_interactions'] / rep['total_interactions'] * 100):.1f}%" if rep['total_interactions'] > 0 else "N/A", - "Last Updated": rep['last_updated'] - } - - output(rep_data, ctx.obj.get('output_format', 'table')) - else: - success(f"Reputation updated for {agent_id}") - else: - error(f"Failed to update reputation for {agent_id}") - raise click.Abort() - - except Exception as e: - error(f"Error updating reputation: {str(e)}") - raise click.Abort() - -@agent_comm.command() -@click.argument('agent_id') -@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format') -@click.pass_context -def status(ctx, agent_id, format): - """Get detailed agent status""" - try: - config = load_multichain_config() - comm = CrossChainAgentCommunication(config) - - # Get agent status - agent_status = asyncio.run(comm.get_agent_status(agent_id)) - - if not agent_status: - error(f"Agent {agent_id} not found") - raise click.Abort() - - # Format output - status_data = [ - {"Metric": "Agent ID", "Value": agent_status["agent_info"]["agent_id"]}, - {"Metric": "Name", "Value": agent_status["agent_info"]["name"]}, - {"Metric": "Chain ID", "Value": agent_status["agent_info"]["chain_id"]}, - {"Metric": "Status", "Value": agent_status["status"]}, - {"Metric": "Reputation", "Value": f"{agent_status['agent_info']['reputation_score']:.3f}" if agent_status.get('reputation') else "N/A"}, - {"Metric": "Capabilities", "Value": ", ".join(agent_status["agent_info"]["capabilities"])}, - {"Metric": "Message Queue Size", "Value": agent_status["message_queue_size"]}, - {"Metric": "Active Collaborations", "Value": agent_status["active_collaborations"]}, - {"Metric": "Last Seen", "Value": agent_status["last_seen"]}, - {"Metric": "Endpoint", "Value": agent_status["agent_info"]["endpoint"]}, - {"Metric": "Version", "Value": agent_status["agent_info"]["version"]} - ] - - output(status_data, ctx.obj.get('output_format', format), title=f"Agent Status: {agent_id}") - - except Exception as e: - error(f"Error getting agent status: {str(e)}") - raise click.Abort() - -@agent_comm.command() -@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format') -@click.pass_context -def network(ctx, format): - """Get cross-chain network overview""" - try: - config = load_multichain_config() - comm = CrossChainAgentCommunication(config) - - # Get network overview - overview = asyncio.run(comm.get_network_overview()) - - if not overview: - error("No network data available") - raise click.Abort() - - # Overview data - overview_data = [ - {"Metric": "Total Agents", "Value": overview["total_agents"]}, - {"Metric": "Active Agents", "Value": overview["active_agents"]}, - {"Metric": "Total Collaborations", "Value": overview["total_collaborations"]}, - {"Metric": "Active Collaborations", "Value": overview["active_collaborations"]}, - {"Metric": "Total Messages", "Value": overview["total_messages"]}, - {"Metric": "Queued Messages", "Value": overview["queued_messages"]}, - {"Metric": "Average Reputation", "Value": f"{overview['average_reputation']:.3f}"}, - {"Metric": "Routing Table Size", "Value": overview["routing_table_size"]}, - {"Metric": "Discovery Cache Size", "Value": overview["discovery_cache_size"]} - ] - - output(overview_data, ctx.obj.get('output_format', format), title="Network Overview") - - # Agents by chain - if overview["agents_by_chain"]: - chain_data = [ - {"Chain ID": chain_id, "Total Agents": count, "Active Agents": overview["active_agents_by_chain"].get(chain_id, 0)} - for chain_id, count in overview["agents_by_chain"].items() - ] - - output(chain_data, ctx.obj.get('output_format', format), title="Agents by Chain") - - # Collaborations by type - if overview["collaborations_by_type"]: - collab_data = [ - {"Type": collab_type, "Count": count} - for collab_type, count in overview["collaborations_by_type"].items() - ] - - output(collab_data, ctx.obj.get('output_format', format), title="Collaborations by Type") - - except Exception as e: - error(f"Error getting network overview: {str(e)}") - raise click.Abort() - -@agent_comm.command() -@click.option('--realtime', is_flag=True, help='Real-time monitoring') -@click.option('--interval', default=10, help='Update interval in seconds') -@click.pass_context -def monitor(ctx, realtime, interval): - """Monitor cross-chain agent communication""" - try: - config = load_multichain_config() - comm = CrossChainAgentCommunication(config) - - if realtime: - # Real-time monitoring - from rich.console import Console - from rich.live import Live - from rich.table import Table - import time - - console = Console() - - def generate_monitor_table(): - try: - overview = asyncio.run(comm.get_network_overview()) - - table = Table(title=f"Agent Network Monitor - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") - table.add_column("Metric", style="cyan") - table.add_column("Value", style="green") - - table.add_row("Total Agents", str(overview["total_agents"])) - table.add_row("Active Agents", str(overview["active_agents"])) - table.add_row("Active Collaborations", str(overview["active_collaborations"])) - table.add_row("Queued Messages", str(overview["queued_messages"])) - table.add_row("Avg Reputation", f"{overview['average_reputation']:.3f}") - - # Add top chains by agent count - if overview["agents_by_chain"]: - table.add_row("", "") - table.add_row("Top Chains by Agents", "") - for chain_id, count in sorted(overview["agents_by_chain"].items(), key=lambda x: x[1], reverse=True)[:3]: - active = overview["active_agents_by_chain"].get(chain_id, 0) - table.add_row(f" {chain_id}", f"{count} total, {active} active") - - return table - except Exception as e: - return f"Error getting network data: {e}" - - with Live(generate_monitor_table(), refresh_per_second=1) as live: - try: - while True: - live.update(generate_monitor_table()) - time.sleep(interval) - except KeyboardInterrupt: - console.click.echo("\n[yellow]Monitoring stopped by user[/yellow]") - else: - # Single snapshot - overview = asyncio.run(comm.get_network_overview()) - - monitor_data = [ - {"Metric": "Total Agents", "Value": overview["total_agents"]}, - {"Metric": "Active Agents", "Value": overview["active_agents"]}, - {"Metric": "Total Collaborations", "Value": overview["total_collaborations"]}, - {"Metric": "Active Collaborations", "Value": overview["active_collaborations"]}, - {"Metric": "Total Messages", "Value": overview["total_messages"]}, - {"Metric": "Queued Messages", "Value": overview["queued_messages"]}, - {"Metric": "Average Reputation", "Value": f"{overview['average_reputation']:.3f}"}, - {"Metric": "Routing Table Size", "Value": overview["routing_table_size"]} - ] - - output(monitor_data, ctx.obj.get('output_format', 'table'), title="Agent Network Monitor") - - except Exception as e: - error(f"Error during monitoring: {str(e)}") - raise click.Abort() diff --git a/cli/src/aitbc_cli/commands/agent_sdk.py b/cli/src/aitbc_cli/commands/agent_sdk.py deleted file mode 100644 index 292dea5c..00000000 --- a/cli/src/aitbc_cli/commands/agent_sdk.py +++ /dev/null @@ -1,607 +0,0 @@ -"""Agent SDK commands for AITBC CLI - Basic agent management using the Agent SDK""" - -import asyncio -import json -import sys -from pathlib import Path -from typing import Optional - -# Add Agent SDK to path -sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent.parent.parent / "packages" / "py" / "aitbc-agent-sdk" / "src")) - -try: - from aitbc_agent import Agent, ComputeProvider, ComputeConsumer, AITBCAgent - from aitbc_agent.agent import AgentCapabilities -except ImportError: - # Fallback if Agent SDK is not installed - Agent = None - ComputeProvider = None - ComputeConsumer = None - AITBCAgent = None - - -def get_agent_config_dir() -> Path: - """Get the agent configuration directory""" - config_dir = Path.home() / ".aitbc" / "agents" - config_dir.mkdir(parents=True, exist_ok=True) - return config_dir - - -def create_agent(name: str, agent_type: str, capabilities: dict, coordinator_url: Optional[str] = None) -> dict: - """Create a new agent using the Agent SDK""" - if Agent is None: - return {"error": "Agent SDK not available. Install from packages/py/aitbc-agent-sdk"} - - try: - if agent_type == "provider": - agent = ComputeProvider.create_provider( - name=name, - capabilities=capabilities, - pricing_model={"base_rate": 50.0, "currency": "AITBC"} - ) - elif agent_type == "consumer": - agent = ComputeConsumer.create( - name=name, - agent_type="consumer", - capabilities=capabilities - ) - else: - agent = Agent.create( - name=name, - agent_type=agent_type, - capabilities=capabilities - ) - - if coordinator_url: - agent.coordinator_url = coordinator_url - - # Save agent configuration - config_dir = get_agent_config_dir() - config_file = config_dir / f"{name}.json" - - agent_config = { - "agent_id": agent.identity.id, - "name": agent.identity.name, - "address": agent.identity.address, - "agent_type": agent_type, - "capabilities": capabilities, - "coordinator_url": coordinator_url or config.coordinator_url - } - - with open(config_file, 'w') as f: - json.dump(agent_config, f, indent=2) - - return { - "success": True, - "agent_id": agent.identity.id, - "name": agent.identity.name, - "address": agent.identity.address, - "agent_type": agent_type, - "capabilities": capabilities, - "config_file": str(config_file) - } - except Exception as e: - return {"error": str(e)} - - -async def register_agent(agent_id: str, coordinator_url: str = None) -> dict: - """Register an agent with the coordinator""" - if coordinator_url is None: - config = get_config() - coordinator_url = config.coordinator_url - if Agent is None: - return {"error": "Agent SDK not available"} - - try: - # For now, return a simulated registration response - # In a real implementation, this would load the agent from storage and call register() - return { - "success": True, - "agent_id": agent_id, - "registered": True, - "coordinator_url": coordinator_url, - "message": "Agent registered successfully (simulated)" - } - except Exception as e: - return {"error": str(e)} - - -def get_agent_capabilities() -> dict: - """Get auto-detected system capabilities for creating a provider""" - if ComputeProvider is None: - return {"error": "Agent SDK not available"} - - try: - return ComputeProvider.assess_capabilities() - except Exception as e: - return {"error": str(e)} - - -def list_local_agents(agent_dir: Optional[Path] = None) -> list: - """List locally stored agent configurations""" - if agent_dir is None: - agent_dir = get_agent_config_dir() - - agents = [] - if agent_dir.exists(): - for agent_file in agent_dir.glob("*.json"): - try: - with open(agent_file) as f: - agent_data = json.load(f) - agents.append({ - "name": agent_file.stem, - "file": str(agent_file), - **agent_data - }) - except Exception: - pass - - return agents - - -def get_agent_status(agent_id: str) -> dict: - """Get status information for an agent""" - # For now, return a simulated status - # In a real implementation, this would query the coordinator - return { - "agent_id": agent_id, - "status": "active", - "registered": True, - "reputation_score": 0.85, - "last_seen": "2026-04-29T09:40:00Z", - "message": "Agent status retrieved (simulated)" - } - - -def set_agent_config(name: str, key: str, value: str) -> dict: - """Set a configuration value for an agent""" - try: - config_dir = get_agent_config_dir() - config_file = config_dir / f"{name}.json" - - if not config_file.exists(): - return {"error": f"Agent configuration not found: {name}"} - - with open(config_file) as f: - config = json.load(f) - - # Parse value (handle JSON for complex values) - try: - parsed_value = json.loads(value) - except json.JSONDecodeError: - parsed_value = value - - config[key] = parsed_value - - with open(config_file, 'w') as f: - json.dump(config, f, indent=2) - - return { - "success": True, - "name": name, - "key": key, - "value": parsed_value - } - except Exception as e: - return {"error": str(e)} - - -def get_agent_config(name: str, key: Optional[str] = None) -> dict: - """Get configuration value(s) for an agent""" - try: - config_dir = get_agent_config_dir() - config_file = config_dir / f"{name}.json" - - if not config_file.exists(): - return {"error": f"Agent configuration not found: {name}"} - - with open(config_file) as f: - config = json.load(f) - - if key: - if key not in config: - return {"error": f"Configuration key not found: {key}"} - return { - "success": True, - "name": name, - "key": key, - "value": config[key] - } - else: - return { - "success": True, - "name": name, - "config": config - } - except Exception as e: - return {"error": str(e)} - - -def validate_agent_config(name: str) -> dict: - """Validate agent configuration""" - try: - config_dir = get_agent_config_dir() - config_file = config_dir / f"{name}.json" - - if not config_file.exists(): - return {"error": f"Agent configuration not found: {name}"} - - with open(config_file) as f: - config = json.load(f) - - # Validate required fields - required_fields = ["agent_id", "name", "address", "agent_type", "capabilities"] - missing_fields = [field for field in required_fields if field not in config] - - if missing_fields: - return { - "valid": False, - "error": f"Missing required fields: {', '.join(missing_fields)}" - } - - # Validate capabilities structure - capabilities = config.get("capabilities", {}) - if "compute_type" not in capabilities: - return { - "valid": False, - "error": "Missing compute_type in capabilities" - } - - return { - "valid": True, - "name": name, - "message": "Configuration is valid" - } - except Exception as e: - return {"valid": False, "error": str(e)} - - -def import_agent_config(file_path: str, name: Optional[str] = None) -> dict: - """Import agent configuration from file""" - try: - import_file = Path(file_path) - if not import_file.exists(): - return {"error": f"File not found: {file_path}"} - - with open(import_file) as f: - config = json.load(f) - - # Use name from file or override - agent_name = name or config.get("name", import_file.stem) - config["name"] = agent_name - - # Save to agent config directory - config_dir = get_agent_config_dir() - config_file = config_dir / f"{agent_name}.json" - - with open(config_file, 'w') as f: - json.dump(config, f, indent=2) - - return { - "success": True, - "name": agent_name, - "config_file": str(config_file), - "imported_from": file_path - } - except Exception as e: - return {"error": str(e)} - - -def export_agent_config(name: str, output_path: str) -> dict: - """Export agent configuration to file""" - try: - config_dir = get_agent_config_dir() - config_file = config_dir / f"{name}.json" - - if not config_file.exists(): - return {"error": f"Agent configuration not found: {name}"} - - with open(config_file) as f: - config = json.load(f) - - output_file = Path(output_path) - output_file.parent.mkdir(parents=True, exist_ok=True) - - with open(output_file, 'w') as f: - json.dump(config, f, indent=2) - - return { - "success": True, - "name": name, - "exported_to": output_path - } - except Exception as e: - return {"error": str(e)} - - -# CLI command handlers using Click -try: - import click - from ..utils import output, error, success - - @click.group() - def agent(): - """Agent SDK management commands""" - pass - - @agent.command() - @click.argument('name') - @click.option('--type', 'agent_type', default='provider', type=click.Choice(['provider', 'consumer', 'general']), help='Agent type') - @click.option('--compute-type', default='inference', help='Compute type (inference, training, processing)') - @click.option('--gpu-memory', type=int, help='GPU memory in GB') - @click.option('--models', help='Comma-separated list of supported models') - @click.option('--performance', type=float, default=0.8, help='Performance score (0.0-1.0)') - @click.option('--max-jobs', type=int, default=1, help='Maximum concurrent jobs') - @click.option('--specialization', help='Agent specialization') - @click.option('--coordinator-url', help='Coordinator URL') - @click.option('--auto-detect', is_flag=True, help='Auto-detect capabilities') - @click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format') - @click.pass_context - def create(ctx, name, agent_type, compute_type, gpu_memory, models, performance, max_jobs, specialization, coordinator_url, auto_detect, format): - """Create a new agent""" - try: - # Build capabilities - if auto_detect: - capabilities = get_agent_capabilities() - if "error" in capabilities: - error(f"Auto-detection failed: {capabilities['error']}") - raise click.Abort() - else: - capabilities = { - "compute_type": compute_type, - "performance_score": performance, - "max_concurrent_jobs": max_jobs - } - - if gpu_memory: - capabilities["gpu_memory"] = gpu_memory - - if models: - capabilities["supported_models"] = [m.strip() for m in models.split(',')] - - if specialization: - capabilities["specialization"] = specialization - - # Create agent - result = create_agent(name, agent_type, capabilities, coordinator_url) - - if "error" in result: - error(f"Failed to create agent: {result['error']}") - raise click.Abort() - - success(f"Agent created successfully!") - - agent_data = [ - {"Field": "Agent ID", "Value": result["agent_id"]}, - {"Field": "Name", "Value": result["name"]}, - {"Field": "Address", "Value": result["address"]}, - {"Field": "Type", "Value": result["agent_type"]}, - {"Field": "Compute Type", "Value": capabilities.get("compute_type", "N/A")}, - {"Field": "GPU Memory", "Value": f"{capabilities.get('gpu_memory', 'N/A')} GB"}, - {"Field": "Performance Score", "Value": f"{capabilities.get('performance_score', 'N/A'):.2f}"}, - {"Field": "Max Jobs", "Value": capabilities.get("max_concurrent_jobs", "N/A")}, - {"Field": "Config File", "Value": result.get("config_file", "N/A")} - ] - - output(agent_data, ctx.obj.get('output_format', format), title="Agent Created") - - except Exception as e: - error(f"Error creating agent: {str(e)}") - raise click.Abort() - - @agent.command() - @click.argument('agent_id') - @click.option('--coordinator-url', default='http://localhost:9001', help='Coordinator URL') - @click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format') - @click.pass_context - def register(ctx, agent_id, coordinator_url, format): - """Register an agent with the coordinator""" - try: - result = asyncio.run(register_agent(agent_id, coordinator_url)) - - if "error" in result: - error(f"Failed to register agent: {result['error']}") - raise click.Abort() - - success(f"Agent {agent_id} registered successfully!") - - reg_data = [ - {"Field": "Agent ID", "Value": result["agent_id"]}, - {"Field": "Registered", "Value": str(result["registered"])}, - {"Field": "Coordinator URL", "Value": result["coordinator_url"]}, - {"Field": "Message", "Value": result["message"]} - ] - - output(reg_data, ctx.obj.get('output_format', format), title="Agent Registration") - - except Exception as e: - error(f"Error registering agent: {str(e)}") - raise click.Abort() - - @agent.command() - @click.option('--agent-dir', type=click.Path(), help='Agent directory path') - @click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format') - @click.pass_context - def list(ctx, agent_dir, format): - """List local agents""" - try: - agents = list_local_agents(Path(agent_dir) if agent_dir else None) - - if not agents: - output("No local agents found", ctx.obj.get('output_format', format)) - return - - agent_list = [ - { - "Name": agent["name"], - "Type": agent.get("agent_type", "unknown"), - "Address": agent.get("address", "N/A"), - "File": agent["file"] - } - for agent in agents - ] - - output(agent_list, ctx.obj.get('output_format', format), title="Local Agents") - - except Exception as e: - error(f"Error listing agents: {str(e)}") - raise click.Abort() - - @agent.command() - @click.argument('agent_id') - @click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format') - @click.pass_context - def status(ctx, agent_id, format): - """Get agent status""" - try: - status_data = get_agent_status(agent_id) - - status_list = [ - {"Field": "Agent ID", "Value": status_data["agent_id"]}, - {"Field": "Status", "Value": status_data["status"]}, - {"Field": "Registered", "Value": str(status_data["registered"])}, - {"Field": "Reputation Score", "Value": f"{status_data['reputation_score']:.3f}"}, - {"Field": "Last Seen", "Value": status_data["last_seen"]}, - {"Field": "Message", "Value": status_data["message"]} - ] - - output(status_list, ctx.obj.get('output_format', format), title=f"Agent Status: {agent_id}") - - except Exception as e: - error(f"Error getting agent status: {str(e)}") - raise click.Abort() - - @agent.command() - @click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format') - @click.pass_context - def capabilities(ctx, format): - """Show auto-detected system capabilities""" - try: - caps = get_agent_capabilities() - - if "error" in caps: - error(f"Failed to detect capabilities: {caps['error']}") - raise click.Abort() - - caps_list = [ - {"Field": "GPU Memory", "Value": f"{caps['gpu_memory']} MiB"}, - {"Field": "GPU Count", "Value": str(caps.get('gpu_count', 0))}, - {"Field": "Compute Capability", "Value": caps.get('compute_capability', 'unknown')}, - {"Field": "Performance Score", "Value": f"{caps['performance_score']:.2f}"}, - {"Field": "Max Concurrent Jobs", "Value": str(caps['max_concurrent_jobs'])}, - {"Field": "Supported Models", "Value": ", ".join(caps.get('supported_models', []))} - ] - - output(caps_list, ctx.obj.get('output_format', format), title="System Capabilities") - - except Exception as e: - error(f"Error detecting capabilities: {str(e)}") - raise click.Abort() - - @agent.command() - @click.argument('name') - @click.argument('key') - @click.argument('value') - @click.pass_context - def config_set(ctx, name, key, value): - """Set a configuration value for an agent""" - try: - result = set_agent_config(name, key, value) - - if "error" in result: - error(f"Failed to set configuration: {result['error']}") - raise click.Abort() - - success(f"Configuration set: {name}.{key} = {result['value']}") - - except Exception as e: - error(f"Error setting configuration: {str(e)}") - raise click.Abort() - - @agent.command() - @click.argument('name') - @click.option('--key', help='Specific configuration key to retrieve') - @click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format') - @click.pass_context - def config_get(ctx, name, key, format): - """Get configuration value(s) for an agent""" - try: - result = get_agent_config(name, key) - - if "error" in result: - error(f"Failed to get configuration: {result['error']}") - raise click.Abort() - - if key: - config_data = [ - {"Field": "Name", "Value": result["name"]}, - {"Field": "Key", "Value": result["key"]}, - {"Field": "Value", "Value": str(result["value"])} - ] - output(config_data, ctx.obj.get('output_format', format), title=f"Agent Config: {name}.{key}") - else: - output(result["config"], ctx.obj.get('output_format', format), title=f"Agent Config: {name}") - - except Exception as e: - error(f"Error getting configuration: {str(e)}") - raise click.Abort() - - @agent.command() - @click.argument('name') - @click.pass_context - def config_validate(ctx, name): - """Validate agent configuration""" - try: - result = validate_agent_config(name) - - if result.get("valid"): - success(f"Configuration is valid: {name}") - else: - error(f"Configuration validation failed: {result.get('error')}") - raise click.Abort() - - except Exception as e: - error(f"Error validating configuration: {str(e)}") - raise click.Abort() - - @agent.command() - @click.argument('file_path') - @click.option('--name', help='Override agent name') - @click.pass_context - def config_import(ctx, file_path, name): - """Import agent configuration from file""" - try: - result = import_agent_config(file_path, name) - - if "error" in result: - error(f"Failed to import configuration: {result['error']}") - raise click.Abort() - - success(f"Configuration imported: {result['name']} -> {result['config_file']}") - - except Exception as e: - error(f"Error importing configuration: {str(e)}") - raise click.Abort() - - @agent.command() - @click.argument('name') - @click.argument('output_path') - @click.pass_context - def config_export(ctx, name, output_path): - """Export agent configuration to file""" - try: - result = export_agent_config(name, output_path) - - if "error" in result: - error(f"Failed to export configuration: {result['error']}") - raise click.Abort() - - success(f"Configuration exported: {name} -> {result['exported_to']}") - - except Exception as e: - error(f"Error exporting configuration: {str(e)}") - raise click.Abort() - -except ImportError: - # Click not available, commands will be added programmatically - pass diff --git a/cli/src/aitbc_cli/commands/analytics.py b/cli/src/aitbc_cli/commands/analytics.py deleted file mode 100755 index 40e2d15b..00000000 --- a/cli/src/aitbc_cli/commands/analytics.py +++ /dev/null @@ -1,402 +0,0 @@ -"""Analytics and monitoring commands for AITBC CLI""" - -import click -import asyncio -from datetime import datetime, timedelta -from typing import Optional -from ..core.config import load_multichain_config -from ..core.analytics import ChainAnalytics -from ..utils import output, error, success - -@click.group() -def analytics(): - """Chain analytics and monitoring commands""" - pass - -@analytics.command() -@click.option('--chain-id', help='Specific chain ID to analyze') -@click.option('--hours', default=24, help='Time range in hours') -@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format') -@click.pass_context -def summary(ctx, chain_id, hours, format): - """Get performance summary for chains""" - try: - config = load_multichain_config() - analytics = ChainAnalytics(config) - - if chain_id: - # Single chain summary - summary = analytics.get_chain_performance_summary(chain_id, hours) - if not summary: - error(f"No data available for chain {chain_id}") - raise click.Abort() - - # Format summary for display - summary_data = [ - {"Metric": "Chain ID", "Value": summary["chain_id"]}, - {"Metric": "Time Range", "Value": f"{summary['time_range_hours']} hours"}, - {"Metric": "Data Points", "Value": summary["data_points"]}, - {"Metric": "Health Score", "Value": f"{summary['health_score']:.1f}/100"}, - {"Metric": "Active Alerts", "Value": summary["active_alerts"]}, - {"Metric": "Avg TPS", "Value": f"{summary['statistics']['tps']['avg']:.2f}"}, - {"Metric": "Avg Block Time", "Value": f"{summary['statistics']['block_time']['avg']:.2f}s"}, - {"Metric": "Avg Gas Price", "Value": f"{summary['statistics']['gas_price']['avg']:,} wei"} - ] - - output(summary_data, ctx.obj.get('output_format', format), title=f"Chain Summary: {chain_id}") - else: - # Cross-chain analysis - analysis = analytics.get_cross_chain_analysis() - - if not analysis: - error("No analytics data available") - raise click.Abort() - - # Overview data - overview_data = [ - {"Metric": "Total Chains", "Value": analysis["total_chains"]}, - {"Metric": "Active Chains", "Value": analysis["active_chains"]}, - {"Metric": "Total Alerts", "Value": analysis["alerts_summary"]["total_alerts"]}, - {"Metric": "Critical Alerts", "Value": analysis["alerts_summary"]["critical_alerts"]}, - {"Metric": "Total Memory Usage", "Value": f"{analysis['resource_usage']['total_memory_mb']:.1f}MB"}, - {"Metric": "Total Disk Usage", "Value": f"{analysis['resource_usage']['total_disk_mb']:.1f}MB"}, - {"Metric": "Total Clients", "Value": analysis["resource_usage"]["total_clients"]}, - {"Metric": "Total Agents", "Value": analysis["resource_usage"]["total_agents"]} - ] - - output(overview_data, ctx.obj.get('output_format', format), title="Cross-Chain Analysis Overview") - - # Performance comparison - if analysis["performance_comparison"]: - comparison_data = [ - { - "Chain ID": chain_id, - "TPS": f"{data['tps']:.2f}", - "Block Time": f"{data['block_time']:.2f}s", - "Health Score": f"{data['health_score']:.1f}/100" - } - for chain_id, data in analysis["performance_comparison"].items() - ] - - output(comparison_data, ctx.obj.get('output_format', format), title="Chain Performance Comparison") - - except Exception as e: - error(f"Error getting analytics summary: {str(e)}") - raise click.Abort() - -@analytics.command() -@click.option('--realtime', is_flag=True, help='Real-time monitoring') -@click.option('--interval', default=30, help='Update interval in seconds') -@click.option('--chain-id', help='Monitor specific chain') -@click.pass_context -def monitor(ctx, realtime, interval, chain_id): - """Monitor chain performance in real-time""" - try: - config = load_multichain_config() - analytics = ChainAnalytics(config) - - if realtime: - # Real-time monitoring - from rich.console import Console - from rich.live import Live - from rich.table import Table - import time - - console = Console() - - def generate_monitor_table(): - try: - # Collect latest metrics - asyncio.run(analytics.collect_all_metrics()) - - table = Table(title=f"Chain Monitor - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") - table.add_column("Chain ID", style="cyan") - table.add_column("TPS", style="green") - table.add_column("Block Time", style="yellow") - table.add_column("Health", style="red") - table.add_column("Alerts", style="magenta") - - if chain_id: - # Single chain monitoring - summary = analytics.get_chain_performance_summary(chain_id, 1) - if summary: - health_color = "green" if summary["health_score"] > 70 else "yellow" if summary["health_score"] > 40 else "red" - table.add_row( - chain_id, - f"{summary['statistics']['tps']['avg']:.2f}", - f"{summary['statistics']['block_time']['avg']:.2f}s", - f"[{health_color}]{summary['health_score']:.1f}[/{health_color}]", - str(summary["active_alerts"]) - ) - else: - # All chains monitoring - analysis = analytics.get_cross_chain_analysis() - for chain_id, data in analysis["performance_comparison"].items(): - health_color = "green" if data["health_score"] > 70 else "yellow" if data["health_score"] > 40 else "red" - table.add_row( - chain_id, - f"{data['tps']:.2f}", - f"{data['block_time']:.2f}s", - f"[{health_color}]{data['health_score']:.1f}[/{health_color}]", - str(len([a for a in analytics.alerts if a.chain_id == chain_id])) - ) - - return table - except Exception as e: - return f"Error collecting metrics: {e}" - - with Live(generate_monitor_table(), refresh_per_second=1) as live: - try: - while True: - live.update(generate_monitor_table()) - time.sleep(interval) - except KeyboardInterrupt: - console.click.echo("\n[yellow]Monitoring stopped by user[/yellow]") - else: - # Single snapshot - asyncio.run(analytics.collect_all_metrics()) - - if chain_id: - summary = analytics.get_chain_performance_summary(chain_id, 1) - if not summary: - error(f"No data available for chain {chain_id}") - raise click.Abort() - - monitor_data = [ - {"Metric": "Chain ID", "Value": summary["chain_id"]}, - {"Metric": "Current TPS", "Value": f"{summary['statistics']['tps']['avg']:.2f}"}, - {"Metric": "Current Block Time", "Value": f"{summary['statistics']['block_time']['avg']:.2f}s"}, - {"Metric": "Health Score", "Value": f"{summary['health_score']:.1f}/100"}, - {"Metric": "Active Alerts", "Value": summary["active_alerts"]}, - {"Metric": "Memory Usage", "Value": f"{summary['latest_metrics']['memory_usage_mb']:.1f}MB"}, - {"Metric": "Disk Usage", "Value": f"{summary['latest_metrics']['disk_usage_mb']:.1f}MB"}, - {"Metric": "Active Nodes", "Value": summary["latest_metrics"]["active_nodes"]}, - {"Metric": "Client Count", "Value": summary["latest_metrics"]["client_count"]}, - {"Metric": "Agent Count", "Value": summary["latest_metrics"]["agent_count"]} - ] - - output(monitor_data, ctx.obj.get('output_format', 'table'), title=f"Chain Monitor: {chain_id}") - else: - analysis = analytics.get_cross_chain_analysis() - - monitor_data = [ - {"Metric": "Total Chains", "Value": analysis["total_chains"]}, - {"Metric": "Active Chains", "Value": analysis["active_chains"]}, - {"Metric": "Total Memory Usage", "Value": f"{analysis['resource_usage']['total_memory_mb']:.1f}MB"}, - {"Metric": "Total Disk Usage", "Value": f"{analysis['resource_usage']['total_disk_mb']:.1f}MB"}, - {"Metric": "Total Clients", "Value": analysis["resource_usage"]["total_clients"]}, - {"Metric": "Total Agents", "Value": analysis["resource_usage"]["total_agents"]}, - {"Metric": "Total Alerts", "Value": analysis["alerts_summary"]["total_alerts"]}, - {"Metric": "Critical Alerts", "Value": analysis["alerts_summary"]["critical_alerts"]} - ] - - output(monitor_data, ctx.obj.get('output_format', 'table'), title="System Monitor") - - except Exception as e: - error(f"Error during monitoring: {str(e)}") - raise click.Abort() - -@analytics.command() -@click.option('--chain-id', help='Specific chain ID for predictions') -@click.option('--hours', default=24, help='Prediction time horizon in hours') -@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format') -@click.pass_context -def predict(ctx, chain_id, hours, format): - """Predict chain performance""" - try: - config = load_multichain_config() - analytics = ChainAnalytics(config) - - # Collect current metrics first - asyncio.run(analytics.collect_all_metrics()) - - if chain_id: - # Single chain prediction - predictions = asyncio.run(analytics.predict_chain_performance(chain_id, hours)) - - if not predictions: - error(f"No prediction data available for chain {chain_id}") - raise click.Abort() - - prediction_data = [ - { - "Metric": pred.metric, - "Predicted Value": f"{pred.predicted_value:.2f}", - "Confidence": f"{pred.confidence:.1%}", - "Time Horizon": f"{pred.time_horizon_hours}h" - } - for pred in predictions - ] - - output(prediction_data, ctx.obj.get('output_format', format), title=f"Performance Predictions: {chain_id}") - else: - # All chains prediction - analysis = analytics.get_cross_chain_analysis() - all_predictions = {} - - for chain_id in analysis["performance_comparison"].keys(): - predictions = asyncio.run(analytics.predict_chain_performance(chain_id, hours)) - if predictions: - all_predictions[chain_id] = predictions - - if not all_predictions: - error("No prediction data available") - raise click.Abort() - - # Format predictions for display - prediction_data = [] - for chain_id, predictions in all_predictions.items(): - for pred in predictions: - prediction_data.append({ - "Chain ID": chain_id, - "Metric": pred.metric, - "Predicted Value": f"{pred.predicted_value:.2f}", - "Confidence": f"{pred.confidence:.1%}", - "Time Horizon": f"{pred.time_horizon_hours}h" - }) - - output(prediction_data, ctx.obj.get('output_format', format), title="Chain Performance Predictions") - - except Exception as e: - error(f"Error generating predictions: {str(e)}") - raise click.Abort() - -@analytics.command() -@click.option('--chain-id', help='Specific chain ID for recommendations') -@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format') -@click.pass_context -def optimize(ctx, chain_id, format): - """Get optimization recommendations""" - try: - config = load_multichain_config() - analytics = ChainAnalytics(config) - - # Collect current metrics first - asyncio.run(analytics.collect_all_metrics()) - - if chain_id: - # Single chain recommendations - recommendations = analytics.get_optimization_recommendations(chain_id) - - if not recommendations: - success(f"No optimization recommendations for chain {chain_id}") - return - - recommendation_data = [ - { - "Type": rec["type"], - "Priority": rec["priority"], - "Issue": rec["issue"], - "Current Value": rec["current_value"], - "Recommended Action": rec["recommended_action"], - "Expected Improvement": rec["expected_improvement"] - } - for rec in recommendations - ] - - output(recommendation_data, ctx.obj.get('output_format', format), title=f"Optimization Recommendations: {chain_id}") - else: - # All chains recommendations - analysis = analytics.get_cross_chain_analysis() - all_recommendations = {} - - for chain_id in analysis["performance_comparison"].keys(): - recommendations = analytics.get_optimization_recommendations(chain_id) - if recommendations: - all_recommendations[chain_id] = recommendations - - if not all_recommendations: - success("No optimization recommendations available") - return - - # Format recommendations for display - recommendation_data = [] - for chain_id, recommendations in all_recommendations.items(): - for rec in recommendations: - recommendation_data.append({ - "Chain ID": chain_id, - "Type": rec["type"], - "Priority": rec["priority"], - "Issue": rec["issue"], - "Current Value": rec["current_value"], - "Recommended Action": rec["recommended_action"] - }) - - output(recommendation_data, ctx.obj.get('output_format', format), title="Chain Optimization Recommendations") - - except Exception as e: - error(f"Error getting optimization recommendations: {str(e)}") - raise click.Abort() - -@analytics.command() -@click.option('--severity', type=click.Choice(['all', 'critical', 'warning']), default='all', help='Alert severity filter') -@click.option('--hours', default=24, help='Time range in hours') -@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format') -@click.pass_context -def alerts(ctx, severity, hours, format): - """View performance alerts""" - try: - config = load_multichain_config() - analytics = ChainAnalytics(config) - - # Collect current metrics first - asyncio.run(analytics.collect_all_metrics()) - - # Filter alerts - cutoff_time = datetime.now() - timedelta(hours=hours) - filtered_alerts = [ - alert for alert in analytics.alerts - if alert.timestamp >= cutoff_time - ] - - if severity != 'all': - filtered_alerts = [a for a in filtered_alerts if a.severity == severity] - - if not filtered_alerts: - success("No alerts found") - return - - alert_data = [ - { - "Chain ID": alert.chain_id, - "Type": alert.alert_type, - "Severity": alert.severity, - "Message": alert.message, - "Current Value": f"{alert.current_value:.2f}", - "Threshold": f"{alert.threshold:.2f}", - "Time": alert.timestamp.strftime("%Y-%m-%d %H:%M:%S") - } - for alert in filtered_alerts - ] - - output(alert_data, ctx.obj.get('output_format', format), title=f"Performance Alerts (Last {hours}h)") - - except Exception as e: - error(f"Error getting alerts: {str(e)}") - raise click.Abort() - -@analytics.command() -@click.option('--format', type=click.Choice(['json']), default='json', help='Output format') -@click.pass_context -def dashboard(ctx, format): - """Get complete dashboard data""" - try: - config = load_multichain_config() - analytics = ChainAnalytics(config) - - # Collect current metrics - asyncio.run(analytics.collect_all_metrics()) - - # Get dashboard data - dashboard_data = analytics.get_dashboard_data() - - if format == 'json': - import json - click.echo(json.dumps(dashboard_data, indent=2, default=str)) - else: - error("Dashboard data only available in JSON format") - raise click.Abort() - - except Exception as e: - error(f"Error getting dashboard data: {str(e)}") - raise click.Abort() diff --git a/cli/src/aitbc_cli/commands/chain.py b/cli/src/aitbc_cli/commands/chain.py deleted file mode 100755 index 5c2640aa..00000000 --- a/cli/src/aitbc_cli/commands/chain.py +++ /dev/null @@ -1,564 +0,0 @@ -"""Chain management commands for AITBC CLI""" - -import click -from click import echo -from typing import Optional -from ..core.chain_manager import ChainManager, ChainNotFoundError, NodeNotAvailableError -from ..core.config import MultiChainConfig, load_multichain_config -from ..models.chain import ChainType -from ..utils import output, error, success - -@click.group() -def chain(): - """Multi-chain management commands""" - pass - -@chain.command() -@click.option('--type', 'chain_type', type=click.Choice(['main', 'topic', 'private', 'all']), - default='all', help='Filter by chain type') -@click.option('--show-private', is_flag=True, help='Show private chains') -@click.option('--sort', type=click.Choice(['id', 'size', 'nodes', 'created']), - default='id', help='Sort by field') -@click.pass_context -def list(ctx, chain_type, show_private, sort): - """List all available chains""" - try: - config = load_multichain_config() - chain_manager = ChainManager(config) - - # Get chains - import asyncio - chains = asyncio.run(chain_manager.list_chains( - chain_type=ChainType(chain_type) if chain_type != 'all' else None, - include_private=show_private, - sort_by=sort - )) - - if not chains: - output("No chains found", ctx.obj.get('output_format', 'table')) - return - - # Format output - chains_data = [ - { - "Chain ID": chain.id, - "Type": chain.type.value, - "Purpose": chain.purpose, - "Name": chain.name, - "Size": f"{chain.size_mb:.1f}MB", - "Nodes": chain.node_count, - "Contracts": chain.contract_count, - "Clients": chain.client_count, - "Miners": chain.miner_count, - "Status": chain.status.value - } - for chain in chains - ] - - output(chains_data, ctx.obj.get('output_format', 'table'), title="Available Chains") - - except Exception as e: - error(f"Error listing chains: {str(e)}") - raise click.Abort() - -@chain.command() -@click.option('--chain-id', help='Specific chain ID to check status (shows all if not specified)') -@click.option('--detailed', is_flag=True, help='Show detailed status information') -@click.pass_context -def status(ctx, chain_id, detailed): - """Check status of chains""" - try: - config = load_multichain_config() - chain_manager = ChainManager(config) - - import asyncio - - if chain_id: - # Get specific chain status - chain_info = asyncio.run(chain_manager.get_chain_info(chain_id, detailed=detailed)) - - status_data = { - "Chain ID": chain_info.id, - "Name": chain_info.name, - "Type": chain_info.type.value, - "Status": chain_info.status.value, - "Block Height": chain_info.block_height, - "Active Nodes": chain_info.active_nodes, - "Total Nodes": chain_info.node_count - } - - if detailed: - status_data.update({ - "Consensus": chain_info.consensus_algorithm.value, - "TPS": f"{chain_info.tps:.1f}", - "Gas Price": f"{chain_info.gas_price / 1e9:.1f} gwei", - "Memory Usage": f"{chain_info.memory_usage_mb:.1f}MB" - }) - - output(status_data, ctx.obj.get('output_format', 'table'), title=f"Chain Status: {chain_id}") - else: - # Get all chains status - chains = asyncio.run(chain_manager.list_chains()) - - if not chains: - output({"message": "No chains found"}, ctx.obj.get('output_format', 'table')) - return - - status_list = [] - for chain in chains: - status_info = { - "Chain ID": chain.id, - "Name": chain.name, - "Type": chain.type.value, - "Status": chain.status.value, - "Block Height": chain.block_height, - "Active Nodes": chain.active_nodes - } - status_list.append(status_info) - - # Simple output without formatting - echo(status_list) - - except ChainNotFoundError: - error(f"Chain {chain_id} not found") - raise click.Abort() - except Exception as e: - error(f"Error getting chain status: {str(e)}") - raise click.Abort() - -@chain.command() -@click.argument('chain_id') -@click.option('--detailed', is_flag=True, help='Show detailed information') -@click.option('--metrics', is_flag=True, help='Show performance metrics') -@click.pass_context -def info(ctx, chain_id, detailed, metrics): - """Get detailed information about a chain""" - try: - config = load_multichain_config() - chain_manager = ChainManager(config) - - import asyncio - chain_info = asyncio.run(chain_manager.get_chain_info(chain_id, detailed, metrics)) - - # Basic information - basic_info = { - "Chain ID": chain_info.id, - "Type": chain_info.type.value, - "Purpose": chain_info.purpose, - "Name": chain_info.name, - "Description": chain_info.description or "No description", - "Status": chain_info.status.value, - "Created": chain_info.created_at.strftime("%Y-%m-%d %H:%M:%S"), - "Block Height": chain_info.block_height, - "Size": f"{chain_info.size_mb:.1f}MB" - } - - output(basic_info, ctx.obj.get('output_format', 'table'), title=f"Chain Information: {chain_id}") - - if detailed: - # Network details - network_info = { - "Total Nodes": chain_info.node_count, - "Active Nodes": chain_info.active_nodes, - "Consensus": chain_info.consensus_algorithm.value, - "Block Time": f"{chain_info.block_time}s", - "Clients": chain_info.client_count, - "Miners": chain_info.miner_count, - "Contracts": chain_info.contract_count, - "Agents": chain_info.agent_count, - "Privacy": chain_info.privacy.visibility, - "Access Control": chain_info.privacy.access_control - } - - output(network_info, ctx.obj.get('output_format', 'table'), title="Network Details") - - if metrics: - # Performance metrics - performance_info = { - "TPS": f"{chain_info.tps:.1f}", - "Avg Block Time": f"{chain_info.avg_block_time:.1f}s", - "Avg Gas Used": f"{chain_info.avg_gas_used:,}", - "Gas Price": f"{chain_info.gas_price / 1e9:.1f} gwei", - "Growth Rate": f"{chain_info.growth_rate_mb_per_day:.1f}MB/day", - "Memory Usage": f"{chain_info.memory_usage_mb:.1f}MB", - "Disk Usage": f"{chain_info.disk_usage_mb:.1f}MB" - } - - output(performance_info, ctx.obj.get('output_format', 'table'), title="Performance Metrics") - - except ChainNotFoundError: - error(f"Chain {chain_id} not found") - raise click.Abort() - except Exception as e: - error(f"Error getting chain info: {str(e)}") - raise click.Abort() - -@chain.command() -@click.argument('config_file', type=click.Path(exists=True)) -@click.option('--node', help='Target node for chain creation') -@click.option('--dry-run', is_flag=True, help='Show what would be created without actually creating') -@click.pass_context -def create(ctx, config_file, node, dry_run): - """Create a new chain from configuration file""" - try: - import yaml - from ..models.chain import ChainConfig - - config = load_multichain_config() - chain_manager = ChainManager(config) - - # Load and validate configuration - with open(config_file, 'r') as f: - config_data = yaml.safe_load(f) - - chain_config = ChainConfig(**config_data['chain']) - - if dry_run: - dry_run_info = { - "Chain Type": chain_config.type.value, - "Purpose": chain_config.purpose, - "Name": chain_config.name, - "Description": chain_config.description or "No description", - "Consensus": chain_config.consensus.algorithm.value, - "Privacy": chain_config.privacy.visibility, - "Target Node": node or "Auto-selected" - } - - output(dry_run_info, ctx.obj.get('output_format', 'table'), title="Dry Run - Chain Creation") - return - - # Create chain - chain_id = chain_manager.create_chain(chain_config, node) - - success(f"Chain created successfully!") - result = { - "Chain ID": chain_id, - "Type": chain_config.type.value, - "Purpose": chain_config.purpose, - "Name": chain_config.name, - "Node": node or "Auto-selected" - } - - output(result, ctx.obj.get('output_format', 'table')) - - if chain_config.privacy.visibility == "private": - success("Private chain created! Use access codes to invite participants.") - - except Exception as e: - error(f"Error creating chain: {str(e)}") - raise click.Abort() - -@chain.command() -@click.argument('chain_id') -@click.option('--force', is_flag=True, help='Force deletion without confirmation') -@click.option('--confirm', is_flag=True, help='Confirm deletion') -@click.pass_context -def delete(ctx, chain_id, force, confirm): - """Delete a chain permanently""" - try: - config = load_multichain_config() - chain_manager = ChainManager(config) - - # Get chain information for confirmation - import asyncio - chain_info = asyncio.run(chain_manager.get_chain_info(chain_id, detailed=True)) - - if not force: - # Show warning and confirmation - warning_info = { - "Chain ID": chain_id, - "Type": chain_info.type.value, - "Purpose": chain_info.purpose, - "Name": chain_info.name, - "Status": chain_info.status.value, - "Participants": chain_info.client_count, - "Transactions": "Multiple" # Would get actual count - } - - output(warning_info, ctx.obj.get('output_format', 'table'), title="Chain Deletion Warning") - - if not confirm: - error("To confirm deletion, use --confirm flag") - raise click.Abort() - - # Delete chain - import asyncio - is_success = asyncio.run(chain_manager.delete_chain(chain_id, force)) - - if is_success: - success(f"Chain {chain_id} deleted successfully!") - else: - error(f"Failed to delete chain {chain_id}") - raise click.Abort() - - except ChainNotFoundError: - error(f"Chain {chain_id} not found") - raise click.Abort() - except Exception as e: - error(f"Error deleting chain: {str(e)}") - raise click.Abort() - -@chain.command() -@click.argument('chain_id') -@click.argument('node_id') -@click.pass_context -def add(ctx, chain_id, node_id): - """Add a chain to a specific node""" - try: - config = load_multichain_config() - chain_manager = ChainManager(config) - - import asyncio - is_success = asyncio.run(chain_manager.add_chain_to_node(chain_id, node_id)) - - if is_success: - success(f"Chain {chain_id} added to node {node_id} successfully!") - else: - error(f"Failed to add chain {chain_id} to node {node_id}") - raise click.Abort() - - except Exception as e: - error(f"Error adding chain to node: {str(e)}") - raise click.Abort() - -@chain.command() -@click.argument('chain_id') -@click.argument('node_id') -@click.option('--migrate', is_flag=True, help='Migrate to another node before removal') -@click.pass_context -def remove(ctx, chain_id, node_id, migrate): - """Remove a chain from a specific node""" - try: - config = load_multichain_config() - chain_manager = ChainManager(config) - - is_success = chain_manager.remove_chain_from_node(chain_id, node_id, migrate) - - if is_success: - success(f"Chain {chain_id} removed from node {node_id} successfully!") - else: - error(f"Failed to remove chain {chain_id} from node {node_id}") - raise click.Abort() - - except Exception as e: - error(f"Error removing chain from node: {str(e)}") - raise click.Abort() - -@chain.command() -@click.argument('chain_id') -@click.argument('from_node') -@click.argument('to_node') -@click.option('--dry-run', is_flag=True, help='Show migration plan without executing') -@click.option('--verify', is_flag=True, help='Verify migration after completion') -@click.pass_context -def migrate(ctx, chain_id, from_node, to_node, dry_run, verify): - """Migrate a chain between nodes""" - try: - config = load_multichain_config() - chain_manager = ChainManager(config) - - migration_result = chain_manager.migrate_chain(chain_id, from_node, to_node, dry_run) - - if dry_run: - plan_info = { - "Chain ID": chain_id, - "Source Node": from_node, - "Target Node": to_node, - "Feasible": "Yes" if migration_result.success else "No", - "Estimated Time": f"{migration_result.transfer_time_seconds}s", - "Error": migration_result.error or "None" - } - - output(plan_info, ctx.obj.get('output_format', 'table'), title="Migration Plan") - return - - if migration_result.success: - success(f"Chain migration completed successfully!") - result = { - "Chain ID": chain_id, - "Source Node": from_node, - "Target Node": to_node, - "Blocks Transferred": migration_result.blocks_transferred, - "Transfer Time": f"{migration_result.transfer_time_seconds}s", - "Verification": "Passed" if migration_result.verification_passed else "Failed" - } - - output(result, ctx.obj.get('output_format', 'table')) - else: - error(f"Migration failed: {migration_result.error}") - raise click.Abort() - - except Exception as e: - error(f"Error during migration: {str(e)}") - raise click.Abort() - -@chain.command() -@click.argument('chain_id') -@click.option('--path', help='Backup directory path') -@click.option('--compress', is_flag=True, help='Compress backup') -@click.option('--verify', is_flag=True, help='Verify backup integrity') -@click.pass_context -def backup(ctx, chain_id, path, compress, verify): - """Backup chain data""" - try: - config = load_multichain_config() - chain_manager = ChainManager(config) - - import asyncio - backup_result = asyncio.run(chain_manager.backup_chain(chain_id, path, compress, verify)) - - success(f"Chain backup completed successfully!") - result = { - "Chain ID": chain_id, - "Backup File": backup_result.backup_file, - "Original Size": f"{backup_result.original_size_mb:.1f}MB", - "Backup Size": f"{backup_result.backup_size_mb:.1f}MB", - "Compression": f"{backup_result.compression_ratio:.1f}x" if compress else "None", - "Checksum": backup_result.checksum, - "Verification": "Passed" if backup_result.verification_passed else "Failed" - } - - output(result, ctx.obj.get('output_format', 'table')) - - except Exception as e: - error(f"Error during backup: {str(e)}") - raise click.Abort() - -@chain.command() -@click.argument('backup_file', type=click.Path(exists=True)) -@click.option('--node', help='Target node for restoration') -@click.option('--verify', is_flag=True, help='Verify restoration') -@click.pass_context -def restore(ctx, backup_file, node, verify): - """Restore chain from backup""" - try: - config = load_multichain_config() - chain_manager = ChainManager(config) - - import asyncio - restore_result = asyncio.run(chain_manager.restore_chain(backup_file, node, verify)) - - success(f"Chain restoration completed successfully!") - result = { - "Chain ID": restore_result.chain_id, - "Node": restore_result.node_id, - "Blocks Restored": restore_result.blocks_restored, - "Verification": "Passed" if restore_result.verification_passed else "Failed" - } - - output(result, ctx.obj.get('output_format', 'table')) - - except Exception as e: - error(f"Error during restoration: {str(e)}") - raise click.Abort() - -@chain.command() -@click.argument('chain_id') -@click.option('--realtime', is_flag=True, help='Real-time monitoring') -@click.option('--export', help='Export monitoring data to file') -@click.option('--interval', default=5, help='Update interval in seconds') -@click.pass_context -def monitor(ctx, chain_id, realtime, export, interval): - """Monitor chain activity""" - try: - config = load_multichain_config() - chain_manager = ChainManager(config) - - if realtime: - # Real-time monitoring (placeholder implementation) - from rich.console import Console - from rich.layout import Layout - from rich.live import Live - import time - - console = Console() - - def generate_monitor_layout(): - try: - import asyncio - chain_info = asyncio.run(chain_manager.get_chain_info(chain_id, detailed=True, metrics=True)) - - layout = Layout() - layout.split_column( - Layout(name="header", size=3), - Layout(name="stats"), - Layout(name="activity", size=10) - ) - - # Header - layout["header"].update( - f"Chain Monitor: {chain_id} - {chain_info.status.value.upper()}" - ) - - # Stats table - stats_data = [ - ["Block Height", str(chain_info.block_height)], - ["TPS", f"{chain_info.tps:.1f}"], - ["Active Nodes", str(chain_info.active_nodes)], - ["Gas Price", f"{chain_info.gas_price / 1e9:.1f} gwei"], - ["Memory Usage", f"{chain_info.memory_usage_mb:.1f}MB"], - ["Disk Usage", f"{chain_info.disk_usage_mb:.1f}MB"] - ] - - layout["stats"].update(str(stats_data)) - - # Recent activity (placeholder) - layout["activity"].update("Recent activity would be displayed here") - - return layout - except Exception as e: - return f"Error getting chain info: {e}" - - with Live(generate_monitor_layout(), refresh_per_second=1) as live: - try: - while True: - live.update(generate_monitor_layout()) - time.sleep(interval) - except KeyboardInterrupt: - console.click.echo("\n[yellow]Monitoring stopped by user[/yellow]") - else: - # Single snapshot - import asyncio - chain_info = asyncio.run(chain_manager.get_chain_info(chain_id, detailed=True, metrics=True)) - - stats_data = [ - { - "Metric": "Block Height", - "Value": str(chain_info.block_height) - }, - { - "Metric": "TPS", - "Value": f"{chain_info.tps:.1f}" - }, - { - "Metric": "Active Nodes", - "Value": str(chain_info.active_nodes) - }, - { - "Metric": "Gas Price", - "Value": f"{chain_info.gas_price / 1e9:.1f} gwei" - }, - { - "Metric": "Memory Usage", - "Value": f"{chain_info.memory_usage_mb:.1f}MB" - }, - { - "Metric": "Disk Usage", - "Value": f"{chain_info.disk_usage_mb:.1f}MB" - } - ] - - output(stats_data, ctx.obj.get('output_format', 'table'), title=f"Chain Statistics: {chain_id}") - - if export: - import json - with open(export, 'w') as f: - json.dump(chain_info.dict(), f, indent=2, default=str) - success(f"Statistics exported to {export}") - - except ChainNotFoundError: - error(f"Chain {chain_id} not found") - raise click.Abort() - except Exception as e: - error(f"Error during monitoring: {str(e)}") - raise click.Abort() diff --git a/cli/src/aitbc_cli/commands/config.py b/cli/src/aitbc_cli/commands/config.py deleted file mode 100644 index 7d66688d..00000000 --- a/cli/src/aitbc_cli/commands/config.py +++ /dev/null @@ -1,473 +0,0 @@ -"""Configuration commands for AITBC CLI""" - -import click -import os -import shlex -import subprocess -import yaml -import json -from pathlib import Path -from typing import Optional, Dict, Any -from ..config import get_config, Config -from ..utils import output, error, success - - -@click.group() -def config(): - """Manage CLI configuration""" - pass - - -@config.command() -@click.pass_context -def show(ctx): - """Show current configuration""" - config = ctx.obj['config'] - - config_dict = { - "coordinator_url": config.coordinator_url, - "api_key": "***REDACTED***" if config.api_key else None, - "timeout": getattr(config, 'timeout', 30), - "config_file": getattr(config, 'config_file', None) - } - - output(config_dict, ctx.obj['output_format']) - - -@config.command() -@click.argument("key") -@click.argument("value") -@click.option("--global", "global_config", is_flag=True, help="Set global config") -@click.pass_context -def set(ctx, key: str, value: str, global_config: bool): - """Set configuration value""" - config = ctx.obj['config'] - - # Determine config file path - if global_config: - config_dir = Path.home() / ".config" / "aitbc" - config_dir.mkdir(parents=True, exist_ok=True) - config_file = config_dir / "config.yaml" - else: - config_file = Path.cwd() / ".aitbc.yaml" - - # Load existing config - if config_file.exists(): - with open(config_file) as f: - config_data = yaml.safe_load(f) or {} - else: - config_data = {} - - # Set the value - if key == "api_key": - config_data["api_key"] = value - if ctx.obj['output_format'] == 'table': - success("API key set (use --global to set permanently)") - elif key == "coordinator_url": - config_data["coordinator_url"] = value - if ctx.obj['output_format'] == 'table': - success(f"Coordinator URL set to: {value}") - elif key == "timeout": - try: - config_data["timeout"] = int(value) - if ctx.obj['output_format'] == 'table': - success(f"Timeout set to: {value}s") - except ValueError: - error("Timeout must be an integer") - ctx.exit(1) - else: - error(f"Unknown configuration key: {key}") - ctx.exit(1) - - # Save config - with open(config_file, 'w') as f: - yaml.dump(config_data, f, default_flow_style=False) - - output({ - "config_file": str(config_file), - "key": key, - "value": value - }, ctx.obj['output_format']) - - -@config.command() -@click.option("--global", "global_config", is_flag=True, help="Show global config") -def path(global_config: bool): - """Show configuration file path""" - if global_config: - config_dir = Path.home() / ".config" / "aitbc" - config_file = config_dir / "config.yaml" - else: - config_file = Path.cwd() / ".aitbc.yaml" - - output({ - "config_file": str(config_file), - "exists": config_file.exists() - }) - - -@config.command() -@click.option("--global", "global_config", is_flag=True, help="Edit global config") -@click.pass_context -def edit(ctx, global_config: bool): - """Open configuration file in editor""" - # Determine config file path - if global_config: - config_dir = Path.home() / ".config" / "aitbc" - config_dir.mkdir(parents=True, exist_ok=True) - config_file = config_dir / "config.yaml" - else: - config_file = Path.cwd() / ".aitbc.yaml" - - # Create if doesn't exist - if not config_file.exists(): - config = ctx.obj['config'] - config_data = { - "coordinator_url": config.coordinator_url, - "timeout": getattr(config, 'timeout', 30) - } - with open(config_file, 'w') as f: - yaml.dump(config_data, f, default_flow_style=False) - - # Open in editor - editor = os.getenv('EDITOR', 'nano').strip() or 'nano' - editor_cmd = shlex.split(editor) - subprocess.run([*editor_cmd, str(config_file)], check=False) - - -@config.command() -@click.option("--global", "global_config", is_flag=True, help="Reset global config") -@click.pass_context -def reset(ctx, global_config: bool): - """Reset configuration to defaults""" - # Determine config file path - if global_config: - config_dir = Path.home() / ".config" / "aitbc" - config_file = config_dir / "config.yaml" - else: - config_file = Path.cwd() / ".aitbc.yaml" - - if not config_file.exists(): - output({"message": "No configuration file found"}) - return - - if not click.confirm(f"Reset configuration at {config_file}?"): - return - - # Remove config file - config_file.unlink() - success("Configuration reset to defaults") - - -@config.command() -@click.option("--format", "output_format", type=click.Choice(['yaml', 'json']), default='yaml', help="Output format") -@click.option("--global", "global_config", is_flag=True, help="Export global config") -@click.pass_context -def export(ctx, output_format: str, global_config: bool): - """Export configuration""" - # Determine config file path - if global_config: - config_dir = Path.home() / ".config" / "aitbc" - config_file = config_dir / "config.yaml" - else: - config_file = Path.cwd() / ".aitbc.yaml" - - if not config_file.exists(): - error("No configuration file found") - ctx.exit(1) - - with open(config_file) as f: - config_data = yaml.safe_load(f) or {} - - # Redact sensitive data - if 'api_key' in config_data: - config_data['api_key'] = "***REDACTED***" - - if output_format == 'json': - click.echo(json.dumps(config_data, indent=2)) - else: - click.echo(yaml.dump(config_data, default_flow_style=False)) - - -@config.command() -@click.argument("file_path") -@click.option("--merge", is_flag=True, help="Merge with existing config") -@click.option("--global", "global_config", is_flag=True, help="Import to global config") -@click.pass_context -def import_config(ctx, file_path: str, merge: bool, global_config: bool): - """Import configuration from file""" - import_file = Path(file_path) - - if not import_file.exists(): - error(f"File not found: {file_path}") - ctx.exit(1) - - # Load import file - try: - with open(import_file) as f: - if import_file.suffix.lower() == '.json': - import_data = json.load(f) - else: - import_data = yaml.safe_load(f) - except json.JSONDecodeError: - error("Invalid JSON data") - ctx.exit(1) - except Exception as e: - error(f"Failed to parse file: {e}") - ctx.exit(1) - - # Determine target config file - if global_config: - config_dir = Path.home() / ".config" / "aitbc" - config_dir.mkdir(parents=True, exist_ok=True) - config_file = config_dir / "config.yaml" - else: - config_file = Path.cwd() / ".aitbc.yaml" - - # Load existing config if merging - if merge and config_file.exists(): - with open(config_file) as f: - config_data = yaml.safe_load(f) or {} - config_data.update(import_data) - else: - config_data = import_data - - # Save config - with open(config_file, 'w') as f: - yaml.dump(config_data, f, default_flow_style=False) - - if ctx.obj['output_format'] == 'table': - success(f"Configuration imported to {config_file}") - - -@config.command() -@click.pass_context -def validate(ctx): - """Validate configuration""" - config = ctx.obj['config'] - - errors = [] - warnings = [] - - # Validate coordinator URL - if not config.coordinator_url: - errors.append("Coordinator URL is not set") - elif not config.coordinator_url.startswith(('http://', 'https://')): - errors.append("Coordinator URL must start with http:// or https://") - - # Validate API key - if not config.api_key: - warnings.append("API key is not set") - elif len(config.api_key) < 10: - errors.append("API key appears to be too short") - - # Validate timeout - timeout = getattr(config, 'timeout', 30) - if not isinstance(timeout, (int, float)) or timeout <= 0: - errors.append("Timeout must be a positive number") - - # Output results - result = { - "valid": len(errors) == 0, - "errors": errors, - "warnings": warnings - } - - if errors: - error("Configuration validation failed") - ctx.exit(1) - elif warnings: - if ctx.obj['output_format'] == 'table': - success("Configuration valid with warnings") - else: - if ctx.obj['output_format'] == 'table': - success("Configuration is valid") - - output(result, ctx.obj['output_format']) - - -@config.command() -def environments(): - """List available environments""" - env_vars = [ - 'AITBC_COORDINATOR_URL', - 'AITBC_API_KEY', - 'AITBC_TIMEOUT', - 'AITBC_CONFIG_FILE', - 'CLIENT_API_KEY', - 'MINER_API_KEY', - 'ADMIN_API_KEY' - ] - - env_data = {} - for var in env_vars: - value = os.getenv(var) - if value: - if 'API_KEY' in var: - value = "***REDACTED***" - env_data[var] = value - - output({ - "environment_variables": env_data, - "note": "Use export VAR=value to set environment variables" - }) - - -@config.group() -def profiles(): - """Manage configuration profiles""" - pass - - -@profiles.command() -@click.argument("name") -@click.pass_context -def save(ctx, name: str): - """Save current configuration as a profile""" - config = ctx.obj['config'] - - # Create profiles directory - profiles_dir = Path.home() / ".config" / "aitbc" / "profiles" - profiles_dir.mkdir(parents=True, exist_ok=True) - - profile_file = profiles_dir / f"{name}.yaml" - - # Save profile (without API key) - profile_data = { - "coordinator_url": config.coordinator_url, - "timeout": getattr(config, 'timeout', 30) - } - - with open(profile_file, 'w') as f: - yaml.dump(profile_data, f, default_flow_style=False) - - if ctx.obj['output_format'] == 'table': - success(f"Profile '{name}' saved") - - -@profiles.command() -def list(): - """List available profiles""" - profiles_dir = Path.home() / ".config" / "aitbc" / "profiles" - - if not profiles_dir.exists(): - output({"profiles": []}) - return - - profiles = [] - for profile_file in profiles_dir.glob("*.yaml"): - with open(profile_file) as f: - profile_data = yaml.safe_load(f) - - profiles.append({ - "name": profile_file.stem, - "coordinator_url": profile_data.get("coordinator_url"), - "timeout": profile_data.get("timeout", 30) - }) - - output({"profiles": profiles}) - - -@profiles.command() -@click.argument("name") -@click.pass_context -def load(ctx, name: str): - """Load a configuration profile""" - profiles_dir = Path.home() / ".config" / "aitbc" / "profiles" - profile_file = profiles_dir / f"{name}.yaml" - - if not profile_file.exists(): - error(f"Profile '{name}' not found") - ctx.exit(1) - - with open(profile_file) as f: - profile_data = yaml.safe_load(f) - - # Load to current config - config_file = Path.cwd() / ".aitbc.yaml" - - with open(config_file, 'w') as f: - yaml.dump(profile_data, f, default_flow_style=False) - - if ctx.obj['output_format'] == 'table': - success(f"Profile '{name}' loaded") - - -@profiles.command() -@click.argument("name") -@click.pass_context -def delete(ctx, name: str): - """Delete a configuration profile""" - profiles_dir = Path.home() / ".config" / "aitbc" / "profiles" - profile_file = profiles_dir / f"{name}.yaml" - - if not profile_file.exists(): - error(f"Profile '{name}' not found") - ctx.exit(1) - - if not click.confirm(f"Delete profile '{name}'?"): - return - - profile_file.unlink() - if ctx.obj['output_format'] == 'table': - success(f"Profile '{name}' deleted") - - -@config.command(name="set-secret") -@click.argument("key") -@click.argument("value") -@click.pass_context -def set_secret(ctx, key: str, value: str): - """Set an encrypted configuration value""" - from ..utils import encrypt_value - - config_dir = Path.home() / ".config" / "aitbc" - config_dir.mkdir(parents=True, exist_ok=True) - secrets_file = config_dir / "secrets.json" - - secrets = {} - if secrets_file.exists(): - with open(secrets_file) as f: - secrets = json.load(f) - - secrets[key] = encrypt_value(value) - - with open(secrets_file, "w") as f: - json.dump(secrets, f, indent=2) - - # Restrict file permissions - secrets_file.chmod(0o600) - - if ctx.obj['output_format'] == 'table': - success(f"Secret '{key}' saved (encrypted)") - output({"key": key, "status": "encrypted"}, ctx.obj['output_format']) - - -@config.command(name="get-secret") -@click.argument("key") -@click.pass_context -def get_secret(ctx, key: str): - """Get a decrypted configuration value""" - from ..utils import decrypt_value - - secrets_file = Path.home() / ".config" / "aitbc" / "secrets.json" - - if not secrets_file.exists(): - error("No secrets file found") - ctx.exit(1) - return - - with open(secrets_file) as f: - secrets = json.load(f) - - if key not in secrets: - error(f"Secret '{key}' not found") - ctx.exit(1) - return - - decrypted = decrypt_value(secrets[key]) - output({"key": key, "value": decrypted}, ctx.obj['output_format']) - - -# Add profiles group to config -config.add_command(profiles) diff --git a/cli/src/aitbc_cli/commands/cross_chain.py b/cli/src/aitbc_cli/commands/cross_chain.py deleted file mode 100755 index 805aa911..00000000 --- a/cli/src/aitbc_cli/commands/cross_chain.py +++ /dev/null @@ -1,434 +0,0 @@ -"""Cross-chain trading commands for AITBC CLI""" - -import click -import json -from typing import Optional -from tabulate import tabulate -from ..config import get_config -from ..utils import success, error, output - -# Import shared modules -from aitbc import get_logger, AITBCHTTPClient, NetworkError - -# Initialize logger -logger = get_logger(__name__) - - -@click.group() -def cross_chain(): - """Cross-chain trading operations""" - pass - - -@cross_chain.command() -@click.option("--from-chain", help="Source chain ID") -@click.option("--to-chain", help="Target chain ID") -@click.option("--from-token", help="Source token symbol") -@click.option("--to-token", help="Target token symbol") -@click.pass_context -def rates(ctx, from_chain: Optional[str], to_chain: Optional[str], - from_token: Optional[str], to_token: Optional[str]): - """Get cross-chain exchange rates""" - config = ctx.obj['config'] - - try: - with AITBCHTTPClient() as client: - # Get rates from cross-chain exchange - response = client.get( - f"{config.exchange_service_url}/cross-chain/rates", - timeout=10 - ) - - if response.status_code == 200: - rates_data = response.json() - rates = rates_data.get('rates', {}) - - if from_chain and to_chain: - # Get specific rate - pair_key = f"{from_chain}-{to_chain}" - if pair_key in rates: - success(f"Exchange rate {from_chain} → {to_chain}: {rates[pair_key]}") - else: - error(f"No rate available for {from_chain} → {to_chain}") - else: - # Show all rates - success("Cross-chain exchange rates:") - rate_table = [] - for pair, rate in rates.items(): - chains = pair.split('-') - rate_table.append([chains[0], chains[1], f"{rate:.6f}"]) - - if rate_table: - headers = ["From Chain", "To Chain", "Rate"] - click.echo(tabulate(rate_table, headers=headers, tablefmt="grid")) - else: - output("No cross-chain rates available") - else: - error(f"Failed to get cross-chain rates: {response.status_code}") - except Exception as e: - error(f"Network error: {e}") - - -@cross_chain.command() -@click.option("--from-chain", required=True, help="Source chain ID") -@click.option("--to-chain", required=True, help="Target chain ID") -@click.option("--from-token", required=True, help="Source token symbol") -@click.option("--to-token", required=True, help="Target token symbol") -@click.option("--amount", type=float, required=True, help="Amount to swap") -@click.option("--min-amount", type=float, help="Minimum amount to receive") -@click.option("--slippage", type=float, default=0.01, help="Slippage tolerance (0-0.1)") -@click.option("--address", help="User wallet address") -@click.pass_context -def swap(ctx, from_chain: str, to_chain: str, from_token: str, to_token: str, - amount: float, min_amount: Optional[float], slippage: float, address: Optional[str]): - """Create cross-chain swap""" - config = ctx.obj['config'] - - # Validate inputs - if from_chain == to_chain: - error("Source and target chains must be different") - return - - if amount <= 0: - error("Amount must be greater than 0") - return - - # Use default address if not provided - if not address: - address = config.get('default_address', '0x1234567890123456789012345678901234567890') - - # Calculate minimum amount if not provided - if not min_amount: - # Get rate first - try: - with AITBCHTTPClient() as client: - response = client.get( - f"{config.exchange_service_url}/cross-chain/rates", - timeout=10 - ) - if response.status_code == 200: - rates_data = response.json() - pair_key = f"{from_chain}-{to_chain}" - rate = rates_data.get('rates', {}).get(pair_key, 1.0) - min_amount = amount * rate * (1 - slippage) * 0.97 # Account for fees - else: - min_amount = amount * 0.95 # Conservative fallback - except (requests.RequestException, KeyError, ValueError): - min_amount = amount * 0.95 - - swap_data = { - "from_chain": from_chain, - "to_chain": to_chain, - "from_token": from_token, - "to_token": to_token, - "amount": amount, - "min_amount": min_amount, - "user_address": address, - "slippage_tolerance": slippage - } - - try: - http_client = AITBCHTTPClient(base_url=config.exchange_service_url, timeout=30) - swap_result = http_client.post("/swap", json=swap_data) - success("Cross-chain swap created successfully!") - output({ - "Swap ID": swap_result.get('swap_id'), - "From Chain": swap_result.get('from_chain'), - "To Chain": swap_result.get('to_chain'), - "Amount": swap_result.get('amount'), - "Expected Amount": swap_result.get('expected_amount'), - "Rate": swap_result.get('rate'), - "Total Fees": swap_result.get('total_fees'), - "Status": swap_result.get('status') - }, ctx.obj['output_format']) - - # Show swap ID for tracking - success(f"Track swap with: aitbc cross-chain status {swap_result.get('swap_id')}") - except Exception as e: - error(f"Network error: {e}") - - -@cross_chain.command() -@click.argument("swap_id") -@click.pass_context -def status(ctx, swap_id: str): - """Check cross-chain swap status""" - try: - http_client = AITBCHTTPClient(base_url=config.exchange_service_url, timeout=10) - swap_data = http_client.get(f"/cross-chain/swap/{swap_id}") - success(f"Swap Status: {swap_data.get('status', 'unknown')}") - - # Display swap details - details = { - "Swap ID": swap_data.get('swap_id'), - "From Chain": swap_data.get('from_chain'), - "To Chain": swap_data.get('to_chain'), - "From Token": swap_data.get('from_token'), - "To Token": swap_data.get('to_token'), - "Amount": swap_data.get('amount'), - "Expected Amount": swap_data.get('expected_amount'), - "Actual Amount": swap_data.get('actual_amount'), - "Status": swap_data.get('status'), - "Created At": swap_data.get('created_at'), - "Completed At": swap_data.get('completed_at'), - "Bridge Fee": swap_data.get('bridge_fee'), - "From Tx Hash": swap_data.get('from_tx_hash'), - "To Tx Hash": swap_data.get('to_tx_hash') - } - - output(details, ctx.obj['output_format']) - - # Show additional status info - if swap_data.get('status') == 'completed': - success("āœ… Swap completed successfully!") - elif swap_data.get('status') == 'failed': - error("āŒ Swap failed") - if swap_data.get('error_message'): - error(f"Error: {swap_data['error_message']}") - elif swap_data.get('status') == 'pending': - success("ā³ Swap is pending...") - elif swap_data.get('status') == 'executing': - success("šŸ”„ Swap is executing...") - elif swap_data.get('status') == 'refunded': - success("šŸ’° Swap was refunded") - except Exception as e: - error(f"Network error: {e}") - - -@cross_chain.command() -@click.option("--user-address", help="Filter by user address") -@click.option("--status", help="Filter by status") -@click.option("--limit", type=int, default=10, help="Number of swaps to show") -@click.pass_context -def swaps(ctx, user_address: Optional[str], status: Optional[str], limit: int): - """List cross-chain swaps""" - params = {} - if user_address: - params['user_address'] = user_address - if status: - params['status'] = status - - try: - http_client = AITBCHTTPClient(base_url=config.exchange_service_url, timeout=10) - swaps_data = http_client.get("/cross-chain/swaps", params=params) - swaps = swaps_data.get('swaps', []) - - if swaps: - success(f"Found {len(swaps)} cross-chain swaps:") - - # Create table - swap_table = [] - for swap in swaps[:limit]: - swap_table.append([ - swap.get('swap_id', '')[:8] + '...', - swap.get('from_chain', ''), - swap.get('to_chain', ''), - swap.get('amount', 0), - swap.get('status', ''), - swap.get('created_at', '')[:19] - ]) - - table(["ID", "From", "To", "Amount", "Status", "Created"], swap_table) - - if len(swaps) > limit: - success(f"Showing {limit} of {len(swaps)} total swaps") - else: - success("No cross-chain swaps found") - except Exception as e: - error(f"Network error: {e}") - - -@cross_chain.command() -@click.option("--source-chain", required=True, help="Source chain ID") -@click.option("--target-chain", required=True, help="Target chain ID") -@click.option("--token", required=True, help="Token to bridge") -@click.option("--amount", type=float, required=True, help="Amount to bridge") -@click.option("--recipient", help="Recipient address") -@click.pass_context -def bridge(ctx, source_chain: str, target_chain: str, token: str, - amount: float, recipient: Optional[str]): - """Create cross-chain bridge transaction""" - config = ctx.obj['config'] - - # Validate inputs - if source_chain == target_chain: - error("Source and target chains must be different") - return - - if amount <= 0: - error("Amount must be greater than 0") - return - - # Use default recipient if not provided - if not recipient: - recipient = config.get('default_address', '0x1234567890123456789012345678901234567890') - - bridge_data = { - "source_chain": source_chain, - "target_chain": target_chain, - "token": token, - "amount": amount, - "recipient_address": recipient - } - - try: - http_client = AITBCHTTPClient(base_url=config.exchange_service_url, timeout=30) - bridge_result = http_client.post("/cross-chain/bridge", json=bridge_data) - success("Cross-chain bridge created successfully!") - output({ - "Bridge ID": bridge_result.get('bridge_id'), - "Source Chain": bridge_result.get('source_chain'), - "Target Chain": bridge_result.get('target_chain'), - "Token": bridge_result.get('token'), - "Amount": bridge_result.get('amount'), - "Bridge Fee": bridge_result.get('bridge_fee'), - "Status": bridge_result.get('status') - }, ctx.obj['output_format']) - - # Show bridge ID for tracking - success(f"Track bridge with: aitbc cross-chain bridge-status {bridge_result.get('bridge_id')}") - except Exception as e: - error(f"Network error: {e}") - - -@cross_chain.command() -@click.argument("bridge_id") -@click.pass_context -def bridge_status(ctx, bridge_id: str): - """Check cross-chain bridge status""" - try: - http_client = AITBCHTTPClient(base_url=config.exchange_service_url, timeout=10) - bridge_data = http_client.get(f"/cross-chain/bridge/{bridge_id}") - success(f"Bridge Status: {bridge_data.get('status', 'unknown')}") - - # Display bridge details - details = { - "Bridge ID": bridge_data.get('bridge_id'), - "Source Chain": bridge_data.get('source_chain'), - "Target Chain": bridge_data.get('target_chain'), - "Token": bridge_data.get('token'), - "Amount": bridge_data.get('amount'), - "Recipient Address": bridge_data.get('recipient_address'), - "Status": bridge_data.get('status'), - "Created At": bridge_data.get('created_at'), - "Completed At": bridge_data.get('completed_at'), - "Bridge Fee": bridge_data.get('bridge_fee'), - "Source Tx Hash": bridge_data.get('source_tx_hash'), - "Target Tx Hash": bridge_data.get('target_tx_hash') - } - - output(details, ctx.obj['output_format']) - - # Show additional status info - if bridge_data.get('status') == 'completed': - success("āœ… Bridge completed successfully!") - elif bridge_data.get('status') == 'failed': - error("āŒ Bridge failed") - if bridge_data.get('error_message'): - error(f"Error: {bridge_data['error_message']}") - elif bridge_data.get('status') == 'pending': - success("ā³ Bridge is pending...") - elif bridge_data.get('status') == 'locked': - success("šŸ”’ Bridge is locked...") - elif bridge_data.get('status') == 'transferred': - success("šŸ”„ Bridge is transferring...") - except Exception as e: - error(f"Network error: {e}") - - -@cross_chain.command() -@click.pass_context -def pools(ctx): - """Show cross-chain liquidity pools""" - try: - http_client = AITBCHTTPClient(base_url=config.exchange_service_url, timeout=10) - response = http_client.get( - f"/cross-chain/pools", - timeout=10 - ) - - if response.status_code == 200: - pools_data = response.json() - pools = pools_data.get('pools', []) - - if pools: - success(f"Found {len(pools)} cross-chain liquidity pools:") - - # Create table - pool_table = [] - for pool in pools: - pool_table.append([ - pool.get('pool_id', ''), - pool.get('token_a', ''), - pool.get('token_b', ''), - pool.get('chain_a', ''), - pool.get('chain_b', ''), - f"{pool.get('reserve_a', 0):.2f}", - f"{pool.get('reserve_b', 0):.2f}", - f"{pool.get('total_liquidity', 0):.2f}", - f"{pool.get('apr', 0):.2%}" - ]) - - table(["Pool ID", "Token A", "Token B", "Chain A", "Chain B", - "Reserve A", "Reserve B", "Liquidity", "APR"], pool_table) - else: - success("No cross-chain liquidity pools found") - else: - error(f"Failed to get pools: {response.status_code}") - except Exception as e: - error(f"Network error: {e}") - - -@cross_chain.command() -@click.pass_context -def stats(ctx): - """Show cross-chain trading statistics""" - try: - http_client = AITBCHTTPClient(base_url=config.exchange_service_url, timeout=10) - response = http_client.get( - f"/cross-chain/stats", - timeout=10 - ) - - if response.status_code == 200: - stats_data = response.json() - - success("Cross-Chain Trading Statistics:") - - # Show swap stats - swap_stats = stats_data.get('swap_stats', []) - if swap_stats: - success("Swap Statistics:") - swap_table = [] - for stat in swap_stats: - swap_table.append([ - stat.get('status', ''), - stat.get('count', 0), - f"{stat.get('volume', 0):.2f}" - ]) - table(["Status", "Count", "Volume"], swap_table) - - # Show bridge stats - bridge_stats = stats_data.get('bridge_stats', []) - if bridge_stats: - success("Bridge Statistics:") - bridge_table = [] - for stat in bridge_stats: - bridge_table.append([ - stat.get('status', ''), - stat.get('count', 0), - f"{stat.get('volume', 0):.2f}" - ]) - table(["Status", "Count", "Volume"], bridge_table) - - # Show overall stats - success("Overall Statistics:") - output({ - "Total Volume": f"{stats_data.get('total_volume', 0):.2f}", - "Supported Chains": ", ".join(stats_data.get('supported_chains', [])), - "Last Updated": stats_data.get('timestamp', '') - }, ctx.obj['output_format']) - else: - error(f"Failed to get stats: {response.status_code}") - except Exception as e: - error(f"Network error: {e}") diff --git a/cli/src/aitbc_cli/commands/deployment.py b/cli/src/aitbc_cli/commands/deployment.py deleted file mode 100755 index 2dde8399..00000000 --- a/cli/src/aitbc_cli/commands/deployment.py +++ /dev/null @@ -1,377 +0,0 @@ -"""Production deployment and scaling commands for AITBC CLI""" - -import click -import asyncio -import json -from datetime import datetime -from typing import Optional -from ..core.deployment import ( - ProductionDeployment, ScalingPolicy, DeploymentStatus -) -from ..utils import output, error, success - -@click.group() -def deploy(): - """Production deployment and scaling commands""" - pass - -@deploy.command() -@click.argument('name') -@click.argument('environment') -@click.argument('region') -@click.argument('instance_type') -@click.argument('min_instances', type=int) -@click.argument('max_instances', type=int) -@click.argument('desired_instances', type=int) -@click.argument('port', type=int) -@click.argument('domain') -@click.option('--db-host', default='localhost', help='Database host') -@click.option('--db-port', default=5432, help='Database port') -@click.option('--db-name', default='aitbc', help='Database name') -@click.pass_context -def create(ctx, name, environment, region, instance_type, min_instances, max_instances, desired_instances, port, domain, db_host, db_port, db_name): - """Create a new deployment configuration""" - try: - deployment = ProductionDeployment() - - # Database configuration - database_config = { - "host": db_host, - "port": db_port, - "name": db_name, - "ssl_enabled": True if environment == "production" else False - } - - # Create deployment - deployment_id = asyncio.run(deployment.create_deployment( - name=name, - environment=environment, - region=region, - instance_type=instance_type, - min_instances=min_instances, - max_instances=max_instances, - desired_instances=desired_instances, - port=port, - domain=domain, - database_config=database_config - )) - - if deployment_id: - success(f"Deployment configuration created! ID: {deployment_id}") - - deployment_data = { - "Deployment ID": deployment_id, - "Name": name, - "Environment": environment, - "Region": region, - "Instance Type": instance_type, - "Min Instances": min_instances, - "Max Instances": max_instances, - "Desired Instances": desired_instances, - "Port": port, - "Domain": domain, - "Status": "pending", - "Created": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - - output(deployment_data, ctx.obj.get('output_format', 'table')) - else: - error("Failed to create deployment configuration") - raise click.Abort() - - except Exception as e: - error(f"Error creating deployment: {str(e)}") - raise click.Abort() - -@deploy.command() -@click.argument('deployment_id') -@click.pass_context -def start(ctx, deployment_id): - """Deploy the application to production""" - try: - deployment = ProductionDeployment() - - # Deploy application - success_deploy = asyncio.run(deployment.deploy_application(deployment_id)) - - if success_deploy: - success(f"Deployment {deployment_id} started successfully!") - - deployment_data = { - "Deployment ID": deployment_id, - "Status": "running", - "Started": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - - output(deployment_data, ctx.obj.get('output_format', 'table')) - else: - error(f"Failed to start deployment {deployment_id}") - raise click.Abort() - - except Exception as e: - error(f"Error starting deployment: {str(e)}") - raise click.Abort() - -@deploy.command() -@click.argument('deployment_id') -@click.argument('target_instances', type=int) -@click.option('--reason', default='manual', help='Scaling reason') -@click.pass_context -def scale(ctx, deployment_id, target_instances, reason): - """Scale a deployment to target instance count""" - try: - deployment = ProductionDeployment() - - # Scale deployment - success_scale = asyncio.run(deployment.scale_deployment(deployment_id, target_instances, reason)) - - if success_scale: - success(f"Deployment {deployment_id} scaled to {target_instances} instances!") - - scaling_data = { - "Deployment ID": deployment_id, - "Target Instances": target_instances, - "Reason": reason, - "Status": "completed", - "Scaled": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - - output(scaling_data, ctx.obj.get('output_format', 'table')) - else: - error(f"Failed to scale deployment {deployment_id}") - raise click.Abort() - - except Exception as e: - error(f"Error scaling deployment: {str(e)}") - raise click.Abort() - -@deploy.command() -@click.argument('deployment_id') -@click.pass_context -def status(ctx, deployment_id): - """Get comprehensive deployment status""" - try: - deployment = ProductionDeployment() - - # Get deployment status - status_data = asyncio.run(deployment.get_deployment_status(deployment_id)) - - if not status_data: - error(f"Deployment {deployment_id} not found") - raise click.Abort() - - # Format deployment info - deployment_info = status_data["deployment"] - info_data = [ - {"Metric": "Deployment ID", "Value": deployment_info["deployment_id"]}, - {"Metric": "Name", "Value": deployment_info["name"]}, - {"Metric": "Environment", "Value": deployment_info["environment"]}, - {"Metric": "Region", "Value": deployment_info["region"]}, - {"Metric": "Instance Type", "Value": deployment_info["instance_type"]}, - {"Metric": "Min Instances", "Value": deployment_info["min_instances"]}, - {"Metric": "Max Instances", "Value": deployment_info["max_instances"]}, - {"Metric": "Desired Instances", "Value": deployment_info["desired_instances"]}, - {"Metric": "Port", "Value": deployment_info["port"]}, - {"Metric": "Domain", "Value": deployment_info["domain"]}, - {"Metric": "Health Status", "Value": "Healthy" if status_data["health_status"] else "Unhealthy"}, - {"Metric": "Uptime", "Value": f"{status_data['uptime_percentage']:.2f}%"} - ] - - output(info_data, ctx.obj.get('output_format', 'table'), title=f"Deployment Status: {deployment_id}") - - # Show metrics if available - if status_data["metrics"]: - metrics = status_data["metrics"] - metrics_data = [ - {"Metric": "CPU Usage", "Value": f"{metrics['cpu_usage']:.1f}%"}, - {"Metric": "Memory Usage", "Value": f"{metrics['memory_usage']:.1f}%"}, - {"Metric": "Disk Usage", "Value": f"{metrics['disk_usage']:.1f}%"}, - {"Metric": "Request Count", "Value": metrics['request_count']}, - {"Metric": "Error Rate", "Value": f"{metrics['error_rate']:.2f}%"}, - {"Metric": "Response Time", "Value": f"{metrics['response_time']:.1f}ms"}, - {"Metric": "Active Instances", "Value": metrics['active_instances']} - ] - - output(metrics_data, ctx.obj.get('output_format', 'table'), title="Performance Metrics") - - # Show recent scaling events - if status_data["recent_scaling_events"]: - events = status_data["recent_scaling_events"] - events_data = [ - { - "Event ID": event["event_id"][:8], - "Type": event["scaling_type"], - "From": event["old_instances"], - "To": event["new_instances"], - "Reason": event["trigger_reason"], - "Success": "Yes" if event["success"] else "No", - "Time": event["triggered_at"] - } - for event in events - ] - - output(events_data, ctx.obj.get('output_format', 'table'), title="Recent Scaling Events") - - except Exception as e: - error(f"Error getting deployment status: {str(e)}") - raise click.Abort() - -@deploy.command() -@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format') -@click.pass_context -def overview(ctx, format): - """Get overview of all deployments""" - try: - deployment = ProductionDeployment() - - # Get cluster overview - overview_data = asyncio.run(deployment.get_cluster_overview()) - - if not overview_data: - error("No deployment data available") - raise click.Abort() - - # Cluster metrics - cluster_data = [ - {"Metric": "Total Deployments", "Value": overview_data["total_deployments"]}, - {"Metric": "Running Deployments", "Value": overview_data["running_deployments"]}, - {"Metric": "Total Instances", "Value": overview_data["total_instances"]}, - {"Metric": "Health Check Coverage", "Value": f"{overview_data['health_check_coverage']:.1%}"}, - {"Metric": "Recent Scaling Events", "Value": overview_data["recent_scaling_events"]}, - {"Metric": "Scaling Success Rate", "Value": f"{overview_data['successful_scaling_rate']:.1%}"} - ] - - output(cluster_data, ctx.obj.get('output_format', format), title="Cluster Overview") - - # Aggregate metrics - if "aggregate_metrics" in overview_data: - metrics = overview_data["aggregate_metrics"] - metrics_data = [ - {"Metric": "Average CPU Usage", "Value": f"{metrics['total_cpu_usage']:.1f}%"}, - {"Metric": "Average Memory Usage", "Value": f"{metrics['total_memory_usage']:.1f}%"}, - {"Metric": "Average Disk Usage", "Value": f"{metrics['total_disk_usage']:.1f}%"}, - {"Metric": "Average Response Time", "Value": f"{metrics['average_response_time']:.1f}ms"}, - {"Metric": "Average Error Rate", "Value": f"{metrics['average_error_rate']:.2f}%"}, - {"Metric": "Average Uptime", "Value": f"{metrics['average_uptime']:.1f}%"} - ] - - output(metrics_data, ctx.obj.get('output_format', format), title="Aggregate Performance Metrics") - - except Exception as e: - error(f"Error getting cluster overview: {str(e)}") - raise click.Abort() - -@deploy.command() -@click.argument('deployment_id') -@click.option('--interval', default=60, help='Update interval in seconds') -@click.pass_context -def monitor(ctx, deployment_id, interval): - """Monitor deployment performance in real-time""" - try: - deployment = ProductionDeployment() - - # Real-time monitoring - from rich.console import Console - from rich.live import Live - from rich.table import Table - import time - - console = Console() - - def generate_monitor_table(): - try: - status_data = asyncio.run(deployment.get_deployment_status(deployment_id)) - - if not status_data: - return f"Deployment {deployment_id} not found" - - deployment_info = status_data["deployment"] - metrics = status_data.get("metrics") - - table = Table(title=f"Deployment Monitor - {deployment_info['name']} ({deployment_id[:8]}) - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") - table.add_column("Metric", style="cyan") - table.add_column("Value", style="green") - - table.add_row("Environment", deployment_info["environment"]) - table.add_row("Desired Instances", str(deployment_info["desired_instances"])) - table.add_row("Health Status", "āœ… Healthy" if status_data["health_status"] else "āŒ Unhealthy") - table.add_row("Uptime", f"{status_data['uptime_percentage']:.2f}%") - - if metrics: - table.add_row("CPU Usage", f"{metrics['cpu_usage']:.1f}%") - table.add_row("Memory Usage", f"{metrics['memory_usage']:.1f}%") - table.add_row("Disk Usage", f"{metrics['disk_usage']:.1f}%") - table.add_row("Request Count", str(metrics['request_count'])) - table.add_row("Error Rate", f"{metrics['error_rate']:.2f}%") - table.add_row("Response Time", f"{metrics['response_time']:.1f}ms") - table.add_row("Active Instances", str(metrics['active_instances'])) - - return table - except Exception as e: - return f"Error getting deployment data: {e}" - - with Live(generate_monitor_table(), refresh_per_second=1) as live: - try: - while True: - live.update(generate_monitor_table()) - time.sleep(interval) - except KeyboardInterrupt: - console.click.echo("\n[yellow]Monitoring stopped by user[/yellow]") - except Exception as e: - error(f"Error during monitoring: {str(e)}") - raise click.Abort() - -@deploy.command() -@click.argument('deployment_id') -@click.pass_context -def auto_scale(ctx, deployment_id): - """Trigger auto-scaling evaluation for a deployment""" - try: - deployment = ProductionDeployment() - - # Trigger auto-scaling - success_auto = asyncio.run(deployment.auto_scale_deployment(deployment_id)) - - if success_auto: - success(f"Auto-scaling evaluation completed for deployment {deployment_id}") - else: - error(f"Auto-scaling evaluation failed for deployment {deployment_id}") - raise click.Abort() - - except Exception as e: - error(f"Error in auto-scaling: {str(e)}") - raise click.Abort() - -@deploy.command() -@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format') -@click.pass_context -def list_deployments(ctx, format): - """List all deployments""" - try: - deployment = ProductionDeployment() - - # Get all deployment statuses - deployments = [] - for deployment_id in deployment.deployments.keys(): - status_data = asyncio.run(deployment.get_deployment_status(deployment_id)) - if status_data: - deployment_info = status_data["deployment"] - deployments.append({ - "Deployment ID": deployment_info["deployment_id"][:8], - "Name": deployment_info["name"], - "Environment": deployment_info["environment"], - "Instances": f"{deployment_info['desired_instances']}/{deployment_info['max_instances']}", - "Status": "Running" if status_data["health_status"] else "Stopped", - "Uptime": f"{status_data['uptime_percentage']:.1f}%", - "Created": deployment_info["created_at"] - }) - - if not deployments: - output("No deployments found", ctx.obj.get('output_format', 'table')) - return - - output(deployments, ctx.obj.get('output_format', format), title="All Deployments") - - except Exception as e: - error(f"Error listing deployments: {str(e)}") - raise click.Abort() diff --git a/cli/src/aitbc_cli/commands/edge.py b/cli/src/aitbc_cli/commands/edge.py deleted file mode 100644 index a8fe00c3..00000000 --- a/cli/src/aitbc_cli/commands/edge.py +++ /dev/null @@ -1,509 +0,0 @@ -""" -Edge API CLI Commands -Commands for interacting with the Edge API service -""" - -import click -import httpx -from typing import Optional -from ..utils import output, error, success, info, warning -from ..config import get_config - -# Initialize logger -logger = None - - -@click.group() -def edge(): - """Edge API commands for island, GPU, database, serve, and metrics operations""" - pass - - -def get_edge_client(): - """Get Edge API HTTP client""" - config = get_config() - base_url = f"http://{config.edge_api_host}:{config.edge_api_port}" - return httpx.Client(base_url=base_url, timeout=30.0) - - -@edge.group() -def island(): - """Island operations via Edge API""" - pass - - -@island.command() -@click.argument('island_id') -@click.argument('island_name') -@click.argument('chain_id') -@click.option('--role', default='compute-provider', help='Island role') -@click.option('--is-hub', is_flag=True, help='Mark as hub node') -def join(island_id: str, island_name: str, chain_id: str, role: str, is_hub: bool): - """Join an island""" - try: - client = get_edge_client() - response = client.post("/v1/islands/join", json={ - "island_id": island_id, - "island_name": island_name, - "chain_id": chain_id, - "role": role, - "is_hub": is_hub - }) - response.raise_for_status() - result = response.json() - - if result.get("success"): - success(f"Successfully joined island {island_id}") - output(result) - else: - error(f"Failed to join island: {result.get('message', 'Unknown error')}") - except Exception as e: - error(f"Error joining island: {str(e)}") - - -@island.command() -@click.argument('island_id') -def leave(island_id: str): - """Leave an island""" - try: - client = get_edge_client() - response = client.post("/v1/islands/leave", json={"island_id": island_id}) - response.raise_for_status() - result = response.json() - - if result.get("success"): - success(f"Successfully left island {island_id}") - output(result) - else: - error(f"Failed to leave island: {result.get('message', 'Unknown error')}") - except Exception as e: - error(f"Error leaving island: {str(e)}") - - -@island.command(name='list') -def list_islands(): - """List all islands""" - try: - client = get_edge_client() - response = client.get("/v1/islands/") - response.raise_for_status() - result = response.json() - - islands = result.get("islands", []) - if islands: - output(islands) - else: - info("No islands found") - except Exception as e: - error(f"Error listing islands: {str(e)}") - - -@island.command() -@click.argument('island_id') -def get(island_id: str): - """Get island details""" - try: - client = get_edge_client() - response = client.get(f"/v1/islands/{island_id}") - response.raise_for_status() - result = response.json() - output(result) - except Exception as e: - error(f"Error getting island details: {str(e)}") - - -@island.command() -@click.argument('target_island_id') -def bridge(target_island_id: str): - """Request bridge to another island""" - try: - client = get_edge_client() - response = client.post("/v1/islands/bridge", json={"target_island_id": target_island_id}) - response.raise_for_status() - result = response.json() - - if result.get("success"): - success(f"Bridge request submitted to {target_island_id}") - output(result) - else: - error(f"Failed to request bridge: {result.get('message', 'Unknown error')}") - except Exception as e: - error(f"Error requesting bridge: {str(e)}") - - -@edge.group() -def gpu(): - """GPU operations via Edge API""" - pass - - -@gpu.command() -@click.option('--architecture', help='Filter by GPU architecture') -@click.option('--edge-optimized', is_flag=True, help='Filter edge-optimized GPUs') -@click.option('--min-memory-gb', type=int, help='Minimum memory in GB') -def list_gpus(architecture: Optional[str], edge_optimized: bool, min_memory_gb: Optional[int]): - """List available GPUs""" - try: - client = get_edge_client() - params = {} - if architecture: - params["architecture"] = architecture - if edge_optimized: - params["edge_optimized"] = edge_optimized - if min_memory_gb: - params["min_memory_gb"] = min_memory_gb - - response = client.get("/v1/gpu/", params=params) - response.raise_for_status() - result = response.json() - - gpus = result.get("gpus", []) - if gpus: - output(gpus) - else: - info("No GPUs found") - except Exception as e: - error(f"Error listing GPUs: {str(e)}") - - -@gpu.command() -@click.argument('gpu_id') -def get_gpu(gpu_id: str): - """Get GPU details""" - try: - client = get_edge_client() - response = client.get(f"/v1/gpu/{gpu_id}") - response.raise_for_status() - result = response.json() - output(result) - except Exception as e: - error(f"Error getting GPU details: {str(e)}") - - -@gpu.command() -@click.argument('gpu_id') -def remove_gpu(gpu_id: str): - """Remove GPU from listing""" - try: - client = get_edge_client() - response = client.delete(f"/v1/gpu/{gpu_id}") - response.raise_for_status() - result = response.json() - success(result.get("message", f"GPU {gpu_id} removed")) - except Exception as e: - error(f"Error removing GPU: {str(e)}") - - -@gpu.command() -@click.argument('miner_id') -def scan_gpus(miner_id: str): - """Scan GPUs for a miner""" - try: - client = get_edge_client() - response = client.post("/v1/gpu/scan", json={"miner_id": miner_id}) - response.raise_for_status() - result = response.json() - success(f"GPU scan initiated for miner {miner_id}") - output(result) - except Exception as e: - error(f"Error scanning GPUs: {str(e)}") - - -@gpu.command() -@click.argument('gpu_id') -@click.option('--limit', type=int, default=100, help='Number of metrics to return') -def gpu_metrics(gpu_id: str, limit: int): - """Get GPU metrics""" - try: - client = get_edge_client() - response = client.get(f"/v1/gpu/{gpu_id}/metrics", params={"limit": limit}) - response.raise_for_status() - result = response.json() - output(result) - except Exception as e: - error(f"Error getting GPU metrics: {str(e)}") - - -@edge.group() -def database(): - """Database operations via Edge API""" - pass - - -@database.command() -@click.argument('database_id') -@click.argument('island_id') -@click.argument('capacity_gb', type=int) -def init_db(database_id: str, island_id: str, capacity_gb: int): - """Initialize edge database""" - try: - client = get_edge_client() - response = client.post("/v1/database/init", json={ - "database_id": database_id, - "island_id": island_id, - "capacity_gb": capacity_gb - }) - response.raise_for_status() - result = response.json() - - if result.get("success"): - success(f"Database {database_id} initialized") - output(result) - else: - error(f"Failed to initialize database: {result.get('message', 'Unknown error')}") - except Exception as e: - error(f"Error initializing database: {str(e)}") - - -@database.command() -@click.option('--island-id', help='Filter by island ID') -def list_dbs(island_id: Optional[str]): - """List edge databases""" - try: - client = get_edge_client() - params = {} - if island_id: - params["island_id"] = island_id - - response = client.get("/v1/database/", params=params) - response.raise_for_status() - result = response.json() - - databases = result.get("databases", []) - if databases: - output(databases) - else: - info("No databases found") - except Exception as e: - error(f"Error listing databases: {str(e)}") - - -@database.command() -@click.argument('database_id') -def get_db(database_id: str): - """Get database details""" - try: - client = get_edge_client() - response = client.get(f"/v1/database/{database_id}") - response.raise_for_status() - result = response.json() - output(result) - except Exception as e: - error(f"Error getting database details: {str(e)}") - - -@database.command() -@click.argument('database_id') -def delete_db(database_id: str): - """Delete database""" - try: - client = get_edge_client() - response = client.delete(f"/v1/database/{database_id}") - response.raise_for_status() - result = response.json() - success(result.get("message", f"Database {database_id} deleted")) - except Exception as e: - error(f"Error deleting database: {str(e)}") - - -@database.command() -@click.argument('database_id') -def sync_db(database_id: str): - """Sync database""" - try: - client = get_edge_client() - response = client.post(f"/v1/database/{database_id}/sync") - response.raise_for_status() - result = response.json() - - if result.get("success"): - success(f"Database {database_id} synced") - output(result) - else: - error(f"Failed to sync database: {result.get('message', 'Unknown error')}") - except Exception as e: - error(f"Error syncing database: {str(e)}") - - -@edge.group() -def serve(): - """Serve operations via Edge API""" - pass - - -@serve.command() -@click.argument('gpu_id') -@click.argument('model_name') -@click.argument('input_data') -@click.option('--priority', default='normal', help='Request priority') -def submit_request(gpu_id: str, model_name: str, input_data: str, priority: str): - """Submit compute request""" - try: - import json - client = get_edge_client() - response = client.post("/v1/serve/requests", json={ - "gpu_id": gpu_id, - "model_name": model_name, - "input_data": json.loads(input_data), - "priority": priority - }) - response.raise_for_status() - result = response.json() - - if result.get("success"): - success(f"Compute request {result.get('request_id')} submitted") - output(result) - else: - error(f"Failed to submit request: {result.get('message', 'Unknown error')}") - except Exception as e: - error(f"Error submitting compute request: {str(e)}") - - -@serve.command() -@click.option('--gpu-id', help='Filter by GPU ID') -@click.option('--status', help='Filter by status') -def list_requests(gpu_id: Optional[str], status: Optional[str]): - """List compute requests""" - try: - client = get_edge_client() - params = {} - if gpu_id: - params["gpu_id"] = gpu_id - if status: - params["status"] = status - - response = client.get("/v1/serve/requests", params=params) - response.raise_for_status() - result = response.json() - - requests = result.get("requests", []) - if requests: - output(requests) - else: - info("No requests found") - except Exception as e: - error(f"Error listing requests: {str(e)}") - - -@serve.command() -@click.argument('request_id') -def get_request(request_id: str): - """Get compute request details""" - try: - client = get_edge_client() - response = client.get(f"/v1/serve/requests/{request_id}") - response.raise_for_status() - result = response.json() - output(result) - except Exception as e: - error(f"Error getting request details: {str(e)}") - - -@serve.command() -@click.argument('request_id') -def cancel_request(request_id: str): - """Cancel compute request""" - try: - client = get_edge_client() - response = client.post(f"/v1/serve/requests/{request_id}/cancel") - response.raise_for_status() - result = response.json() - success(result.get("message", f"Request {request_id} cancelled")) - except Exception as e: - error(f"Error cancelling request: {str(e)}") - - -@serve.command() -@click.argument('request_id') -def get_result(request_id: str): - """Get compute result""" - try: - client = get_edge_client() - response = client.get(f"/v1/serve/requests/{request_id}/result") - response.raise_for_status() - result = response.json() - output(result) - except Exception as e: - error(f"Error getting result: {str(e)}") - - -@edge.group() -def metrics(): - """Metrics operations via Edge API""" - pass - - -@metrics.command() -@click.argument('gpu_id') -@click.argument('metrics') -def record(gpu_id: str, metrics: str): - """Record edge metrics""" - try: - import json - client = get_edge_client() - response = client.post("/v1/metrics/", json={ - "gpu_id": gpu_id, - "metrics": json.loads(metrics) - }) - response.raise_for_status() - result = response.json() - - if result.get("success"): - success(f"Metrics {result.get('metric_id')} recorded") - output(result) - else: - error(f"Failed to record metrics: {result.get('message', 'Unknown error')}") - except Exception as e: - error(f"Error recording metrics: {str(e)}") - - -@metrics.command() -@click.option('--gpu-id', help='Filter by GPU ID') -@click.option('--limit', type=int, default=100, help='Number of metrics to return') -def list_metrics(gpu_id: Optional[str], limit: int): - """List edge metrics""" - try: - client = get_edge_client() - params = {"limit": limit} - if gpu_id: - params["gpu_id"] = gpu_id - - response = client.get("/v1/metrics/", params=params) - response.raise_for_status() - result = response.json() - - metrics = result.get("metrics", []) - if metrics: - output(metrics) - else: - info("No metrics found") - except Exception as e: - error(f"Error listing metrics: {str(e)}") - - -@metrics.command() -@click.argument('metric_id') -def get_metric(metric_id: str): - """Get metric details""" - try: - client = get_edge_client() - response = client.get(f"/v1/metrics/{metric_id}") - response.raise_for_status() - result = response.json() - output(result) - except Exception as e: - error(f"Error getting metric details: {str(e)}") - - -@metrics.command() -@click.argument('metric_id') -def delete_metric(metric_id: str): - """Delete metric""" - try: - client = get_edge_client() - response = client.delete(f"/v1/metrics/{metric_id}") - response.raise_for_status() - result = response.json() - success(result.get("message", f"Metric {metric_id} deleted")) - except Exception as e: - error(f"Error deleting metric: {str(e)}") diff --git a/cli/src/aitbc_cli/commands/exchange.py b/cli/src/aitbc_cli/commands/exchange.py deleted file mode 100755 index 617b1e3b..00000000 --- a/cli/src/aitbc_cli/commands/exchange.py +++ /dev/null @@ -1,910 +0,0 @@ -"""Exchange integration commands for AITBC CLI""" - -import click -import json -import os -from pathlib import Path -from typing import Optional, Dict, Any, List -from datetime import datetime, timezone -from ..utils import output, error, success, warning -from ..config import get_config - -# Import shared modules -from aitbc import get_logger, AITBCHTTPClient, NetworkError - -# Initialize logger -logger = get_logger(__name__) - - -@click.group() -def exchange(): - """Exchange integration and trading management commands""" - pass - - -@exchange.command() -@click.option("--name", required=True, help="Exchange name (e.g., Binance, Coinbase, Kraken)") -@click.option("--api-key", required=True, help="Exchange API key") -@click.option("--secret-key", help="Exchange API secret key") -@click.option("--sandbox", is_flag=True, help="Use sandbox/testnet environment") -@click.option("--description", help="Exchange description") -@click.pass_context -def register(ctx, name: str, api_key: str, secret_key: Optional[str], sandbox: bool, description: Optional[str]): - """Register a new exchange integration""" - config = get_config() - - # Create exchange configuration - exchange_config = { - "name": name, - "api_key": api_key, - "secret_key": secret_key or "NOT_SET", - "sandbox": sandbox, - "description": description or f"{name} exchange integration", - "created_at": datetime.now(timezone.utc).isoformat(), - "status": "active", - "trading_pairs": [], - "last_sync": None - } - - # Store exchange configuration - exchanges_file = Path.home() / ".aitbc" / "exchanges.json" - exchanges_file.parent.mkdir(parents=True, exist_ok=True) - - # Load existing exchanges - exchanges = {} - if exchanges_file.exists(): - with open(exchanges_file, 'r') as f: - exchanges = json.load(f) - - # Add new exchange - exchanges[name.lower()] = exchange_config - - # Save exchanges - with open(exchanges_file, 'w') as f: - json.dump(exchanges, f, indent=2) - - success(f"Exchange '{name}' registered successfully") - output({ - "exchange": name, - "status": "registered", - "sandbox": sandbox, - "created_at": exchange_config["created_at"] - }) - - -@exchange.command() -@click.option("--base-asset", required=True, help="Base asset symbol (e.g., AITBC)") -@click.option("--quote-asset", required=True, help="Quote asset symbol (e.g., BTC)") -@click.option("--exchange", required=True, help="Exchange name") -@click.option("--min-order-size", type=float, default=0.001, help="Minimum order size") -@click.option("--price-precision", type=int, default=8, help="Price precision") -@click.option("--quantity-precision", type=int, default=8, help="Quantity precision") -@click.pass_context -def create_pair(ctx, base_asset: str, quote_asset: str, exchange: str, min_order_size: float, price_precision: int, quantity_precision: int): - """Create a new trading pair""" - pair_symbol = f"{base_asset}/{quote_asset}" - - # Load exchanges - exchanges_file = Path.home() / ".aitbc" / "exchanges.json" - if not exchanges_file.exists(): - error("No exchanges registered. Use 'aitbc exchange register' first.") - return - - with open(exchanges_file, 'r') as f: - exchanges = json.load(f) - - if exchange.lower() not in exchanges: - error(f"Exchange '{exchange}' not registered.") - return - - # Create trading pair configuration - pair_config = { - "symbol": pair_symbol, - "base_asset": base_asset, - "quote_asset": quote_asset, - "exchange": exchange, - "min_order_size": min_order_size, - "price_precision": price_precision, - "quantity_precision": quantity_precision, - "status": "active", - "created_at": datetime.now(timezone.utc).isoformat(), - "trading_enabled": False - } - - # Update exchange with new pair - exchanges[exchange.lower()]["trading_pairs"].append(pair_config) - - # Save exchanges - with open(exchanges_file, 'w') as f: - json.dump(exchanges, f, indent=2) - - success(f"Trading pair '{pair_symbol}' created on {exchange}") - output({ - "pair": pair_symbol, - "exchange": exchange, - "status": "created", - "min_order_size": min_order_size, - "created_at": pair_config["created_at"] - }) - - -@exchange.command() -@click.option("--pair", required=True, help="Trading pair symbol (e.g., AITBC/BTC)") -@click.option("--price", type=float, help="Initial price for the pair") -@click.option("--base-liquidity", type=float, default=10000, help="Base asset liquidity amount") -@click.option("--quote-liquidity", type=float, default=10000, help="Quote asset liquidity amount") -@click.option("--exchange", help="Exchange name (if not specified, uses first available)") -@click.pass_context -def start_trading(ctx, pair: str, price: Optional[float], base_liquidity: float, quote_liquidity: float, exchange: Optional[str]): - """Start trading for a specific pair""" - - # Load exchanges - exchanges_file = Path.home() / ".aitbc" / "exchanges.json" - if not exchanges_file.exists(): - error("No exchanges registered. Use 'aitbc exchange register' first.") - return - - with open(exchanges_file, 'r') as f: - exchanges = json.load(f) - - # Find the pair - target_exchange = None - target_pair = None - - for exchange_name, exchange_data in exchanges.items(): - for pair_config in exchange_data.get("trading_pairs", []): - if pair_config["symbol"] == pair: - target_exchange = exchange_name - target_pair = pair_config - break - if target_pair: - break - - if not target_pair: - error(f"Trading pair '{pair}' not found. Create it first with 'aitbc exchange create-pair'.") - return - - # Update pair to enable trading - target_pair["trading_enabled"] = True - target_pair["started_at"] = datetime.now(timezone.utc).isoformat() - target_pair["initial_price"] = price or 0.00001 # Default price for AITBC - target_pair["base_liquidity"] = base_liquidity - target_pair["quote_liquidity"] = quote_liquidity - - # Save exchanges - with open(exchanges_file, 'w') as f: - json.dump(exchanges, f, indent=2) - - success(f"Trading started for pair '{pair}' on {target_exchange}") - output({ - "pair": pair, - "exchange": target_exchange, - "status": "trading_active", - "initial_price": target_pair["initial_price"], - "base_liquidity": base_liquidity, - "quote_liquidity": quote_liquidity, - "started_at": target_pair["started_at"] - }) - - -@exchange.command() -@click.option("--pair", help="Trading pair symbol (e.g., AITBC/BTC)") -@click.option("--exchange", help="Exchange name") -@click.option("--real-time", is_flag=True, help="Enable real-time monitoring") -@click.option("--interval", type=int, default=60, help="Update interval in seconds") -@click.pass_context -def monitor(ctx, pair: Optional[str], exchange: Optional[str], real_time: bool, interval: int): - """Monitor exchange trading activity""" - - # Load exchanges - exchanges_file = Path.home() / ".aitbc" / "exchanges.json" - if not exchanges_file.exists(): - error("No exchanges registered. Use 'aitbc exchange register' first.") - return - - with open(exchanges_file, 'r') as f: - exchanges = json.load(f) - - # Filter exchanges and pairs - monitoring_data = [] - - for exchange_name, exchange_data in exchanges.items(): - if exchange and exchange_name != exchange.lower(): - continue - - for pair_config in exchange_data.get("trading_pairs", []): - if pair and pair_config["symbol"] != pair: - continue - - monitoring_data.append({ - "exchange": exchange_name, - "pair": pair_config["symbol"], - "status": "active" if pair_config.get("trading_enabled") else "inactive", - "created_at": pair_config.get("created_at"), - "started_at": pair_config.get("started_at"), - "initial_price": pair_config.get("initial_price"), - "base_liquidity": pair_config.get("base_liquidity"), - "quote_liquidity": pair_config.get("quote_liquidity") - }) - - if not monitoring_data: - error("No trading pairs found for monitoring.") - return - - # Display monitoring data - output({ - "monitoring_active": True, - "real_time": real_time, - "interval": interval, - "pairs": monitoring_data, - "total_pairs": len(monitoring_data) - }) - - if real_time: - warning(f"Real-time monitoring enabled. Updates every {interval} seconds.") - # Note: In a real implementation, this would start a background monitoring process - - -@exchange.command() -@click.option("--pair", required=True, help="Trading pair symbol (e.g., AITBC/BTC)") -@click.option("--amount", type=float, required=True, help="Liquidity amount") -@click.option("--side", type=click.Choice(['buy', 'sell']), default='both', help="Side to provide liquidity") -@click.option("--exchange", help="Exchange name") -@click.pass_context -def add_liquidity(ctx, pair: str, amount: float, side: str, exchange: Optional[str]): - """Add liquidity to a trading pair""" - - # Load exchanges - exchanges_file = Path.home() / ".aitbc" / "exchanges.json" - if not exchanges_file.exists(): - error("No exchanges registered. Use 'aitbc exchange register' first.") - return - - with open(exchanges_file, 'r') as f: - exchanges = json.load(f) - - # Find the pair - target_exchange = None - target_pair = None - - for exchange_name, exchange_data in exchanges.items(): - if exchange and exchange_name != exchange.lower(): - continue - - for pair_config in exchange_data.get("trading_pairs", []): - if pair_config["symbol"] == pair: - target_exchange = exchange_name - target_pair = pair_config - break - if target_pair: - break - - if not target_pair: - error(f"Trading pair '{pair}' not found.") - return - - # Add liquidity - if side == 'buy' or side == 'both': - target_pair["quote_liquidity"] = target_pair.get("quote_liquidity", 0) + amount - if side == 'sell' or side == 'both': - target_pair["base_liquidity"] = target_pair.get("base_liquidity", 0) + amount - - target_pair["liquidity_updated_at"] = datetime.now(timezone.utc).isoformat() - - # Save exchanges - with open(exchanges_file, 'w') as f: - json.dump(exchanges, f, indent=2) - - success(f"Added {amount} liquidity to {pair} on {target_exchange} ({side} side)") - output({ - "pair": pair, - "exchange": target_exchange, - "amount": amount, - "side": side, - "base_liquidity": target_pair.get("base_liquidity"), - "quote_liquidity": target_pair.get("quote_liquidity"), - "updated_at": target_pair["liquidity_updated_at"] - }) - - -@exchange.command() -@click.pass_context -def list(ctx): - """List all registered exchanges and trading pairs""" - - # Load exchanges - exchanges_file = Path.home() / ".aitbc" / "exchanges.json" - if not exchanges_file.exists(): - warning("No exchanges registered.") - return - - with open(exchanges_file, 'r') as f: - exchanges = json.load(f) - - # Format output - exchange_list = [] - for exchange_name, exchange_data in exchanges.items(): - exchange_info = { - "name": exchange_data["name"], - "status": exchange_data["status"], - "sandbox": exchange_data.get("sandbox", False), - "trading_pairs": len(exchange_data.get("trading_pairs", [])), - "created_at": exchange_data["created_at"] - } - exchange_list.append(exchange_info) - - output({ - "exchanges": exchange_list, - "total_exchanges": len(exchange_list), - "total_pairs": sum(ex["trading_pairs"] for ex in exchange_list) - }) - - -@exchange.command() -@click.argument("exchange_name") -@click.pass_context -def status(ctx, exchange_name: str): - """Get detailed status of a specific exchange""" - - # Load exchanges - exchanges_file = Path.home() / ".aitbc" / "exchanges.json" - if not exchanges_file.exists(): - error("No exchanges registered.") - return - - with open(exchanges_file, 'r') as f: - exchanges = json.load(f) - - if exchange_name.lower() not in exchanges: - error(f"Exchange '{exchange_name}' not found.") - return - - exchange_data = exchanges[exchange_name.lower()] - - output({ - "exchange": exchange_data["name"], - "status": exchange_data["status"], - "sandbox": exchange_data.get("sandbox", False), - "description": exchange_data.get("description"), - "created_at": exchange_data["created_at"], - "trading_pairs": exchange_data.get("trading_pairs", []), - "last_sync": exchange_data.get("last_sync") - }) - config = ctx.obj['config'] - - try: - http_client = AITBCHTTPClient(base_url=config.exchange_service_url, timeout=10) - rates_data = http_client.get(f"/exchange/rates") - success("Current exchange rates:") - output(rates_data, ctx.obj['output_format']) - except NetworkError as e: - error(f"Network error: {e}") - except Exception as e: - error(f"Error: {e}") - - -@exchange.command() -@click.option("--aitbc-amount", type=float, help="Amount of AITBC to buy") -@click.option("--btc-amount", type=float, help="Amount of BTC to spend") -@click.option("--user-id", help="User ID for the payment") -@click.option("--notes", help="Additional notes for the payment") -@click.pass_context -def create_payment(ctx, aitbc_amount: Optional[float], btc_amount: Optional[float], - user_id: Optional[str], notes: Optional[str]): - """Create a Bitcoin payment request for AITBC purchase""" - config = ctx.obj['config'] - - # Validate input - if aitbc_amount is not None and aitbc_amount <= 0: - error("AITBC amount must be greater than 0") - return - - if btc_amount is not None and btc_amount <= 0: - error("BTC amount must be greater than 0") - return - - if not aitbc_amount and not btc_amount: - error("Either --aitbc-amount or --btc-amount must be specified") - return - - # Get exchange rates to calculate missing amount - try: - http_client = AITBCHTTPClient(base_url=config.exchange_service_url, timeout=10) - rates = http_client.get("/exchange/rates") - btc_to_aitbc = rates.get('btc_to_aitbc', 100000) - - # Calculate missing amount - if aitbc_amount and not btc_amount: - btc_amount = aitbc_amount / btc_to_aitbc - elif btc_amount and not aitbc_amount: - aitbc_amount = btc_amount * btc_to_aitbc - - # Prepare payment request - payment_data = { - "user_id": user_id or "cli_user", - "aitbc_amount": aitbc_amount, - "btc_amount": btc_amount - } - - if notes: - payment_data["notes"] = notes - - # Create payment - payment = http_client.post("/exchange/create-payment", json=payment_data) - success(f"Payment created: {payment.get('payment_id')}") - success(f"Send {btc_amount:.8f} BTC to: {payment.get('payment_address')}") - success(f"Expires at: {payment.get('expires_at')}") - output(payment, ctx.obj['output_format']) - except NetworkError as e: - error(f"Network error: {e}") - except Exception as e: - error(f"Error: {e}") - - -@exchange.command() -@click.option("--payment-id", required=True, help="Payment ID to check") -@click.pass_context -def payment_status(ctx, payment_id: str): - """Check payment confirmation status""" - config = ctx.obj['config'] - - try: - http_client = AITBCHTTPClient(base_url=config.exchange_service_url, timeout=10) - status_data = http_client.get(f"/exchange/payment-status/{payment_id}") - status = status_data.get('status', 'unknown') - - if status == 'confirmed': - success(f"Payment {payment_id} is confirmed!") - success(f"AITBC amount: {status_data.get('aitbc_amount', 0)}") - elif status == 'pending': - success(f"Payment {payment_id} is pending confirmation") - elif status == 'expired': - error(f"Payment {payment_id} has expired") - else: - success(f"Payment {payment_id} status: {status}") - - output(status_data, ctx.obj['output_format']) - except NetworkError as e: - error(f"Network error: {e}") - except Exception as e: - error(f"Error: {e}") - - -@exchange.command() -@click.pass_context -def market_stats(ctx): - """Get exchange market statistics""" - config = ctx.obj['config'] - - try: - http_client = AITBCHTTPClient(base_url=config.exchange_service_url, timeout=10) - stats = http_client.get("/exchange/market-stats") - success("Exchange market statistics:") - output(stats, ctx.obj['output_format']) - except NetworkError as e: - error(f"Network error: {e}") - except Exception as e: - error(f"Error: {e}") - - -@exchange.group() -def wallet(): - """Bitcoin wallet operations""" - pass - - -@wallet.command() -@click.pass_context -def balance(ctx): - """Get Bitcoin wallet balance""" - config = ctx.obj['config'] - - try: - http_client = AITBCHTTPClient(base_url=config.exchange_service_url, timeout=10) - balance_data = http_client.get("/exchange/wallet/balance") - success("Bitcoin wallet balance:") - output(balance_data, ctx.obj['output_format']) - except NetworkError as e: - error(f"Network error: {e}") - except Exception as e: - error(f"Error: {e}") - - -@wallet.command() -@click.pass_context -def info(ctx): - """Get comprehensive Bitcoin wallet information""" - config = ctx.obj['config'] - - try: - http_client = AITBCHTTPClient(base_url=config.exchange_service_url, timeout=10) - wallet_info = http_client.get("/exchange/wallet/info") - success("Bitcoin wallet information:") - output(wallet_info, ctx.obj['output_format']) - except NetworkError as e: - error(f"Network error: {e}") - except Exception as e: - error(f"Error: {e}") - - -@exchange.command() -@click.option("--name", required=True, help="Exchange name (e.g., Binance, Coinbase)") -@click.option("--api-key", required=True, help="API key for exchange integration") -@click.option("--api-secret", help="API secret for exchange integration") -@click.option("--sandbox", is_flag=True, default=False, help="Use sandbox/testnet environment") -@click.pass_context -def register(ctx, name: str, api_key: str, api_secret: Optional[str], sandbox: bool): - """Register a new exchange integration""" - config = ctx.obj['config'] - - exchange_data = { - "name": name, - "api_key": api_key, - "sandbox": sandbox - } - - if api_secret: - exchange_data["api_secret"] = api_secret - - try: - http_client = AITBCHTTPClient(base_url=config.exchange_service_url, timeout=10) - result = http_client.post("/exchange/register", json=exchange_data) - success(f"Exchange '{name}' registered successfully!") - success(f"Exchange ID: {result.get('exchange_id')}") - output(result, ctx.obj['output_format']) - except NetworkError as e: - error(f"Network error: {e}") - except Exception as e: - error(f"Error: {e}") - - -@exchange.command() -@click.option("--pair", required=True, help="Trading pair (e.g., AITBC/BTC, AITBC/ETH)") -@click.option("--base-asset", required=True, help="Base asset symbol") -@click.option("--quote-asset", required=True, help="Quote asset symbol") -@click.option("--min-order-size", type=float, help="Minimum order size") -@click.option("--max-order-size", type=float, help="Maximum order size") -@click.option("--price-precision", type=int, default=8, help="Price decimal precision") -@click.option("--size-precision", type=int, default=8, help="Size decimal precision") -@click.pass_context -def create_pair(ctx, pair: str, base_asset: str, quote_asset: str, - min_order_size: Optional[float], max_order_size: Optional[float], - price_precision: int, size_precision: int): - """Create a new trading pair""" - config = ctx.obj['config'] - - pair_data = { - "pair": pair, - "base_asset": base_asset, - "quote_asset": quote_asset, - "price_precision": price_precision, - "size_precision": size_precision - } - - if min_order_size is not None: - pair_data["min_order_size"] = min_order_size - if max_order_size is not None: - pair_data["max_order_size"] = max_order_size - - try: - http_client = AITBCHTTPClient(base_url=config.exchange_service_url, timeout=10) - result = http_client.post("/exchange/create-pair", json=pair_data) - success(f"Trading pair '{pair}' created successfully!") - success(f"Pair ID: {result.get('pair_id')}") - output(result, ctx.obj['output_format']) - except NetworkError as e: - error(f"Network error: {e}") - except Exception as e: - error(f"Error: {e}") - - -@exchange.command() -@click.option("--pair", required=True, help="Trading pair to start trading") -@click.option("--exchange", help="Specific exchange to enable") -@click.option("--order-type", multiple=True, default=["limit", "market"], - help="Order types to enable (limit, market, stop_limit)") -@click.pass_context -def start_trading(ctx, pair: str, exchange: Optional[str], order_type: tuple): - """Start trading for a specific pair""" - config = ctx.obj['config'] - - trading_data = { - "pair": pair, - "order_types": list(order_type) - } - - if exchange: - trading_data["exchange"] = exchange - - try: - http_client = AITBCHTTPClient(base_url=config.exchange_service_url, timeout=10) - result = http_client.post("/exchange/start-trading", json=trading_data) - success(f"Trading started for pair '{pair}'!") - success(f"Order types: {', '.join(order_type)}") - output(result, ctx.obj['output_format']) - except NetworkError as e: - error(f"Network error: {e}") - except Exception as e: - error(f"Error: {e}") - - -@exchange.command() -@click.option("--pair", help="Filter by trading pair") -@click.option("--exchange", help="Filter by exchange") -@click.option("--status", help="Filter by status (active, inactive, suspended)") -@click.pass_context -def list_pairs(ctx, pair: Optional[str], exchange: Optional[str], status: Optional[str]): - """List all trading pairs""" - config = ctx.obj['config'] - - params = {} - if pair: - params["pair"] = pair - if exchange: - params["exchange"] = exchange - if status: - params["status"] = status - - try: - http_client = AITBCHTTPClient(base_url=config.exchange_service_url, timeout=10) - pairs = http_client.get("/exchange/pairs", params=params) - success("Trading pairs:") - output(pairs, ctx.obj['output_format']) - except NetworkError as e: - error(f"Network error: {e}") - except Exception as e: - error(f"Error: {e}") - - -@exchange.command() -@click.option("--exchange", required=True, help="Exchange name (binance, coinbasepro, kraken)") -@click.option("--api-key", required=True, help="API key for exchange") -@click.option("--secret", required=True, help="API secret for exchange") -@click.option("--sandbox", is_flag=True, default=True, help="Use sandbox/testnet environment") -@click.option("--passphrase", help="API passphrase (for Coinbase)") -@click.pass_context -def connect(ctx, exchange: str, api_key: str, secret: str, sandbox: bool, passphrase: Optional[str]): - """Connect to a real exchange API""" - try: - # Import the real exchange integration - import sys - exchange_path = str(Path(__file__).resolve().parent.parent.parent.parent / 'apps' / 'exchange') - sys.path.append(exchange_path) - from real_exchange_integration import connect_to_exchange - - # Run async connection - import asyncio - success = asyncio.run(connect_to_exchange(exchange, api_key, secret, sandbox, passphrase)) - - if success: - success(f"āœ… Successfully connected to {exchange}") - if sandbox: - success("🧪 Using sandbox/testnet environment") - else: - error(f"āŒ Failed to connect to {exchange}") - - except ImportError: - error("āŒ Real exchange integration not available. Install ccxt library.") - except Exception as e: - error(f"āŒ Connection error: {e}") - - -@exchange.command() -@click.option("--exchange", help="Check specific exchange (default: all)") -@click.pass_context -def status(ctx, exchange: Optional[str]): - """Check exchange connection status""" - try: - # Import the real exchange integration - import sys - exchange_path = str(Path(__file__).resolve().parent.parent.parent.parent / 'apps' / 'exchange') - sys.path.append(exchange_path) - from real_exchange_integration import get_exchange_status - - # Run async status check - import asyncio - status_data = asyncio.run(get_exchange_status(exchange)) - - # Display status - for exchange_name, health in status_data.items(): - status_icon = "🟢" if health.status.value == "connected" else "šŸ”“" if health.status.value == "error" else "🟔" - - success(f"{status_icon} {exchange_name.upper()}") - success(f" Status: {health.status.value}") - success(f" Latency: {health.latency_ms:.2f}ms") - success(f" Last Check: {health.last_check.strftime('%H:%M:%S')}") - - if health.error_message: - error(f" Error: {health.error_message}") - click.echo("") - - except ImportError: - error("āŒ Real exchange integration not available. Install ccxt library.") - except Exception as e: - error(f"āŒ Status check error: {e}") - - -@exchange.command() -@click.option("--exchange", required=True, help="Exchange name to disconnect") -@click.pass_context -def disconnect(ctx, exchange: str): - """Disconnect from an exchange""" - try: - # Import the real exchange integration - import sys - exchange_path = str(Path(__file__).resolve().parent.parent.parent.parent / 'apps' / 'exchange') - sys.path.append(exchange_path) - from real_exchange_integration import disconnect_from_exchange - - # Run async disconnection - import asyncio - success = asyncio.run(disconnect_from_exchange(exchange)) - - if success: - success(f"šŸ”Œ Disconnected from {exchange}") - else: - error(f"āŒ Failed to disconnect from {exchange}") - - except ImportError: - error("āŒ Real exchange integration not available. Install ccxt library.") - except Exception as e: - error(f"āŒ Disconnection error: {e}") - - -@exchange.command() -@click.option("--exchange", required=True, help="Exchange name") -@click.option("--symbol", required=True, help="Trading symbol (e.g., BTC/USDT)") -@click.option("--limit", type=int, default=20, help="Order book depth") -@click.pass_context -def orderbook(ctx, exchange: str, symbol: str, limit: int): - """Get order book from exchange""" - try: - # Import the real exchange integration - import sys - exchange_path = str(Path(__file__).resolve().parent.parent.parent.parent / 'apps' / 'exchange') - sys.path.append(exchange_path) - from real_exchange_integration import exchange_manager - - # Run async order book fetch - import asyncio - orderbook = asyncio.run(exchange_manager.get_order_book(exchange, symbol, limit)) - - # Display order book - success(f"šŸ“Š Order Book for {symbol} on {exchange.upper()}") - - # Display bids (buy orders) - if 'bids' in orderbook and orderbook['bids']: - success("\n🟢 Bids (Buy Orders):") - for i, bid in enumerate(orderbook['bids'][:10]): - price, amount = bid - success(f" {i+1}. ${price:.8f} x {amount:.6f}") - - # Display asks (sell orders) - if 'asks' in orderbook and orderbook['asks']: - success("\nšŸ”“ Asks (Sell Orders):") - for i, ask in enumerate(orderbook['asks'][:10]): - price, amount = ask - success(f" {i+1}. ${price:.8f} x {amount:.6f}") - - # Spread - if 'bids' in orderbook and 'asks' in orderbook and orderbook['bids'] and orderbook['asks']: - best_bid = orderbook['bids'][0][0] - best_ask = orderbook['asks'][0][0] - spread = best_ask - best_bid - spread_pct = (spread / best_bid) * 100 - - success(f"\nšŸ“ˆ Spread: ${spread:.8f} ({spread_pct:.4f}%)") - success(f"šŸŽÆ Best Bid: ${best_bid:.8f}") - success(f"šŸŽÆ Best Ask: ${best_ask:.8f}") - - except ImportError: - error("āŒ Real exchange integration not available. Install ccxt library.") - except Exception as e: - error(f"āŒ Order book error: {e}") - - -@exchange.command() -@click.option("--exchange", required=True, help="Exchange name") -@click.pass_context -def balance(ctx, exchange: str): - """Get account balance from exchange""" - try: - # Import the real exchange integration - import sys - exchange_path = str(Path(__file__).resolve().parent.parent.parent.parent / 'apps' / 'exchange') - sys.path.append(exchange_path) - from real_exchange_integration import exchange_manager - - # Run async balance fetch - import asyncio - balance_data = asyncio.run(exchange_manager.get_balance(exchange)) - - # Display balance - success(f"šŸ’° Account Balance on {exchange.upper()}") - - if 'total' in balance_data: - for asset, amount in balance_data['total'].items(): - if amount > 0: - available = balance_data.get('free', {}).get(asset, 0) - used = balance_data.get('used', {}).get(asset, 0) - - success(f"\n{asset}:") - success(f" Total: {amount:.8f}") - success(f" Available: {available:.8f}") - success(f" In Orders: {used:.8f}") - else: - warning("No balance data available") - - except ImportError: - error("āŒ Real exchange integration not available. Install ccxt library.") - except Exception as e: - error(f"āŒ Balance error: {e}") - - -@exchange.command() -@click.option("--exchange", required=True, help="Exchange name") -@click.pass_context -def pairs(ctx, exchange: str): - """List supported trading pairs""" - try: - # Import the real exchange integration - import sys - exchange_path = str(Path(__file__).resolve().parent.parent.parent.parent / 'apps' / 'exchange') - sys.path.append(exchange_path) - from real_exchange_integration import exchange_manager - - # Run async pairs fetch - import asyncio - pairs = asyncio.run(exchange_manager.get_supported_pairs(exchange)) - - # Display pairs - success(f"šŸ“‹ Supported Trading Pairs on {exchange.upper()}") - success(f"Found {len(pairs)} trading pairs:\n") - - # Group by base currency - base_currencies = {} - for pair in pairs: - base = pair.split('/')[0] if '/' in pair else pair.split('-')[0] - if base not in base_currencies: - base_currencies[base] = [] - base_currencies[base].append(pair) - - # Display organized pairs - for base in sorted(base_currencies.keys()): - success(f"\nšŸ”¹ {base}:") - for pair in sorted(base_currencies[base][:10]): # Show first 10 per base - success(f" • {pair}") - - if len(base_currencies[base]) > 10: - success(f" ... and {len(base_currencies[base]) - 10} more") - - except ImportError: - error("āŒ Real exchange integration not available. Install ccxt library.") - except Exception as e: - error(f"āŒ Pairs error: {e}") - - -@exchange.command() -@click.pass_context -def list_exchanges(ctx): - """List all supported exchanges""" - try: - # Import the real exchange integration - import sys - exchange_path = str(Path(__file__).resolve().parent.parent.parent.parent / 'apps' / 'exchange') - sys.path.append(exchange_path) - from real_exchange_integration import exchange_manager - - success("šŸ¢ Supported Exchanges:") - for exchange in exchange_manager.supported_exchanges: - success(f" • {exchange.title()}") - - success("\nšŸ“ Usage:") - success(" aitbc exchange connect --exchange binance --api-key --secret ") - success(" aitbc exchange status --exchange binance") - success(" aitbc exchange orderbook --exchange binance --symbol BTC/USDT") - - except ImportError: - error("āŒ Real exchange integration not available. Install ccxt library.") - except Exception as e: - error(f"āŒ Error: {e}") diff --git a/cli/src/aitbc_cli/commands/exchange_island.py b/cli/src/aitbc_cli/commands/exchange_island.py deleted file mode 100644 index 71e9727f..00000000 --- a/cli/src/aitbc_cli/commands/exchange_island.py +++ /dev/null @@ -1,505 +0,0 @@ -""" -Exchange Island CLI Commands -Commands for trading AIT coin against BTC and ETH on the island exchange -""" - -import click -import json -import hashlib -import socket -import os -from datetime import datetime -from decimal import Decimal -from typing import Optional -from ..utils import output, error, success, info, warning -from ..utils.island_credentials import ( - load_island_credentials, get_rpc_endpoint, get_chain_id, - get_island_id, get_island_name -) - -# Import shared modules -from aitbc import get_logger, AITBCHTTPClient, NetworkError - -# Initialize logger -logger = get_logger(__name__) - - -def safe_load_credentials(): - """Load island credentials with graceful error handling""" - try: - return load_island_credentials() - except FileNotFoundError as e: - error(f"Island credentials not found: {e}") - error("Run 'aitbc node island join' to join an island first") - return None - - -# Supported trading pairs -SUPPORTED_PAIRS = ['AIT/BTC', 'AIT/ETH'] - - -@click.group() -def exchange_island(): - """Exchange commands for trading AIT against BTC and ETH on the island""" - pass - - -@exchange_island.command() -@click.argument('ait_amount', type=float) -@click.argument('quote_currency', type=click.Choice(['BTC', 'ETH'])) -@click.option('--max-price', type=float, help='Maximum price to pay per AIT') -@click.pass_context -def buy(ctx, ait_amount: float, quote_currency: str, max_price: Optional[float]): - """Buy AIT with BTC or ETH""" - try: - if ait_amount <= 0: - error("AIT amount must be greater than 0") - raise click.Abort() - - # Load island credentials - credentials = safe_load_credentials() - if not credentials: - return - rpc_endpoint = get_rpc_endpoint() - chain_id = get_chain_id() - island_id = get_island_id() - - # Get user node ID - hostname = socket.gethostname() - local_address = socket.gethostbyname(hostname) - p2p_port = credentials.get('credentials', {}).get('p2p_port', 8001) - - # Get public key for node ID generation - keystore_path = '/var/lib/aitbc/keystore/validator_keys.json' - if os.path.exists(keystore_path): - with open(keystore_path, 'r') as f: - keys = json.load(f) - public_key_pem = None - for key_id, key_data in keys.items(): - public_key_pem = key_data.get('public_key_pem') - break - if public_key_pem: - content = f"{hostname}:{local_address}:{p2p_port}:{public_key_pem}" - user_id = hashlib.sha256(content.encode()).hexdigest() - else: - error("No public key found in keystore") - raise click.Abort() - else: - error(f"Keystore not found at {keystore_path}") - raise click.Abort() - - pair = f"AIT/{quote_currency}" - - # Generate order ID - order_id = f"exchange_buy_{datetime.now().strftime('%Y%m%d%H%M%S')}_{hashlib.sha256(f'{user_id}{ait_amount}{quote_currency}'.encode()).hexdigest()[:8]}" - - # Create buy order transaction - buy_order_data = { - 'type': 'exchange', - 'action': 'buy', - 'order_id': order_id, - 'user_id': user_id, - 'pair': pair, - 'side': 'buy', - 'amount': float(ait_amount), - 'max_price': float(max_price) if max_price else None, - 'status': 'open', - 'island_id': island_id, - 'chain_id': chain_id, - 'created_at': datetime.now().isoformat() - } - - # Submit transaction to blockchain - try: - http_client = AITBCHTTPClient(base_url=rpc_endpoint, timeout=10) - result = http_client.post("/transaction", json=buy_order_data) - success(f"Buy order created successfully!") - success(f"Order ID: {order_id}") - success(f"Buying {ait_amount} AIT with {quote_currency}") - - if max_price: - success(f"Max price: {max_price:.8f} {quote_currency}/AIT") - - order_info = { - "Order ID": order_id, - "Pair": pair, - "Side": "BUY", - "Amount": f"{ait_amount} AIT", - "Max Price": f"{max_price:.8f} {quote_currency}/AIT" if max_price else "Market", - "Status": "open", - "User": user_id[:16] + "...", - "Island": island_id[:16] + "..." - } - output(order_info, ctx.obj.get('output_format', 'table')) - except NetworkError as e: - error(f"Network error submitting transaction: {e}") - raise click.Abort() - except Exception as e: - error(f"Error submitting transaction: {e}") - raise click.Abort() - - except Exception as e: - error(f"Error creating buy order: {str(e)}") - raise click.Abort() - - -@exchange_island.command() -@click.argument('ait_amount', type=float) -@click.argument('quote_currency', type=click.Choice(['BTC', 'ETH'])) -@click.option('--min-price', type=float, help='Minimum price to accept per AIT') -@click.pass_context -def sell(ctx, ait_amount: float, quote_currency: str, min_price: Optional[float]): - """Sell AIT for BTC or ETH""" - try: - if ait_amount <= 0: - error("AIT amount must be greater than 0") - raise click.Abort() - - # Load island credentials - credentials = safe_load_credentials() - if not credentials: - return - rpc_endpoint = get_rpc_endpoint() - chain_id = get_chain_id() - island_id = get_island_id() - - # Get user node ID - hostname = socket.gethostname() - local_address = socket.gethostbyname(hostname) - p2p_port = credentials.get('credentials', {}).get('p2p_port', 8001) - - # Get public key for node ID generation - keystore_path = '/var/lib/aitbc/keystore/validator_keys.json' - if os.path.exists(keystore_path): - with open(keystore_path, 'r') as f: - keys = json.load(f) - public_key_pem = None - for key_id, key_data in keys.items(): - public_key_pem = key_data.get('public_key_pem') - break - if public_key_pem: - content = f"{hostname}:{local_address}:{p2p_port}:{public_key_pem}" - user_id = hashlib.sha256(content.encode()).hexdigest() - else: - error("No public key found in keystore") - raise click.Abort() - else: - error(f"Keystore not found at {keystore_path}") - raise click.Abort() - - pair = f"AIT/{quote_currency}" - - # Generate order ID - order_id = f"exchange_sell_{datetime.now().strftime('%Y%m%d%H%M%S')}_{hashlib.sha256(f'{user_id}{ait_amount}{quote_currency}'.encode()).hexdigest()[:8]}" - - # Create sell order transaction - sell_order_data = { - 'type': 'exchange', - 'action': 'sell', - 'order_id': order_id, - 'user_id': user_id, - 'pair': pair, - 'side': 'sell', - 'amount': float(ait_amount), - 'min_price': float(min_price) if min_price else None, - 'status': 'open', - 'island_id': island_id, - 'chain_id': chain_id, - 'created_at': datetime.now().isoformat() - } - - # Submit transaction to blockchain - try: - http_client = AITBCHTTPClient(base_url=rpc_endpoint, timeout=10) - result = http_client.post("/transaction", json=sell_order_data) - success(f"Sell order created successfully!") - success(f"Order ID: {order_id}") - success(f"Selling {ait_amount} AIT for {quote_currency}") - - if min_price: - success(f"Min price: {min_price:.8f} {quote_currency}/AIT") - - order_info = { - "Order ID": order_id, - "Pair": pair, - "Side": "SELL", - "Amount": f"{ait_amount} AIT", - "Min Price": f"{min_price:.8f} {quote_currency}/AIT" if min_price else "Market", - "Status": "open", - "User": user_id[:16] + "...", - "Island": island_id[:16] + "..." - } - output(order_info, ctx.obj.get('output_format', 'table')) - except NetworkError as e: - error(f"Network error submitting transaction: {e}") - raise click.Abort() - except Exception as e: - error(f"Error creating sell order: {str(e)}") - raise click.Abort() - - -@exchange_island.command() -@click.argument('pair', type=click.Choice(SUPPORTED_PAIRS)) -@click.option('--limit', type=int, default=20, help='Order book depth') -@click.pass_context -def orderbook(ctx, pair: str, limit: int): - """View the order book for a trading pair""" - try: - # Load island credentials - credentials = safe_load_credentials() - if not credentials: - return - rpc_endpoint = get_rpc_endpoint() - island_id = get_island_id() - - # Query blockchain for exchange orders - try: - params = { - 'transaction_type': 'exchange', - 'island_id': island_id, - 'pair': pair, - 'status': 'open', - 'limit': limit * 2 # Get both buys and sells - } - - http_client = AITBCHTTPClient(base_url=rpc_endpoint, timeout=10) - transactions = http_client.get("/transactions", params=params) - - # Separate buy and sell orders - buy_orders = [] - sell_orders = [] - - for order in transactions: - if order.get('side') == 'buy': - buy_orders.append(order) - elif order.get('side') == 'sell': - sell_orders.append(order) - - # Sort buy orders by price descending (highest first) - buy_orders.sort(key=lambda x: x.get('max_price', 0), reverse=True) - # Sort sell orders by price ascending (lowest first) - sell_orders.sort(key=lambda x: x.get('min_price', float('inf'))) - - if not buy_orders and not sell_orders: - info(f"No open orders for {pair}") - return - - # Display sell orders (asks) - if sell_orders: - asks_data = [] - for order in sell_orders[:limit]: - asks_data.append({ - "Price": f"{order.get('min_price', 0):.8f}", - "Amount": f"{order.get('amount', 0):.4f} AIT", - "Total": f"{order.get('min_price', 0) * order.get('amount', 0):.8f} {pair.split('/')[1]}", - "User": order.get('user_id', '')[:16] + "...", - "Order": order.get('order_id', '')[:16] + "..." - }) - - output(asks_data, ctx.obj.get('output_format', 'table'), title=f"Sell Orders (Asks) - {pair}") - - # Display buy orders (bids) - if buy_orders: - bids_data = [] - for order in buy_orders[:limit]: - bids_data.append({ - "Price": f"{order.get('max_price', 0):.8f}", - "Amount": f"{order.get('amount', 0):.4f} AIT", - "Total": f"{order.get('max_price', 0) * order.get('amount', 0):.8f} {pair.split('/')[1]}", - "User": order.get('user_id', '')[:16] + "...", - "Order": order.get('order_id', '')[:16] + "..." - }) - - output(bids_data, ctx.obj.get('output_format', 'table'), title=f"Buy Orders (Bids) - {pair}") - - # Calculate spread if both exist - if sell_orders and buy_orders: - best_ask = sell_orders[0].get('min_price', 0) - best_bid = buy_orders[0].get('max_price', 0) - spread = best_ask - best_bid - if best_bid > 0: - spread_pct = (spread / best_bid) * 100 - info(f"Spread: {spread:.8f} ({spread_pct:.4f}%)") - info(f"Best Bid: {best_bid:.8f} {pair.split('/')[1]}/AIT") - info(f"Best Ask: {best_ask:.8f} {pair.split('/')[1]}/AIT") - except NetworkError as e: - error(f"Network error fetching order book: {e}") - raise click.Abort() - except Exception as e: - error(f"Error fetching order book: {str(e)}") - raise click.Abort() - - -@exchange_island.command() -@click.pass_context -def rates(ctx): - """View current exchange rates for AIT/BTC and AIT/ETH""" - try: - # Load island credentials - credentials = safe_load_credentials() - if not credentials: - return - rpc_endpoint = get_rpc_endpoint() - island_id = get_island_id() - - # Query blockchain for exchange orders to calculate rates - try: - rates_data = [] - - for pair in SUPPORTED_PAIRS: - params = { - 'transaction_type': 'exchange', - 'island_id': island_id, - 'pair': pair, - 'status': 'open', - 'limit': 100 - } - - http_client = AITBCHTTPClient(base_url=rpc_endpoint, timeout=10) - orders = http_client.get("/transactions", params=params) - - # Calculate rates from order book - buy_orders = [o for o in orders if o.get('side') == 'buy'] - sell_orders = [o for o in orders if o.get('side') == 'sell'] - - # Get best bid and ask - best_bid = max([o.get('max_price', 0) for o in buy_orders]) if buy_orders else 0 - best_ask = min([o.get('min_price', float('inf')) for o in sell_orders]) if sell_orders else 0 - - # Calculate mid price - mid_price = (best_bid + best_ask) / 2 if best_bid > 0 and best_ask < float('inf') else 0 - - rates_data.append({ - "Pair": pair, - "Best Bid": f"{best_bid:.8f}" if best_bid > 0 else "N/A", - "Best Ask": f"{best_ask:.8f}" if best_ask < float('inf') else "N/A", - "Mid Price": f"{mid_price:.8f}" if mid_price > 0 else "N/A", - "Buy Orders": len(buy_orders), - "Sell Orders": len(sell_orders) - }) - - output(rates_data, ctx.obj.get('output_format', 'table'), title="Exchange Rates") - - except Exception as e: - error(f"Network error querying blockchain: {e}") - raise click.Abort() - - except Exception as e: - error(f"Error viewing exchange rates: {str(e)}") - raise click.Abort() - - -@exchange_island.command() -@click.option('--user', help='Filter by user ID') -@click.option('--status', help='Filter by status (open, filled, partially_filled, cancelled)') -@click.option('--pair', type=click.Choice(SUPPORTED_PAIRS), help='Filter by trading pair') -@click.pass_context -def orders(ctx, user: Optional[str], status: Optional[str], pair: Optional[str]): - """List exchange orders""" - try: - # Load island credentials - credentials = safe_load_credentials() - if not credentials: - return - rpc_endpoint = get_rpc_endpoint() - island_id = get_island_id() - - # Query blockchain for exchange orders - try: - params = { - 'transaction_type': 'exchange', - 'island_id': island_id - } - if user: - params['user_id'] = user - if status: - params['status'] = status - if pair: - params['pair'] = pair - - http_client = AITBCHTTPClient(base_url=rpc_endpoint, timeout=10) - orders = http_client.get("/transactions", params=params) - - if not orders: - info("No exchange orders found") - return - - # Format output - orders_data = [] - for order in orders: - orders_data.append({ - "Order ID": order.get('order_id', '')[:20] + "...", - "Pair": order.get('pair'), - "Side": order.get('side', '').upper(), - "Amount": f"{order.get('amount', 0):.4f} AIT", - "Price": f"{order.get('max_price', order.get('min_price', 0)):.8f}" if order.get('max_price') or order.get('min_price') else "Market", - "Status": order.get('status'), - "User": order.get('user_id', '')[:16] + "...", - "Created": order.get('created_at', '')[:19] - }) - - output(orders_data, ctx.obj.get('output_format', 'table'), title=f"Exchange Orders ({island_id[:16]}...)") - except NetworkError as e: - error(f"Network error querying blockchain: {e}") - raise click.Abort() - - except Exception as e: - error(f"Error listing orders: {str(e)}") - raise click.Abort() - - -@exchange_island.command() -@click.argument('order_id') -@click.pass_context -def cancel(ctx, order_id: str): - """Cancel an exchange order""" - try: - # Load island credentials - credentials = safe_load_credentials() - if not credentials: - return - rpc_endpoint = get_rpc_endpoint() - chain_id = get_chain_id() - island_id = get_island_id() - - # Get local node ID - hostname = socket.gethostname() - local_address = socket.gethostbyname(hostname) - p2p_port = credentials.get('credentials', {}).get('p2p_port', 8001) - - keystore_path = '/var/lib/aitbc/keystore/validator_keys.json' - if os.path.exists(keystore_path): - with open(keystore_path, 'r') as f: - keys = json.load(f) - public_key_pem = None - for key_id, key_data in keys.items(): - public_key_pem = key_data.get('public_key_pem') - break - if public_key_pem: - content = f"{hostname}:{local_address}:{p2p_port}:{public_key_pem}" - local_node_id = hashlib.sha256(content.encode()).hexdigest() - - # Create cancel transaction - cancel_data = { - 'type': 'exchange', - 'action': 'cancel', - 'order_id': order_id, - 'user_id': local_node_id, - 'status': 'cancelled', - 'cancelled_at': datetime.now().isoformat(), - 'island_id': island_id, - 'chain_id': chain_id - } - - # Submit transaction to blockchain - try: - http_client = AITBCHTTPClient(base_url=rpc_endpoint, timeout=10) - result = http_client.post("/transaction", json=cancel_data) - success(f"Order {order_id} cancelled successfully!") - except NetworkError as e: - error(f"Network error submitting transaction: {e}") - raise click.Abort() - - except Exception as e: - error(f"Error cancelling order: {str(e)}") - raise click.Abort() diff --git a/cli/src/aitbc_cli/commands/genesis.py b/cli/src/aitbc_cli/commands/genesis.py deleted file mode 100644 index 883d6f5d..00000000 --- a/cli/src/aitbc_cli/commands/genesis.py +++ /dev/null @@ -1,194 +0,0 @@ -"""Genesis block and wallet generation commands for AITBC CLI""" - -import click -from typing import Optional -from ..utils import output, error, success -import subprocess -import sys -from pathlib import Path - - -@click.group() -def genesis(): - """Genesis block and wallet generation commands""" - pass - - -@genesis.command() -@click.option("--chain-id", default="ait-mainnet", help="Chain ID for genesis") -@click.option("--create-wallet", is_flag=True, help="Create genesis wallet with secure random key") -@click.option("--password", help="Wallet password (auto-generated if not provided)") -@click.option("--proposer", help="Proposer address (defaults to genesis wallet)") -@click.option("--force", is_flag=True, help="Force overwrite existing genesis") -@click.option("--register-service", is_flag=True, help="Register genesis wallet with wallet service") -@click.option("--service-url", default="http://localhost:8003", help="Wallet service URL") -@click.pass_context -def init(ctx, chain_id: str, create_wallet: bool, password: Optional[str], proposer: Optional[str], - force: bool, register_service: bool, service_url: str): - """Initialize genesis block and wallet for a blockchain""" - script_path = Path("/opt/aitbc/apps/blockchain-node/scripts/unified_genesis.py") - - if not script_path.exists(): - error(f"Genesis generation script not found: {script_path}") - return - - # Build command - cmd = [ - sys.executable, - str(script_path), - "--chain-id", chain_id - ] - - if create_wallet: - cmd.append("--create-wallet") - - if password: - cmd.extend(["--password", password]) - - if proposer: - cmd.extend(["--proposer", proposer]) - - if force: - cmd.append("--force") - - if register_service: - cmd.append("--register-service") - cmd.extend(["--service-url", service_url]) - - try: - success(f"Running genesis generation for {chain_id}...") - result = subprocess.run(cmd, capture_output=True, text=True, check=True) - output(result.stdout, ctx.obj.get("output_format", "table")) - success(f"Genesis generation completed successfully") - except subprocess.CalledProcessError as e: - error(f"Genesis generation failed: {e.stderr}") - return - - -@genesis.command() -@click.option("--chain-id", default="ait-mainnet", help="Chain ID to verify") -@click.pass_context -def verify(ctx, chain_id: str): - """Verify genesis block and wallet configuration""" - import json - import sqlite3 - - # Check genesis config file - genesis_path = Path(f"/var/lib/aitbc/data/{chain_id}/genesis.json") - if not genesis_path.exists(): - error(f"Genesis config not found: {genesis_path}") - return - - try: - with open(genesis_path) as f: - genesis_data = json.load(f) - - success(f"āœ“ Genesis config found: {genesis_path}") - output({ - "chain_id": genesis_data.get("chain_id"), - "genesis_hash": genesis_data.get("block", {}).get("hash"), - "proposer": genesis_data.get("block", {}).get("proposer"), - "allocations_count": len(genesis_data.get("allocations", [])) - }, ctx.obj.get("output_format", "table")) - except Exception as e: - error(f"Failed to read genesis config: {e}") - return - - # Check database - db_path = Path("/var/lib/aitbc/data/chain.db") - if not db_path.exists(): - error(f"Database not found: {db_path}") - return - - try: - conn = sqlite3.connect(str(db_path)) - cursor = conn.cursor() - - # Check genesis block - cursor.execute("SELECT * FROM block WHERE height=0 AND chain_id=?", (chain_id,)) - genesis_block = cursor.fetchone() - - if genesis_block: - success(f"āœ“ Genesis block found in database") - output({ - "height": genesis_block[1], - "hash": genesis_block[2], - "proposer": genesis_block[4] - }, ctx.obj.get("output_format", "table")) - else: - error(f"Genesis block not found in database for chain {chain_id}") - - # Check genesis accounts - cursor.execute("SELECT COUNT(*) FROM account WHERE chain_id=?", (chain_id,)) - account_count = cursor.fetchone()[0] - - if account_count > 0: - success(f"āœ“ Found {account_count} accounts in database") - else: - error(f"No accounts found in database for chain {chain_id}") - - conn.close() - except Exception as e: - error(f"Failed to verify database: {e}") - return - - # Check genesis wallet - wallet_path = Path("/var/lib/aitbc/keystore/genesis.json") - if wallet_path.exists(): - success(f"āœ“ Genesis wallet found: {wallet_path}") - try: - with open(wallet_path) as f: - wallet_data = json.load(f) - output({ - "address": wallet_data.get("address"), - "public_key": wallet_data.get("public_key")[:16] + "..." if wallet_data.get("public_key") else None - }, ctx.obj.get("output_format", "table")) - except Exception as e: - error(f"Failed to read genesis wallet: {e}") - else: - error(f"Genesis wallet not found: {wallet_path}") - - -@genesis.command() -@click.option("--chain-id", default="ait-mainnet", help="Chain ID to show info for") -@click.pass_context -def info(ctx, chain_id: str): - """Show genesis block information""" - import json - import sqlite3 - - genesis_path = Path(f"/var/lib/aitbc/data/{chain_id}/genesis.json") - if not genesis_path.exists(): - error(f"Genesis config not found: {genesis_path}") - return - - try: - with open(genesis_path) as f: - genesis_data = json.load(f) - - block = genesis_data.get("block", {}) - allocations = genesis_data.get("allocations", []) - - output({ - "chain_id": genesis_data.get("chain_id"), - "genesis_block": { - "height": block.get("height"), - "hash": block.get("hash"), - "parent_hash": block.get("parent_hash"), - "proposer": block.get("proposer"), - "timestamp": block.get("timestamp"), - "tx_count": block.get("tx_count") - }, - "allocations": [ - { - "address": alloc.get("address"), - "balance": alloc.get("balance"), - "nonce": alloc.get("nonce") - } - for alloc in allocations[:5] # Show first 5 - ], - "total_allocations": len(allocations) - }, ctx.obj.get("output_format", "table")) - - except Exception as e: - error(f"Failed to read genesis info: {e}") diff --git a/cli/src/aitbc_cli/commands/gpu_marketplace.py b/cli/src/aitbc_cli/commands/gpu_marketplace.py deleted file mode 100644 index dbc9618e..00000000 --- a/cli/src/aitbc_cli/commands/gpu_marketplace.py +++ /dev/null @@ -1,672 +0,0 @@ -""" -GPU Marketplace CLI Commands -Commands for bidding on and offering GPU power in the AITBC island marketplace -""" - -import click -import json -import hashlib -import socket -import os -import asyncio -from datetime import datetime -from decimal import Decimal -from typing import Optional, List -from ..utils import output, error, success, info, warning -from ..utils.island_credentials import ( - load_island_credentials, get_rpc_endpoint, get_chain_id, - get_island_id, get_island_name, validate_credentials -) -from ..config import get_config - -# Import shared modules -from aitbc import get_logger, AITBCHTTPClient, NetworkError - -# Initialize logger -logger = get_logger(__name__) - - -def safe_load_credentials(): - """Load island credentials with graceful error handling""" - try: - return load_island_credentials() - except FileNotFoundError as e: - error(f"Island credentials not found: {e}") - error("Run 'aitbc node island join' to join an island first") - return None - - -@click.group() -def gpu(): - """GPU marketplace commands for bidding and offering GPU power""" - pass - - -@gpu.command() -@click.argument('gpu_count', type=int) -@click.argument('price_per_gpu', type=float) -@click.argument('duration_hours', type=int) -@click.option('--specs', help='GPU specifications (JSON string)') -@click.option('--description', help='Description of the GPU offer') -@click.pass_context -def offer(ctx, gpu_count: int, price_per_gpu: float, duration_hours: int, specs: Optional[str], description: Optional[str]): - """Offer GPU power for sale in the marketplace""" - try: - # Load CLI config - config = get_config() - - # Load island credentials - credentials = safe_load_credentials() - if not credentials: - return - chain_id = get_chain_id() - island_id = get_island_id() - - # Get provider node ID - hostname = socket.gethostname() - local_address = socket.gethostbyname(hostname) - p2p_port = credentials.get('credentials', {}).get('p2p_port', 8001) - - # Get public key for node ID generation - keystore_path = '/var/lib/aitbc/keystore/validator_keys.json' - if os.path.exists(keystore_path): - with open(keystore_path, 'r') as f: - keys = json.load(f) - public_key_pem = None - for key_id, key_data in keys.items(): - public_key_pem = key_data.get('public_key_pem') - break - if public_key_pem: - content = f"{hostname}:{local_address}:{p2p_port}:{public_key_pem}" - provider_node_id = hashlib.sha256(content.encode()).hexdigest() - else: - error("No public key found in keystore") - raise click.Abort() - else: - error(f"Keystore not found at {keystore_path}") - raise click.Abort() - - # Calculate total price - total_price = price_per_gpu * gpu_count * duration_hours - - # Generate offer ID - offer_id = f"gpu_offer_{datetime.now().strftime('%Y%m%d%H%M%S')}_{hashlib.sha256(f'{provider_node_id}{gpu_count}{price_per_gpu}'.encode()).hexdigest()[:8]}" - - # Parse specifications - gpu_specs = {} - if specs: - try: - gpu_specs = json.loads(specs) - except json.JSONDecodeError: - error("Invalid JSON specifications") - raise click.Abort() - - # Create offer transaction - offer_data = { - 'type': 'gpu_marketplace', - 'action': 'offer', - 'offer_id': offer_id, - 'provider_node_id': provider_node_id, - 'gpu_count': gpu_count, - 'price_per_gpu': float(price_per_gpu), - 'duration_hours': duration_hours, - 'total_price': float(total_price), - 'status': 'active', - 'specs': gpu_specs, - 'description': description or f"{gpu_count} GPUs for {duration_hours} hours", - 'island_id': island_id, - 'chain_id': chain_id, - 'created_at': datetime.now().isoformat() - } - - # Submit transaction to GPU service - try: - http_client = AITBCHTTPClient(base_url=config.gpu_service_url, timeout=10) - result = http_client.post("/v1/transactions", json=offer_data) - success(f"GPU offer created successfully!") - success(f"Offer ID: {offer_id}") - success(f"Total Price: {total_price:.2f} AIT") - - offer_info = { - "Offer ID": offer_id, - "GPU Count": gpu_count, - "Price per GPU": f"{price_per_gpu:.4f} AIT/hour", - "Duration": f"{duration_hours} hours", - "Total Price": f"{total_price:.2f} AIT", - "Status": "active", - "Provider Node": provider_node_id[:16] + "...", - "Island": island_id[:16] + "..." - } - - output(offer_info, ctx.obj.get('output_format', 'table')) - except NetworkError as e: - error(f"Network error submitting transaction: {e}") - raise click.Abort() - - except Exception as e: - error(f"Error creating GPU offer: {str(e)}") - raise click.Abort() - - -@gpu.command() -@click.argument('gpu_count', type=int) -@click.argument('max_price', type=float) -@click.argument('duration_hours', type=int) -@click.option('--specs', help='Required GPU specifications (JSON string)') -@click.pass_context -def bid(ctx, gpu_count: int, max_price: float, duration_hours: int, specs: Optional[str]): - """Bid on GPU power in the marketplace""" - try: - # Load CLI config - config = get_config() - - # Load island credentials - credentials = safe_load_credentials() - if not credentials: - return - chain_id = get_chain_id() - island_id = get_island_id() - - # Get bidder node ID - hostname = socket.gethostname() - local_address = socket.gethostbyname(hostname) - p2p_port = credentials.get('credentials', {}).get('p2p_port', 8001) - - # Get public key for node ID generation - keystore_path = '/var/lib/aitbc/keystore/validator_keys.json' - if os.path.exists(keystore_path): - with open(keystore_path, 'r') as f: - keys = json.load(f) - public_key_pem = None - for key_id, key_data in keys.items(): - public_key_pem = key_data.get('public_key_pem') - break - if public_key_pem: - content = f"{hostname}:{local_address}:{p2p_port}:{public_key_pem}" - bidder_node_id = hashlib.sha256(content.encode()).hexdigest() - else: - error("No public key found in keystore") - raise click.Abort() - else: - error(f"Keystore not found at {keystore_path}") - raise click.Abort() - - # Calculate max total price - max_total_price = max_price * gpu_count * duration_hours - - # Generate bid ID - bid_id = f"gpu_bid_{datetime.now().strftime('%Y%m%d%H%M%S')}_{hashlib.sha256(f'{bidder_node_id}{gpu_count}{max_price}'.encode()).hexdigest()[:8]}" - - # Parse specifications - gpu_specs = {} - if specs: - try: - gpu_specs = json.loads(specs) - except json.JSONDecodeError: - error("Invalid JSON specifications") - raise click.Abort() - - # Create bid transaction - bid_data = { - 'type': 'gpu_marketplace', - 'action': 'bid', - 'bid_id': bid_id, - 'bidder_node_id': bidder_node_id, - 'gpu_count': gpu_count, - 'max_price_per_gpu': float(max_price), - 'duration_hours': duration_hours, - 'max_total_price': float(max_total_price), - 'status': 'pending', - 'specs': gpu_specs, - 'island_id': island_id, - 'chain_id': chain_id, - 'created_at': datetime.now().isoformat() - } - - # Submit transaction to GPU service - try: - http_client = AITBCHTTPClient(base_url=config.gpu_service_url, timeout=10) - result = http_client.post("/v1/transactions", json=bid_data) - success(f"GPU bid created successfully!") - success(f"Bid ID: {bid_id}") - success(f"Max Total Price: {max_total_price:.2f} AIT") - - bid_info = { - "Bid ID": bid_id, - "GPU Count": gpu_count, - "Max Price per GPU": f"{max_price:.4f} AIT/hour", - "Duration": f"{duration_hours} hours", - "Max Total Price": f"{max_total_price:.2f} AIT", - "Status": "pending", - "Bidder Node": bidder_node_id[:16] + "...", - "Island": island_id[:16] + "..." - } - - output(bid_info, ctx.obj.get('output_format', 'table')) - except NetworkError as e: - error(f"Network error submitting transaction: {e}") - raise click.Abort() - - except Exception as e: - error(f"Error creating GPU bid: {str(e)}") - raise click.Abort() - - -@gpu.command() -@click.option('--provider', help='Filter by provider node ID') -@click.option('--status', help='Filter by status (active, pending, accepted, completed, cancelled)') -@click.option('--type', type=click.Choice(['offer', 'bid', 'all']), default='all', help='Filter by type') -@click.pass_context -def list(ctx, provider: Optional[str], status: Optional[str], type: str): - """List GPU marketplace offers and bids""" - try: - # Load CLI config - config = get_config() - - # Load island credentials - credentials = safe_load_credentials() - if not credentials: - return - island_id = get_island_id() - - # Query GPU service for GPU marketplace transactions - try: - params = { - 'transaction_type': 'gpu_marketplace', - 'island_id': island_id - } - if provider: - params['provider_node_id'] = provider - if status: - params['status'] = status - if type != 'all': - params['action'] = type - - http_client = AITBCHTTPClient(base_url=config.gpu_service_url, timeout=10) - transactions = http_client.get("/v1/transactions", params=params) - - if not transactions: - info("No GPU marketplace transactions found") - return - - # Format output - market_data = [] - for tx in transactions: - action = tx.get('action') - if action == 'offer': - market_data.append({ - "ID": tx.get('offer_id', tx.get('transaction_id', 'N/A'))[:20] + "...", - "Type": "OFFER", - "GPU Count": tx.get('gpu_count'), - "Price": f"{tx.get('price_per_gpu', 0):.4f} AIT/h", - "Duration": f"{tx.get('duration_hours')}h", - "Total": f"{tx.get('total_price', 0):.2f} AIT", - "Status": tx.get('status'), - "Provider": tx.get('provider_node_id', '')[:16] + "...", - "Created": tx.get('created_at', '')[:19] - }) - elif action == 'bid': - market_data.append({ - "ID": tx.get('bid_id', tx.get('transaction_id', 'N/A'))[:20] + "...", - "Type": "BID", - "GPU Count": tx.get('gpu_count'), - "Max Price": f"{tx.get('max_price_per_gpu', 0):.4f} AIT/h", - "Duration": f"{tx.get('duration_hours')}h", - "Max Total": f"{tx.get('max_total_price', 0):.2f} AIT", - "Status": tx.get('status'), - "Bidder": tx.get('bidder_node_id', '')[:16] + "...", - "Created": tx.get('created_at', '')[:19] - }) - - output(market_data, ctx.obj.get('output_format', 'table'), title=f"GPU Marketplace ({island_id[:16]}...)") - except NetworkError as e: - error(f"Network error querying blockchain: {e}") - raise click.Abort() - - except Exception as e: - error(f"Error listing GPU marketplace: {str(e)}") - raise click.Abort() - - -@gpu.command() -@click.argument('order_id') -@click.pass_context -def cancel(ctx, order_id: str): - """Cancel a GPU offer or bid""" - try: - # Load CLI config - config = get_config() - - # Load island credentials - credentials = safe_load_credentials() - if not credentials: - return - chain_id = get_chain_id() - island_id = get_island_id() - - # Get local node ID - hostname = socket.gethostname() - local_address = socket.gethostbyname(hostname) - p2p_port = credentials.get('credentials', {}).get('p2p_port', 8001) - - keystore_path = '/var/lib/aitbc/keystore/validator_keys.json' - if os.path.exists(keystore_path): - with open(keystore_path, 'r') as f: - keys = json.load(f) - public_key_pem = None - for key_id, key_data in keys.items(): - public_key_pem = key_data.get('public_key_pem') - break - if public_key_pem: - content = f"{hostname}:{local_address}:{p2p_port}:{public_key_pem}" - local_node_id = hashlib.sha256(content.encode()).hexdigest() - - # Determine if it's an offer or bid - if order_id.startswith('gpu_offer'): - action = 'cancel' - elif order_id.startswith('gpu_bid'): - action = 'cancel' - else: - error("Invalid order ID format. Must start with 'gpu_offer' or 'gpu_bid'") - raise click.Abort() - - # Create cancel transaction - cancel_data = { - 'type': 'gpu_marketplace', - 'action': action, - 'order_id': order_id, - 'node_id': local_node_id, - 'status': 'cancelled', - 'cancelled_at': datetime.now().isoformat(), - 'island_id': island_id, - 'chain_id': chain_id - } - - # Submit transaction to GPU service - try: - http_client = AITBCHTTPClient(base_url=config.gpu_service_url, timeout=10) - result = http_client.post("/v1/transactions", json=cancel_data) - success(f"Order {order_id} cancelled successfully!") - except NetworkError as e: - error(f"Network error submitting transaction: {e}") - raise click.Abort() - - except Exception as e: - error(f"Error cancelling order: {str(e)}") - raise click.Abort() - - -@gpu.command() -@click.argument('bid_id') -@click.pass_context -def accept(ctx, bid_id: str): - """Accept a GPU bid (provider only)""" - try: - # Load CLI config - config = get_config() - - # Load island credentials - credentials = safe_load_credentials() - if not credentials: - return - chain_id = get_chain_id() - island_id = get_island_id() - - # Get provider node ID - hostname = socket.gethostname() - local_address = socket.gethostbyname(hostname) - p2p_port = credentials.get('credentials', {}).get('p2p_port', 8001) - - keystore_path = '/var/lib/aitbc/keystore/validator_keys.json' - if os.path.exists(keystore_path): - with open(keystore_path, 'r') as f: - keys = json.load(f) - public_key_pem = None - for key_id, key_data in keys.items(): - public_key_pem = key_data.get('public_key_pem') - break - if public_key_pem: - content = f"{hostname}:{local_address}:{p2p_port}:{public_key_pem}" - provider_node_id = hashlib.sha256(content.encode()).hexdigest() - else: - error("No public key found in keystore") - raise click.Abort() - else: - error(f"Keystore not found at {keystore_path}") - raise click.Abort() - - # Create accept transaction - accept_data = { - 'type': 'gpu_marketplace', - 'action': 'accept', - 'bid_id': bid_id, - 'provider_node_id': provider_node_id, - 'status': 'accepted', - 'accepted_at': datetime.now().isoformat(), - 'island_id': island_id, - 'chain_id': chain_id - } - - # Submit transaction to GPU service - try: - http_client = AITBCHTTPClient(base_url=config.gpu_service_url, timeout=10) - result = http_client.post("/v1/transactions", json=accept_data) - success(f"Bid {bid_id} accepted successfully!") - except NetworkError as e: - error(f"Network error submitting transaction: {e}") - raise click.Abort() - - except Exception as e: - error(f"Error accepting bid: {str(e)}") - raise click.Abort() - - -@gpu.command() -@click.argument('order_id') -@click.pass_context -def status(ctx, order_id: str): - """Check the status of a GPU order""" - try: - # Load CLI config - config = get_config() - - # Load island credentials - credentials = safe_load_credentials() - if not credentials: - return - island_id = get_island_id() - - # Query GPU service for the order - try: - params = { - 'transaction_type': 'gpu_marketplace', - 'island_id': island_id, - 'order_id': order_id - } - - http_client = AITBCHTTPClient(base_url=config.gpu_service_url, timeout=10) - transactions = http_client.get("/v1/transactions", params=params) - - if not transactions: - error(f"Order {order_id} not found") - raise click.Abort() - - tx = transactions[0] - action = tx.get('action') - - order_info = { - "Order ID": order_id, - "Type": action.upper(), - "Status": tx.get('status'), - "Created": tx.get('created_at'), - } - - if action == 'offer': - order_info.update({ - "GPU Count": tx.get('gpu_count'), - "Price per GPU": f"{tx.get('price_per_gpu', 0):.4f} AIT/h", - "Duration": f"{tx.get('duration_hours')}h", - "Total Price": f"{tx.get('total_price', 0):.2f} AIT", - "Provider": tx.get('provider_node_id', '')[:16] + "..." - }) - elif action == 'bid': - order_info.update({ - "GPU Count": tx.get('gpu_count'), - "Max Price": f"{tx.get('max_price_per_gpu', 0):.4f} AIT/h", - "Duration": f"{tx.get('duration_hours')}h", - "Max Total": f"{tx.get('max_total_price', 0):.2f} AIT", - "Bidder": tx.get('bidder_node_id', '')[:16] + "..." - }) - - if 'accepted_at' in tx: - order_info["Accepted"] = tx['accepted_at'] - if 'cancelled_at' in tx: - order_info["Cancelled"] = tx['cancelled_at'] - - output(order_info, ctx.obj.get('output_format', 'table'), title=f"Order Status: {order_id}") - except NetworkError as e: - error(f"Network error querying blockchain: {e}") - raise click.Abort() - - except Exception as e: - error(f"Error checking order status: {str(e)}") - raise click.Abort() - - -@gpu.command() -@click.pass_context -def match(ctx): - """Match GPU bids with offers (price discovery)""" - try: - # Load CLI config - config = get_config() - - # Load island credentials - credentials = safe_load_credentials() - if not credentials: - return - island_id = get_island_id() - - # Query GPU service for open offers and bids - try: - params = { - 'transaction_type': 'gpu_marketplace', - 'island_id': island_id, - 'status': 'active' - } - - http_client = AITBCHTTPClient(base_url=config.gpu_service_url, timeout=10) - transactions = http_client.get("/v1/transactions", params=params) - - # Separate offers and bids - offers = [] - bids = [] - - for tx in transactions: - if tx.get('action') == 'offer': - offers.append(tx) - elif tx.get('action') == 'bid': - bids.append(tx) - - if not offers or not bids: - info("No active offers or bids to match") - return - - # Sort offers by price (lowest first) - offers.sort(key=lambda x: x.get('price_per_gpu', float('inf'))) - # Sort bids by price (highest first) - bids.sort(key=lambda x: x.get('max_price_per_gpu', 0), reverse=True) - - # Match bids with offers - matches = [] - for bid in bids: - for offer in offers: - # Check if bid price >= offer price - if bid.get('max_price_per_gpu', 0) >= offer.get('price_per_gpu', float('inf')): - # Check if GPU count matches - if bid.get('gpu_count') == offer.get('gpu_count'): - # Check if duration matches - if bid.get('duration_hours') == offer.get('duration_hours'): - # Create match transaction - match_data = { - 'type': 'gpu_marketplace', - 'action': 'match', - 'bid_id': bid.get('bid_id'), - 'offer_id': offer.get('offer_id'), - 'bidder_node_id': bid.get('bidder_node_id'), - 'provider_node_id': offer.get('provider_node_id'), - 'gpu_count': bid.get('gpu_count'), - 'matched_price': offer.get('price_per_gpu'), - 'duration_hours': bid.get('duration_hours'), - 'total_price': offer.get('total_price'), - 'status': 'matched', - 'matched_at': datetime.now().isoformat(), - 'island_id': island_id, - 'chain_id': get_chain_id() - } - - # Submit match transaction - match_result = http_client.post("/v1/transactions", json=match_data) - matches.append({ - "Bid ID": bid.get('bid_id')[:16] + "...", - "Offer ID": offer.get('offer_id')[:16] + "...", - "GPU Count": bid.get('gpu_count'), - "Matched Price": f"{offer.get('price_per_gpu', 0):.4f} AIT/h", - "Total Price": f"{offer.get('total_price', 0):.2f} AIT", - "Duration": f"{bid.get('duration_hours')}h" - }) - - if matches: - success(f"Matched {len(matches)} GPU orders!") - output(matches, ctx.obj.get('output_format', 'table'), title="GPU Order Matches") - else: - info("No matching orders found") - except NetworkError as e: - error(f"Network error querying blockchain: {e}") - raise click.Abort() - - except Exception as e: - error(f"Error matching orders: {str(e)}") - raise click.Abort() - - -@gpu.command() -@click.pass_context -def providers(ctx): - """Query island members for GPU providers""" - try: - # Load island credentials - credentials = safe_load_credentials() - if not credentials: - return - island_id = get_island_id() - - # Load island members from credentials - members = credentials.get('members', []) - - if not members: - warning("No island members found in credentials") - return - - # Query each member for GPU availability via P2P - info(f"Querying {len(members)} island members for GPU availability...") - - # For now, display the members - # In a full implementation, this would use P2P network to query each member - provider_data = [] - for member in members: - provider_data.append({ - "Node ID": member.get('node_id', '')[:16] + "...", - "Address": member.get('address', 'N/A'), - "Port": member.get('port', 'N/A'), - "Is Hub": member.get('is_hub', False), - "Public Address": member.get('public_address', 'N/A'), - "Public Port": member.get('public_port', 'N/A') - }) - - output(provider_data, ctx.obj.get('output_format', 'table'), title=f"Island Members ({island_id[:16]}...)") - info("Note: GPU availability query via P2P network to be implemented") - - except Exception as e: - error(f"Error querying GPU providers: {str(e)}") - raise click.Abort() diff --git a/cli/src/aitbc_cli/commands/hermes.py b/cli/src/aitbc_cli/commands/hermes.py deleted file mode 100644 index f016b926..00000000 --- a/cli/src/aitbc_cli/commands/hermes.py +++ /dev/null @@ -1,158 +0,0 @@ -""" -Hermes training commands for AITBC CLI -""" - -import json -import time -import os -import subprocess -import datetime -from pathlib import Path -from typing import Optional - -import click - -from ..utils import error, success - - -@click.group() -def hermes(): - """Hermes training operations commands""" - pass - - -@hermes.command() -@click.option('--agent-id', required=True, help='Agent ID') -@click.option('--training-type', required=True, help='Type of training') -@click.option('--dataset', help='Dataset to use') -@click.option('--epochs', type=int, default=100, help='Number of training epochs') -@click.option('--batch-size', type=int, default=32, help='Batch size') -@click.option('--training-data', help='Path to training data JSON file') -@click.option('--stage', help='Training stage') -def train(agent_id: str, training_type: str, dataset: Optional[str], epochs: int, batch_size: int, training_data: Optional[str], stage: Optional[str]): - """Start Hermes training for an agent""" - if training_data: - if not os.path.exists(training_data): - error(f"Training data file not found: {training_data}") - return - - try: - with open(training_data, 'r') as f: - training_config = json.load(f) - - # Validate training data matches stage - if stage and training_config.get('stage') != stage: - error(f"Training data stage mismatch: expected {stage}, got {training_config.get('stage')}") - return - - # Initialize logging - log_dir = "/var/log/aitbc/agent-training" - os.makedirs(log_dir, exist_ok=True) - log_file = f"{log_dir}/agent_{agent_id}_{stage}_{int(time.time())}.log" - - # Execute training operations - operations = training_config.get('training_data', {}).get('operations', []) - completed_ops = 0 - failed_ops = 0 - - success(f"Starting training for agent {agent_id}") - success(f"Operations to execute: {len(operations)}") - - for i, op in enumerate(operations, 1): - operation = op.get('operation') - parameters = op.get('parameters', {}) - - log_entry = { - "timestamp": datetime.datetime.now().isoformat(), - "agent_id": agent_id, - "stage": stage, - "operation": operation, - "prompt": { - "parameters": parameters, - "expected_result": op.get('expected_result') - } - } - - # Execute training via hermes agent - start_time = time.time() - try: - prompt_message = f"Execute AITBC CLI command: {operation}" - if parameters: - prompt_message += f" with parameters: {json.dumps(parameters)}" - - cmd = ["hermes", "agent", "--message", prompt_message, "--agent", "main"] - - result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) - - duration_ms = int((time.time() - start_time) * 1000) - - if result.returncode == 0: - reply = { - "status": "completed", - "result": result.stdout.strip() if result.stdout else "Command executed successfully", - "cli_output": result.stdout.strip() - } - log_entry["status"] = "completed" - completed_ops += 1 - success(f"Operation {i}/{len(operations)}: {operation} - completed ({duration_ms}ms)") - else: - reply = { - "status": "error", - "error": result.stderr.strip() if result.stderr else "Command failed", - "cli_output": result.stdout.strip(), - "cli_error": result.stderr.strip() - } - log_entry["status"] = "failed" - failed_ops += 1 - error(f"Operation {i}/{len(operations)}: {operation} - failed") - - log_entry["reply"] = reply - log_entry["duration_ms"] = duration_ms - - # Write log entry - with open(log_file, 'a') as f: - f.write(json.dumps(log_entry) + "\n") - - except subprocess.TimeoutExpired: - duration_ms = int((time.time() - start_time) * 1000) - reply = { - "status": "error", - "error": "Command timed out after 30 seconds" - } - log_entry["status"] = "failed" - log_entry["reply"] = reply - log_entry["duration_ms"] = duration_ms - failed_ops += 1 - error(f"Operation {i}/{len(operations)}: {operation} - timed out") - - with open(log_file, 'a') as f: - f.write(json.dumps(log_entry) + "\n") - except Exception as e: - error(f"Operation {i}/{len(operations)}: {operation} - exception: {e}") - failed_ops += 1 - - success(f"Training completed: {completed_ops}/{len(operations)} successful") - success(f"Log file: {log_file}") - - except Exception as e: - error(f"Error loading training data: {e}") - else: - success(f"Start {training_type} training for agent {agent_id}") - success(f"Epochs: {epochs}, Batch size: {batch_size}") - - -@hermes.command() -@click.option('--agent-id', help='Agent ID') -@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format') -def status(agent_id: Optional[str], format: str): - """Get Hermes training status""" - success(f"Get Hermes training status for agent {agent_id}") - # TODO: Implement actual status check from coordinator API - - -@hermes.command() -@click.option('--agent-id', help='Agent ID') -def stop(agent_id: Optional[str]): - """Stop Hermes training""" - success(f"Stop Hermes training for agent {agent_id}") - # TODO: Implement actual stop command via coordinator API diff --git a/cli/src/aitbc_cli/commands/marketplace_cmd.py b/cli/src/aitbc_cli/commands/marketplace_cmd.py deleted file mode 100755 index 900b350e..00000000 --- a/cli/src/aitbc_cli/commands/marketplace_cmd.py +++ /dev/null @@ -1,519 +0,0 @@ -"""Global chain marketplace commands for AITBC CLI""" - -import click -import asyncio -import json -from decimal import Decimal -from datetime import datetime -from typing import Optional -from ..core.config import load_multichain_config -from ..core.marketplace import ( - GlobalChainMarketplace, ChainType, MarketplaceStatus, - TransactionStatus -) -from ..utils import output, error, success -from ..config import get_config - -@click.group() -@click.option("--chain-id", help="Chain ID for multichain operations (e.g., ait-mainnet, ait-devnet)") -@click.pass_context -def marketplace(ctx, chain_id: Optional[str]): - """Global chain marketplace commands""" - ctx.ensure_object(dict) - - # Handle chain_id with auto-detection - from ..utils.chain_id import get_chain_id - config = load_multichain_config() - default_rpc_url = config.blockchain_rpc_url if hasattr(config, 'blockchain_rpc_url') else 'http://localhost:8006' - ctx.obj['chain_id'] = get_chain_id(default_rpc_url, override=chain_id) - -@marketplace.command() -@click.argument('chain_id') -@click.argument('chain_name') -@click.argument('chain_type') -@click.argument('description') -@click.argument('seller_id') -@click.argument('price') -@click.option('--currency', default='ETH', help='Currency for pricing') -@click.option('--specs', help='Chain specifications (JSON string)') -@click.option('--metadata', help='Additional metadata (JSON string)') -@click.pass_context -def list(ctx, chain_id, chain_name, chain_type, description, seller_id, price, currency, specs, metadata): - """List a chain for sale in the marketplace""" - try: - config = get_config() - from aitbc import AITBCHTTPClient - - # Parse chain type - try: - chain_type_enum = ChainType(chain_type) - except ValueError: - error(f"Invalid chain type: {chain_type}") - error(f"Valid types: {[t.value for t in ChainType]}") - raise click.Abort() - - # Parse price - try: - price_decimal = Decimal(price) - except (ValueError, TypeError): - error("Invalid price format") - raise click.Abort() - - # Parse specifications - chain_specs = {} - if specs: - try: - chain_specs = json.loads(specs) - except json.JSONDecodeError: - error("Invalid JSON specifications") - raise click.Abort() - - # Parse metadata - metadata_dict = {} - if metadata: - try: - metadata_dict = json.loads(metadata) - except json.JSONDecodeError: - error("Invalid JSON metadata") - raise click.Abort() - - # Create listing transaction - listing_id = f"chain_listing_{datetime.now().strftime('%Y%m%d%H%M%S')}" - listing_data = { - 'type': 'marketplace', - 'action': 'list', - 'listing_id': listing_id, - 'chain_id': chain_id, - 'chain_name': chain_name, - 'chain_type': chain_type, - 'description': description, - 'seller_id': seller_id, - 'price': float(price), - 'currency': currency, - 'specs': chain_specs, - 'metadata': metadata_dict, - 'status': 'active', - 'created_at': datetime.now().isoformat() - } - - # Submit transaction to marketplace service - try: - http_client = AITBCHTTPClient(base_url=config.marketplace_service_url, timeout=10) - result = http_client.post("/v1/transactions", json=listing_data) - success(f"Chain listed successfully! Listing ID: {listing_id}") - - listing_info = { - "Listing ID": listing_id, - "Chain ID": chain_id, - "Chain Name": chain_name, - "Type": chain_type, - "Price": f"{price} {currency}", - "Seller": seller_id, - "Status": "active", - "Created": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - - output(listing_info, ctx.obj.get('output_format', 'table')) - except Exception as e: - error(f"Error submitting transaction: {e}") - raise click.Abort() - - except Exception as e: - error(f"Error creating listing: {str(e)}") - raise click.Abort() - -@marketplace.command() -@click.argument('listing_id') -@click.argument('buyer_id') -@click.option('--payment', default='crypto', help='Payment method') -@click.pass_context -def buy(ctx, listing_id, buyer_id, payment): - """Purchase a chain from the marketplace""" - try: - config = load_multichain_config() - marketplace = GlobalChainMarketplace(config) - - # Purchase chain - transaction_id = asyncio.run(marketplace.purchase_chain(listing_id, buyer_id, payment)) - - if transaction_id: - success(f"Purchase initiated! Transaction ID: {transaction_id}") - - transaction_data = { - "Transaction ID": transaction_id, - "Listing ID": listing_id, - "Buyer": buyer_id, - "Payment Method": payment, - "Status": "pending", - "Created": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - - output(transaction_data, ctx.obj.get('output_format', 'table')) - else: - error("Failed to purchase chain") - raise click.Abort() - - except Exception as e: - error(f"Error purchasing chain: {str(e)}") - raise click.Abort() - -@marketplace.command() -@click.argument('transaction_id') -@click.argument('transaction_hash') -@click.pass_context -def complete(ctx, transaction_id, transaction_hash): - """Complete a marketplace transaction""" - try: - config = load_multichain_config() - marketplace = GlobalChainMarketplace(config) - - # Complete transaction - success = asyncio.run(marketplace.complete_transaction(transaction_id, transaction_hash)) - - if success: - success(f"Transaction {transaction_id} completed successfully!") - - transaction_data = { - "Transaction ID": transaction_id, - "Transaction Hash": transaction_hash, - "Status": "completed", - "Completed": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - - output(transaction_data, ctx.obj.get('output_format', 'table')) - else: - error(f"Failed to complete transaction {transaction_id}") - raise click.Abort() - - except Exception as e: - error(f"Error completing transaction: {str(e)}") - raise click.Abort() - -@marketplace.command() -@click.option('--type', help='Filter by chain type') -@click.option('--min-price', help='Minimum price') -@click.option('--max-price', help='Maximum price') -@click.option('--seller', help='Filter by seller ID') -@click.option('--status', help='Filter by listing status') -@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format') -@click.pass_context -def search(ctx, type, min_price, max_price, seller, status, format): - """Search chain listings in the marketplace""" - try: - config = load_multichain_config() - marketplace = GlobalChainMarketplace(config) - - # Parse filters - chain_type = None - if type: - try: - chain_type = ChainType(type) - except ValueError: - error(f"Invalid chain type: {type}") - raise click.Abort() - - min_price_dec = None - if min_price: - try: - min_price_dec = Decimal(min_price) - except (ValueError, TypeError): - error("Invalid minimum price format") - raise click.Abort() - - max_price_dec = None - if max_price: - try: - max_price_dec = Decimal(max_price) - except (ValueError, TypeError): - error("Invalid maximum price format") - raise click.Abort() - - listing_status = None - if status: - try: - listing_status = MarketplaceStatus(status) - except ValueError: - error(f"Invalid status: {status}") - raise click.Abort() - - # Search listings - listings = asyncio.run(marketplace.search_listings( - chain_type, min_price_dec, max_price_dec, seller, listing_status - )) - - if not listings: - output("No listings found matching your criteria", ctx.obj.get('output_format', 'table')) - return - - # Format output - listing_data = [ - { - "Listing ID": listing.listing_id, - "Chain ID": listing.chain_id, - "Chain Name": listing.chain_name, - "Type": listing.chain_type.value, - "Price": f"{listing.price} {listing.currency}", - "Seller": listing.seller_id, - "Status": listing.status.value, - "Created": listing.created_at.strftime("%Y-%m-%d %H:%M:%S"), - "Expires": listing.expires_at.strftime("%Y-%m-%d %H:%M:%S") - } - for listing in listings - ] - - output(listing_data, ctx.obj.get('output_format', format), title="Marketplace Listings") - - except Exception as e: - error(f"Error searching listings: {str(e)}") - raise click.Abort() - -@marketplace.command() -@click.argument('chain_id') -@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format') -@click.pass_context -def economy(ctx, chain_id, format): - """Get economic metrics for a specific chain""" - try: - config = load_multichain_config() - marketplace = GlobalChainMarketplace(config) - - # Get chain economy - economy = asyncio.run(marketplace.get_chain_economy(chain_id)) - - if not economy: - error(f"No economic data available for chain {chain_id}") - raise click.Abort() - - # Format output - economy_data = [ - {"Metric": "Chain ID", "Value": economy.chain_id}, - {"Metric": "Total Value Locked", "Value": f"{economy.total_value_locked} ETH"}, - {"Metric": "Daily Volume", "Value": f"{economy.daily_volume} ETH"}, - {"Metric": "Market Cap", "Value": f"{economy.market_cap} ETH"}, - {"Metric": "Transaction Count", "Value": economy.transaction_count}, - {"Metric": "Active Users", "Value": economy.active_users}, - {"Metric": "Agent Count", "Value": economy.agent_count}, - {"Metric": "Governance Tokens", "Value": f"{economy.governance_tokens}"}, - {"Metric": "Staking Rewards", "Value": f"{economy.staking_rewards}"}, - {"Metric": "Last Updated", "Value": economy.last_updated.strftime("%Y-%m-%d %H:%M:%S")} - ] - - output(economy_data, ctx.obj.get('output_format', format), title=f"Chain Economy: {chain_id}") - - except Exception as e: - error(f"Error getting chain economy: {str(e)}") - raise click.Abort() - -@marketplace.command() -@click.argument('user_id') -@click.option('--role', type=click.Choice(['buyer', 'seller', 'both']), default='both', help='User role') -@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format') -@click.pass_context -def transactions(ctx, user_id, role, format): - """Get transactions for a specific user""" - try: - config = load_multichain_config() - marketplace = GlobalChainMarketplace(config) - - # Get user transactions - transactions = asyncio.run(marketplace.get_user_transactions(user_id, role)) - - if not transactions: - output(f"No transactions found for user {user_id}", ctx.obj.get('output_format', 'table')) - return - - # Format output - transaction_data = [ - { - "Transaction ID": transaction.transaction_id, - "Listing ID": transaction.listing_id, - "Chain ID": transaction.chain_id, - "Price": f"{transaction.price} {transaction.currency}", - "Role": "buyer" if transaction.buyer_id == user_id else "seller", - "Counterparty": transaction.seller_id if transaction.buyer_id == user_id else transaction.buyer_id, - "Status": transaction.status.value, - "Created": transaction.created_at.strftime("%Y-%m-%d %H:%M:%S"), - "Completed": transaction.completed_at.strftime("%Y-%m-%d %H:%M:%S") if transaction.completed_at else "N/A" - } - for transaction in transactions - ] - - output(transaction_data, ctx.obj.get('output_format', format), title=f"Transactions for {user_id}") - - except Exception as e: - error(f"Error getting user transactions: {str(e)}") - raise click.Abort() - -@marketplace.command() -@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format') -@click.pass_context -def overview(ctx, format): - """Get comprehensive marketplace overview""" - try: - config = load_multichain_config() - marketplace = GlobalChainMarketplace(config) - - # Get marketplace overview - overview = asyncio.run(marketplace.get_marketplace_overview()) - - if not overview: - error("No marketplace data available") - raise click.Abort() - - # Marketplace metrics - if "marketplace_metrics" in overview: - metrics = overview["marketplace_metrics"] - metrics_data = [ - {"Metric": "Total Listings", "Value": metrics["total_listings"]}, - {"Metric": "Active Listings", "Value": metrics["active_listings"]}, - {"Metric": "Total Transactions", "Value": metrics["total_transactions"]}, - {"Metric": "Total Volume", "Value": f"{metrics['total_volume']} ETH"}, - {"Metric": "Average Price", "Value": f"{metrics['average_price']} ETH"}, - {"Metric": "Market Sentiment", "Value": f"{metrics['market_sentiment']:.2f}"} - ] - - output(metrics_data, ctx.obj.get('output_format', format), title="Marketplace Metrics") - - # Volume 24h - if "volume_24h" in overview: - volume_data = [ - {"Metric": "24h Volume", "Value": f"{overview['volume_24h']} ETH"} - ] - - output(volume_data, ctx.obj.get('output_format', format), title="24-Hour Volume") - - # Top performing chains - if "top_performing_chains" in overview: - chains = overview["top_performing_chains"] - if chains: - chain_data = [ - { - "Chain ID": chain["chain_id"], - "Volume": f"{chain['volume']} ETH", - "Transactions": chain["transactions"] - } - for chain in chains[:5] # Top 5 - ] - - output(chain_data, ctx.obj.get('output_format', format), title="Top Performing Chains") - - # Chain types distribution - if "chain_types_distribution" in overview: - distribution = overview["chain_types_distribution"] - if distribution: - dist_data = [ - {"Chain Type": chain_type, "Count": count} - for chain_type, count in distribution.items() - ] - - output(dist_data, ctx.obj.get('output_format', format), title="Chain Types Distribution") - - # User activity - if "user_activity" in overview: - activity = overview["user_activity"] - activity_data = [ - {"Metric": "Active Buyers (7d)", "Value": activity["active_buyers_7d"]}, - {"Metric": "Active Sellers (7d)", "Value": activity["active_sellers_7d"]}, - {"Metric": "Total Unique Users", "Value": activity["total_unique_users"]}, - {"Metric": "Average Reputation", "Value": f"{activity['average_reputation']:.3f}"} - ] - - output(activity_data, ctx.obj.get('output_format', format), title="User Activity") - - # Escrow summary - if "escrow_summary" in overview: - escrow = overview["escrow_summary"] - escrow_data = [ - {"Metric": "Active Escrows", "Value": escrow["active_escrows"]}, - {"Metric": "Released Escrows", "Value": escrow["released_escrows"]}, - {"Metric": "Total Escrow Value", "Value": f"{escrow['total_escrow_value']} ETH"}, - {"Metric": "Escrow Fees Collected", "Value": f"{escrow['escrow_fee_collected']} ETH"} - ] - - output(escrow_data, ctx.obj.get('output_format', format), title="Escrow Summary") - - except Exception as e: - error(f"Error getting marketplace overview: {str(e)}") - raise click.Abort() - -@marketplace.command() -@click.option('--realtime', is_flag=True, help='Real-time monitoring') -@click.option('--interval', default=30, help='Update interval in seconds') -@click.pass_context -def monitor(ctx, realtime, interval): - """Monitor marketplace activity""" - try: - config = load_multichain_config() - marketplace = GlobalChainMarketplace(config) - - if realtime: - # Real-time monitoring - from rich.console import Console - from rich.live import Live - from rich.table import Table - import time - - console = Console() - - def generate_monitor_table(): - try: - overview = asyncio.run(marketplace.get_marketplace_overview()) - - table = Table(title=f"Marketplace Monitor - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") - table.add_column("Metric", style="cyan") - table.add_column("Value", style="green") - - if "marketplace_metrics" in overview: - metrics = overview["marketplace_metrics"] - table.add_row("Total Listings", str(metrics["total_listings"])) - table.add_row("Active Listings", str(metrics["active_listings"])) - table.add_row("Total Transactions", str(metrics["total_transactions"])) - table.add_row("Total Volume", f"{metrics['total_volume']} ETH") - table.add_row("Market Sentiment", f"{metrics['market_sentiment']:.2f}") - - if "volume_24h" in overview: - table.add_row("24h Volume", f"{overview['volume_24h']} ETH") - - if "user_activity" in overview: - activity = overview["user_activity"] - table.add_row("Active Users (7d)", str(activity["active_buyers_7d"] + activity["active_sellers_7d"])) - - return table - except Exception as e: - return f"Error getting marketplace data: {e}" - - with Live(generate_monitor_table(), refresh_per_second=1) as live: - try: - while True: - live.update(generate_monitor_table()) - time.sleep(interval) - except KeyboardInterrupt: - console.click.echo("\n[yellow]Monitoring stopped by user[/yellow]") - else: - # Single snapshot - overview = asyncio.run(marketplace.get_marketplace_overview()) - - monitor_data = [] - - if "marketplace_metrics" in overview: - metrics = overview["marketplace_metrics"] - monitor_data.extend([ - {"Metric": "Total Listings", "Value": metrics["total_listings"]}, - {"Metric": "Active Listings", "Value": metrics["active_listings"]}, - {"Metric": "Total Transactions", "Value": metrics["total_transactions"]}, - {"Metric": "Total Volume", "Value": f"{metrics['total_volume']} ETH"}, - {"Metric": "Market Sentiment", "Value": f"{metrics['market_sentiment']:.2f}"} - ]) - - if "volume_24h" in overview: - monitor_data.append({"Metric": "24h Volume", "Value": f"{overview['volume_24h']} ETH"}) - - if "user_activity" in overview: - activity = overview["user_activity"] - monitor_data.append({"Metric": "Active Users (7d)", "Value": activity["active_buyers_7d"] + activity["active_sellers_7d"]}) - - output(monitor_data, ctx.obj.get('output_format', 'table'), title="Marketplace Monitor") - - except Exception as e: - error(f"Error during monitoring: {str(e)}") - raise click.Abort() diff --git a/cli/src/aitbc_cli/commands/mining.py b/cli/src/aitbc_cli/commands/mining.py deleted file mode 100644 index a365b70a..00000000 --- a/cli/src/aitbc_cli/commands/mining.py +++ /dev/null @@ -1,124 +0,0 @@ -""" -Mining commands for AITBC CLI -""" - -import json -from pathlib import Path -from typing import Optional, Dict - -import click - -from ..utils import error, success -from aitbc import AITBCHTTPClient, NetworkError, KEYSTORE_DIR - -DEFAULT_RPC_URL = "http://localhost:8006" -DEFAULT_KEYSTORE_DIR = KEYSTORE_DIR - - -@click.group() -def mining(): - """Mining operations commands""" - pass - - -@mining.command() -@click.argument('wallet_name') -@click.option('--threads', type=int, default=1, help='Number of mining threads') -@click.option('--rpc-url', help='Blockchain RPC URL') -def start(wallet_name: str, threads: int, rpc_url: Optional[str]): - """Start mining with specified wallet""" - if not rpc_url: - rpc_url = DEFAULT_RPC_URL - - try: - # Get wallet address - keystore_path = DEFAULT_KEYSTORE_DIR / f"{wallet_name}.json" - if not keystore_path.exists(): - error(f"Wallet '{wallet_name}' not found") - return False - - with open(keystore_path) as f: - wallet_data = json.load(f) - address = wallet_data['address'] - - # Start mining via RPC - mining_config = { - "miner_address": address, - "threads": threads, - "enabled": True - } - - try: - http_client = AITBCHTTPClient(base_url=rpc_url, timeout=30) - result = http_client.post("/rpc/mining/start", json=mining_config) - success(f"Mining started with wallet '{wallet_name}'") - click.echo(f"Miner address: {address}") - click.echo(f"Threads: {threads}") - click.echo(f"Status: {result.get('status', 'started')}") - return result - except NetworkError as e: - error(f"Error starting mining: {e}") - return None - except Exception as e: - error(f"Error: {e}") - return False - except Exception as e: - error(f"Error: {e}") - return False - - -@mining.command() -@click.option('--rpc-url', help='Blockchain RPC URL') -def stop(rpc_url: Optional[str]): - """Stop mining""" - if not rpc_url: - rpc_url = DEFAULT_RPC_URL - - try: - http_client = AITBCHTTPClient(base_url=rpc_url, timeout=30) - result = http_client.post("/rpc/mining/stop") - success("Mining stopped") - click.echo(f"Status: {result.get('status', 'stopped')}") - return True - except NetworkError as e: - error(f"Error stopping mining: {e}") - return False - except Exception as e: - error(f"Error: {e}") - return False - - -@mining.command() -@click.option('--rpc-url', help='Blockchain RPC URL') -def status(rpc_url: Optional[str]): - """Get mining status""" - if not rpc_url: - rpc_url = DEFAULT_RPC_URL - - try: - http_client = AITBCHTTPClient(base_url=rpc_url, timeout=30) - result = http_client.get("/rpc/mining/status") - success("Mining status:") - click.echo(json.dumps(result, indent=2)) - except NetworkError as e: - error(f"Error getting mining status: {e}") - except Exception as e: - error(f"Error: {e}") - - -@mining.command(name='list') -@click.option('--rpc-url', help='Blockchain RPC URL') -def list_miners(rpc_url: Optional[str]): - """List active miners""" - if not rpc_url: - rpc_url = DEFAULT_RPC_URL - - try: - http_client = AITBCHTTPClient(base_url=rpc_url, timeout=30) - result = http_client.get("/rpc/mining/miners") - success("Active miners:") - click.echo(json.dumps(result, indent=2)) - except NetworkError as e: - error(f"Error listing miners: {e}") - except Exception as e: - error(f"Error: {e}") diff --git a/cli/src/aitbc_cli/commands/monitor.py b/cli/src/aitbc_cli/commands/monitor.py deleted file mode 100755 index 39c0977a..00000000 --- a/cli/src/aitbc_cli/commands/monitor.py +++ /dev/null @@ -1,473 +0,0 @@ -"""Monitoring and dashboard commands for AITBC CLI""" - -import click -import json -import time -from pathlib import Path -from typing import Optional -from datetime import datetime, timedelta -from ..utils import output, error, success, console - -# Import shared modules -from aitbc import get_logger, AITBCHTTPClient, NetworkError - -# Initialize logger -logger = get_logger(__name__) - - -@click.group() -def monitor(): - """Monitoring, metrics, and alerting commands""" - pass - - -@monitor.command() -@click.option("--refresh", type=int, default=5, help="Refresh interval in seconds") -@click.option("--duration", type=int, default=0, help="Duration in seconds (0 = indefinite)") -@click.pass_context -def dashboard(ctx, refresh: int, duration: int): - """Real-time system dashboard""" - config = ctx.obj['config'] - start_time = time.time() - - try: - while True: - elapsed = time.time() - start_time - if duration > 0 and elapsed >= duration: - break - - console.clear() - console.rule("[bold blue]AITBC Dashboard[/bold blue]") - console.print(f"[dim]Refreshing every {refresh}s | Elapsed: {int(elapsed)}s[/dim]\n") - # Fetch system dashboard - try: - http_client = AITBCHTTPClient(base_url=config.coordinator_url, timeout=5) - # Get dashboard data - url = "/api/v1/dashboard" - dashboard = http_http_client.get( - url, - headers={"X-Api-Key": config.api_key or ""} - ) - console.print("[bold green]Dashboard Status:[/bold green] Online") - # Overall status - overall_status = dashboard.get("overall_status", "unknown") - console.print(f" Overall Status: {overall_status}") - # Services summary - services = dashboard.get("services", {}) - console.print(f" Services: {len(services)}") - for service_name, service_data in services.items(): - status = service_data.get("status", "unknown") - console.print(f" {service_name}: {status}") - # Metrics summary - metrics = dashboard.get("metrics", {}) - if metrics: - health_pct = metrics.get("health_percentage", 0) - console.print(f" Health: {health_pct:.1f}%") - except Exception as e: - console.print(f"[red]Error fetching data: {e}[/red]") - console.print(f"\n[dim]Press Ctrl+C to exit[/dim]") - time.sleep(refresh) - - except KeyboardInterrupt: - console.print("\n[bold]Dashboard stopped[/bold]") -@monitor.command() -@click.option("--period", default="24h", help="Time period (1h, 24h, 7d, 30d)") -@click.option("--export", "export_path", type=click.Path(), help="Export metrics to file") -@click.pass_context -def metrics(ctx, period: str, export_path: Optional[str]): - """Collect and display system metrics""" - config = ctx.obj['config'] - - # Parse period - multipliers = {"h": 3600, "d": 86400} - unit = period[-1] - value = int(period[:-1]) - seconds = value * multipliers.get(unit, 3600) - since = datetime.now() - timedelta(seconds=seconds) - - metrics_data = { - "period": period, - "since": since.isoformat(), - "collected_at": datetime.now().isoformat(), - "coordinator": {}, - "jobs": {}, - "miners": {} - } - - try: - http_client = AITBCHTTPClient(base_url=config.exchange_service_url, timeout=10) - # Coordinator metrics - try: - resp = http_client.get( - f"{config.coordinator_url}/status", - headers={"X-Api-Key": config.api_key or ""} - ) - if resp.status_code == 200: - metrics_data["coordinator"] = resp.json() - metrics_data["coordinator"]["status"] = "online" - else: - metrics_data["coordinator"]["status"] = f"error_{resp.status_code}" - except Exception: - metrics_data["coordinator"]["status"] = "offline" - - # Job metrics - try: - resp = http_client.get( - f"{config.coordinator_url}/v1/jobs", - headers={"X-Api-Key": config.api_key or ""}, - params={"limit": 100} - ) - if resp.status_code == 200: - jobs = resp.json() - if isinstance(jobs, list): - metrics_data["jobs"] = { - "total": len(jobs), - "completed": sum(1 for j in jobs if j.get("status") == "completed"), - "pending": sum(1 for j in jobs if j.get("status") == "pending"), - "failed": sum(1 for j in jobs if j.get("status") == "failed"), - } - except Exception: - metrics_data["jobs"] = {"error": "unavailable"} - - # Miner metrics - try: - resp = http_client.get( - f"{config.coordinator_url}/v1/miners", - headers={"X-Api-Key": config.api_key or ""} - ) - if resp.status_code == 200: - miners = resp.json() - if isinstance(miners, list): - metrics_data["miners"] = { - "total": len(miners), - "online": sum(1 for m in miners if m.get("status") == "ONLINE"), - "offline": sum(1 for m in miners if m.get("status") != "ONLINE"), - } - except Exception: - metrics_data["miners"] = {"error": "unavailable"} - - except Exception as e: - error(f"Failed to collect metrics: {e}") - - if export_path: - with open(export_path, "w") as f: - json.dump(metrics_data, f, indent=2) - success(f"Metrics exported to {export_path}") - - output(metrics_data, ctx.obj['output_format']) - - -@monitor.command() -@click.argument("action", type=click.Choice(["add", "list", "remove", "test"])) -@click.option("--name", help="Alert name") -@click.option("--type", "alert_type", type=click.Choice(["coordinator_down", "miner_offline", "job_failed", "low_balance"]), help="Alert type") -@click.option("--threshold", type=float, help="Alert threshold value") -@click.option("--webhook", help="Webhook URL for notifications") -@click.pass_context -def alerts(ctx, action: str, name: Optional[str], alert_type: Optional[str], - threshold: Optional[float], webhook: Optional[str]): - """Configure monitoring alerts""" - alerts_dir = Path.home() / ".aitbc" / "alerts" - alerts_dir.mkdir(parents=True, exist_ok=True) - alerts_file = alerts_dir / "alerts.json" - - # Load existing alerts - existing = [] - if alerts_file.exists(): - with open(alerts_file) as f: - existing = json.load(f) - - if action == "add": - if not name or not alert_type: - error("Alert name and type required (--name, --type)") - return - alert = { - "name": name, - "type": alert_type, - "threshold": threshold, - "webhook": webhook, - "created_at": datetime.now().isoformat(), - "enabled": True - } - existing.append(alert) - with open(alerts_file, "w") as f: - json.dump(existing, f, indent=2) - success(f"Alert '{name}' added") - output(alert, ctx.obj['output_format']) - - elif action == "list": - if not existing: - output({"message": "No alerts configured"}, ctx.obj['output_format']) - else: - output(existing, ctx.obj['output_format']) - - elif action == "remove": - if not name: - error("Alert name required (--name)") - return - existing = [a for a in existing if a["name"] != name] - with open(alerts_file, "w") as f: - json.dump(existing, f, indent=2) - success(f"Alert '{name}' removed") - - elif action == "test": - if not name: - error("Alert name required (--name)") - return - alert = next((a for a in existing if a["name"] == name), None) - if not alert: - error(f"Alert '{name}' not found") - return - if alert.get("webhook"): - try: - http_client = AITBCHTTPClient(base_url=config.exchange_service_url, timeout=10) - resp = http_client.post(alert["webhook"], json={ - "alert": name, - "type": alert["type"], - "message": f"Test alert from AITBC CLI", - "timestamp": datetime.now().isoformat() - }) - output({"status": "sent", "response_code": resp.status_code}, ctx.obj['output_format']) - except Exception as e: - error(f"Webhook test failed: {e}") - else: - output({"status": "no_webhook", "alert": alert}, ctx.obj['output_format']) - - -@monitor.command() -@click.option("--period", default="7d", help="Analysis period (1d, 7d, 30d)") -@click.pass_context -def history(ctx, period: str): - """Historical data analysis""" - config = ctx.obj['config'] - - multipliers = {"h": 3600, "d": 86400} - unit = period[-1] - value = int(period[:-1]) - seconds = value * multipliers.get(unit, 3600) - since = datetime.now() - timedelta(seconds=seconds) - - analysis = { - "period": period, - "since": since.isoformat(), - "analyzed_at": datetime.now().isoformat(), - "summary": {} - } - - try: - http_client = AITBCHTTPClient(base_url=config.exchange_service_url, timeout=10) - try: - resp = http_client.get( - f"{config.coordinator_url}/v1/jobs", - headers={"X-Api-Key": config.api_key or ""}, - params={"limit": 500} - ) - if resp.status_code == 200: - jobs = resp.json() - if isinstance(jobs, list): - completed = [j for j in jobs if j.get("status") == "completed"] - failed = [j for j in jobs if j.get("status") == "failed"] - analysis["summary"] = { - "total_jobs": len(jobs), - "completed": len(completed), - "failed": len(failed), - "success_rate": f"{len(completed) / max(1, len(jobs)) * 100:.1f}%", - } - except Exception: - analysis["summary"] = {"error": "Could not fetch job data"} - - except Exception as e: - error(f"Analysis failed: {e}") - - output(analysis, ctx.obj['output_format']) - - -@monitor.command() -@click.argument("action", type=click.Choice(["add", "list", "remove", "test"])) -@click.option("--name", help="Webhook name") -@click.option("--url", help="Webhook URL") -@click.option("--events", help="Comma-separated event types (job_completed,miner_offline,alert)") -@click.pass_context -def webhooks(ctx, action: str, name: Optional[str], url: Optional[str], events: Optional[str]): - """Manage webhook notifications""" - webhooks_dir = Path.home() / ".aitbc" / "webhooks" - webhooks_dir.mkdir(parents=True, exist_ok=True) - webhooks_file = webhooks_dir / "webhooks.json" - - existing = [] - if webhooks_file.exists(): - with open(webhooks_file) as f: - existing = json.load(f) - - if action == "add": - if not name or not url: - error("Webhook name and URL required (--name, --url)") - return - webhook = { - "name": name, - "url": url, - "events": events.split(",") if events else ["all"], - "created_at": datetime.now().isoformat(), - "enabled": True - } - existing.append(webhook) - with open(webhooks_file, "w") as f: - json.dump(existing, f, indent=2) - success(f"Webhook '{name}' added") - output(webhook, ctx.obj['output_format']) - - elif action == "list": - if not existing: - output({"message": "No webhooks configured"}, ctx.obj['output_format']) - else: - output(existing, ctx.obj['output_format']) - - elif action == "remove": - if not name: - error("Webhook name required (--name)") - return - existing = [w for w in existing if w["name"] != name] - with open(webhooks_file, "w") as f: - json.dump(existing, f, indent=2) - success(f"Webhook '{name}' removed") - - elif action == "test": - if not name: - error("Webhook name required (--name)") - return - wh = next((w for w in existing if w["name"] == name), None) - if not wh: - error(f"Webhook '{name}' not found") - return - try: - http_client = AITBCHTTPClient(base_url=config.exchange_service_url, timeout=10) - resp = http_client.post(wh["url"], json={ - "event": "test", - "source": "aitbc-cli", - "message": "Test webhook notification", - "timestamp": datetime.now().isoformat() - }) - output({"status": "sent", "response_code": resp.status_code}, ctx.obj['output_format']) - except Exception as e: - error(f"Webhook test failed: {e}") - - -CAMPAIGNS_DIR = Path.home() / ".aitbc" / "campaigns" - - -def _ensure_campaigns(): - CAMPAIGNS_DIR.mkdir(parents=True, exist_ok=True) - campaigns_file = CAMPAIGNS_DIR / "campaigns.json" - if not campaigns_file.exists(): - # Seed with default campaigns - default = {"campaigns": [ - { - "id": "staking_launch", - "name": "Staking Launch Campaign", - "type": "staking", - "apy_boost": 2.0, - "start_date": "2026-02-01T00:00:00", - "end_date": "2026-04-01T00:00:00", - "status": "active", - "total_staked": 0, - "participants": 0, - "rewards_distributed": 0 - }, - { - "id": "liquidity_mining_q1", - "name": "Q1 Liquidity Mining", - "type": "liquidity", - "apy_boost": 3.0, - "start_date": "2026-01-15T00:00:00", - "end_date": "2026-03-15T00:00:00", - "status": "active", - "total_staked": 0, - "participants": 0, - "rewards_distributed": 0 - } - ]} - with open(campaigns_file, "w") as f: - json.dump(default, f, indent=2) - return campaigns_file - - -@monitor.command() -@click.option("--status", type=click.Choice(["active", "ended", "all"]), default="all", help="Filter by status") -@click.pass_context -def campaigns(ctx, status: str): - """List active incentive campaigns""" - campaigns_file = _ensure_campaigns() - with open(campaigns_file) as f: - data = json.load(f) - - campaign_list = data.get("campaigns", []) - - # Auto-update status - now = datetime.now() - for c in campaign_list: - end = datetime.fromisoformat(c["end_date"]) - if now > end and c["status"] == "active": - c["status"] = "ended" - with open(campaigns_file, "w") as f: - json.dump(data, f, indent=2) - - if status != "all": - campaign_list = [c for c in campaign_list if c["status"] == status] - - if not campaign_list: - output({"message": "No campaigns found"}, ctx.obj['output_format']) - return - - output(campaign_list, ctx.obj['output_format']) - - -@monitor.command(name="campaign-stats") -@click.argument("campaign_id", required=False) -@click.pass_context -def campaign_stats(ctx, campaign_id: Optional[str]): - """Campaign performance metrics (TVL, participants, rewards)""" - campaigns_file = _ensure_campaigns() - with open(campaigns_file) as f: - data = json.load(f) - - campaign_list = data.get("campaigns", []) - - if campaign_id: - campaign = next((c for c in campaign_list if c["id"] == campaign_id), None) - if not campaign: - error(f"Campaign '{campaign_id}' not found") - ctx.exit(1) - return - targets = [campaign] - else: - targets = campaign_list - - stats = [] - for c in targets: - start = datetime.fromisoformat(c["start_date"]) - end = datetime.fromisoformat(c["end_date"]) - now = datetime.now() - duration_days = (end - start).days - elapsed_days = min((now - start).days, duration_days) - progress_pct = round(elapsed_days / max(duration_days, 1) * 100, 1) - - stats.append({ - "campaign_id": c["id"], - "name": c["name"], - "type": c["type"], - "status": c["status"], - "apy_boost": c.get("apy_boost", 0), - "tvl": c.get("total_staked", 0), - "participants": c.get("participants", 0), - "rewards_distributed": c.get("rewards_distributed", 0), - "duration_days": duration_days, - "elapsed_days": elapsed_days, - "progress_pct": progress_pct, - "start_date": c["start_date"], - "end_date": c["end_date"] - }) - - if len(stats) == 1: - output(stats[0], ctx.obj['output_format']) - else: - output(stats, ctx.obj['output_format']) diff --git a/cli/src/aitbc_cli/commands/node.py b/cli/src/aitbc_cli/commands/node.py deleted file mode 100755 index 96ed726b..00000000 --- a/cli/src/aitbc_cli/commands/node.py +++ /dev/null @@ -1,1043 +0,0 @@ -""" -Node management commands for AITBC -""" - -import os -import sys -import socket -import json -import hashlib -import click -import asyncio -from pathlib import Path -from typing import Optional -from datetime import datetime - -try: - from ..utils.output import output, success, error, warning, info - from ..core.config import MultiChainConfig, load_multichain_config, get_default_node_config, add_node_config, remove_node_config - from ..core.node_client import NodeClient -except ImportError: - from utils import output, error, success, warning - from core.config import MultiChainConfig, load_multichain_config, get_default_node_config, add_node_config, remove_node_config - from core.node_client import NodeClient - - def info(message): - click.echo(message) -import uuid - -@click.group() -def node(): - """Node management commands""" - pass - -@node.command() -@click.argument('node_id') -@click.pass_context -def info(ctx, node_id): - """Get detailed node information""" - try: - config = load_multichain_config() - - if node_id not in config.nodes: - error(f"Node {node_id} not found in configuration") - raise click.Abort() - - node_config = config.nodes[node_id] - - import asyncio - - async def get_node_info(): - async with NodeClient(node_config) as client: - return await client.get_node_info() - - node_info = asyncio.run(get_node_info()) - - # Basic node information - basic_info = { - "Node ID": node_info["node_id"], - "Node Type": node_info["type"], - "Status": node_info["status"], - "Version": node_info["version"], - "Uptime": f"{node_info['uptime_days']} days, {node_info['uptime_hours']} hours", - "Endpoint": node_config.endpoint - } - - output(basic_info, ctx.obj.get('output_format', 'table'), title=f"Node Information: {node_id}") - - # Performance metrics - metrics = { - "CPU Usage": f"{node_info['cpu_usage']}%", - "Memory Usage": f"{node_info['memory_usage_mb']:.1f}MB", - "Disk Usage": f"{node_info['disk_usage_mb']:.1f}MB", - "Network In": f"{node_info['network_in_mb']:.1f}MB/s", - "Network Out": f"{node_info['network_out_mb']:.1f}MB/s" - } - - output(metrics, ctx.obj.get('output_format', 'table'), title="Performance Metrics") - - # Hosted chains - if node_info.get("hosted_chains"): - chains_data = [ - { - "Chain ID": chain_id, - "Type": chain.get("type", "unknown"), - "Status": chain.get("status", "unknown") - } - for chain_id, chain in node_info["hosted_chains"].items() - ] - - output(chains_data, ctx.obj.get('output_format', 'table'), title="Hosted Chains") - - except Exception as e: - error(f"Error getting node info: {str(e)}") - raise click.Abort() - -@node.command() -@click.option('--show-private', is_flag=True, help='Show private chains') -@click.option('--node-id', help='Specific node ID to query') -@click.pass_context -def chains(ctx, show_private, node_id): - """List chains hosted on all nodes""" - try: - config = load_multichain_config() - - all_chains = [] - - import asyncio - - async def get_all_chains(): - tasks = [] - for nid, node_config in config.nodes.items(): - if node_id and nid != node_id: - continue - async def get_chains_for_node(nid, nconfig): - try: - async with NodeClient(nconfig) as client: - chains = await client.get_hosted_chains() - return [(nid, chain) for chain in chains] - except Exception as e: - click.echo(f"Error getting chains from node {nid}: {e}") - return [] - - tasks.append(get_chains_for_node(node_id, node_config)) - - results = await asyncio.gather(*tasks) - for result in results: - all_chains.extend(result) - - asyncio.run(get_all_chains()) - - if not all_chains: - output("No chains found on any node", ctx.obj.get('output_format', 'table')) - return - - # Filter private chains if not requested - if not show_private: - all_chains = [(node_id, chain) for node_id, chain in all_chains - if chain.privacy.visibility != "private"] - - # Format output - chains_data = [ - { - "Node ID": node_id, - "Chain ID": chain.id, - "Type": chain.type.value, - "Purpose": chain.purpose, - "Name": chain.name, - "Status": chain.status.value, - "Block Height": chain.block_height, - "Size": f"{chain.size_mb:.1f}MB" - } - for node_id, chain in all_chains - ] - - output(chains_data, ctx.obj.get('output_format', 'table'), title="Chains by Node") - - except Exception as e: - error(f"Error listing chains: {str(e)}") - raise click.Abort() - -@node.command() -@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format') -@click.pass_context -def list(ctx, format): - """List all configured nodes""" - try: - config = load_multichain_config() - - if not config.nodes: - output("No nodes configured", ctx.obj.get('output_format', 'table')) - return - - nodes_data = [ - { - "Node ID": node_id, - "Endpoint": node_config.endpoint, - "Timeout": f"{node_config.timeout}s", - "Max Connections": node_config.max_connections, - "Retry Count": node_config.retry_count - } - for node_id, node_config in config.nodes.items() - ] - - output(nodes_data, ctx.obj.get('output_format', 'table'), title="Configured Nodes") - - except Exception as e: - error(f"Error listing nodes: {str(e)}") - raise click.Abort() - -@node.command() -@click.argument('node_id') -@click.argument('endpoint') -@click.option('--timeout', default=30, help='Request timeout in seconds') -@click.option('--max-connections', default=10, help='Maximum concurrent connections') -@click.option('--retry-count', default=3, help='Number of retry attempts') -@click.pass_context -def add(ctx, node_id, endpoint, timeout, max_connections, retry_count): - """Add a new node to configuration""" - try: - config = load_multichain_config() - - if node_id in config.nodes: - error(f"Node {node_id} already exists") - raise click.Abort() - - node_config = get_default_node_config() - node_config.id = node_id - node_config.endpoint = endpoint - node_config.timeout = timeout - node_config.max_connections = max_connections - node_config.retry_count = retry_count - - config = add_node_config(config, node_config) - - from ..core.config import save_multichain_config - save_multichain_config(config) - - success(f"Node {node_id} added successfully!") - - result = { - "Node ID": node_id, - "Endpoint": endpoint, - "Timeout": f"{timeout}s", - "Max Connections": max_connections, - "Retry Count": retry_count - } - - output(result, ctx.obj.get('output_format', 'table')) - - except Exception as e: - error(f"Error adding node: {str(e)}") - raise click.Abort() - -@node.command() -@click.argument('node_id') -@click.option('--force', is_flag=True, help='Force removal without confirmation') -@click.pass_context -def remove(ctx, node_id, force): - """Remove a node from configuration""" - try: - config = load_multichain_config() - - if node_id not in config.nodes: - error(f"Node {node_id} not found") - raise click.Abort() - - if not force: - # Show node information before removal - node_config = config.nodes[node_id] - node_info = { - "Node ID": node_id, - "Endpoint": node_config.endpoint, - "Timeout": f"{node_config.timeout}s", - "Max Connections": node_config.max_connections - } - - output(node_info, ctx.obj.get('output_format', 'table'), title="Node to Remove") - - if not click.confirm(f"Are you sure you want to remove node {node_id}?"): - raise click.Abort() - - config = remove_node_config(config, node_id) - - from ..core.config import save_multichain_config - save_multichain_config(config) - - success(f"Node {node_id} removed successfully!") - - except Exception as e: - error(f"Error removing node: {str(e)}") - raise click.Abort() - -@node.command() -@click.argument('node_id') -@click.option('--realtime', is_flag=True, help='Real-time monitoring') -@click.option('--interval', default=5, help='Update interval in seconds') -@click.pass_context -def monitor(ctx, node_id, realtime, interval): - """Monitor node activity""" - try: - config = load_multichain_config() - - if node_id not in config.nodes: - error(f"Node {node_id} not found") - raise click.Abort() - - node_config = config.nodes[node_id] - - import asyncio - from rich.console import Console - from rich.layout import Layout - from rich.live import Live - import time - - console = Console() - - async def get_node_stats(): - async with NodeClient(node_config) as client: - node_info = await client.get_node_info() - return node_info - - if realtime: - # Real-time monitoring - def generate_monitor_layout(): - try: - node_info = asyncio.run(get_node_stats()) - - layout = Layout() - layout.split_column( - Layout(name="header", size=3), - Layout(name="metrics"), - Layout(name="chains", size=10) - ) - - # Header - layout["header"].update( - f"Node Monitor: {node_id} - {node_info['status'].upper()}" - ) - - # Metrics table - metrics_data = [ - ["CPU Usage", f"{node_info['cpu_usage']}%"], - ["Memory Usage", f"{node_info['memory_usage_mb']:.1f}MB"], - ["Disk Usage", f"{node_info['disk_usage_mb']:.1f}MB"], - ["Network In", f"{node_info['network_in_mb']:.1f}MB/s"], - ["Network Out", f"{node_info['network_out_mb']:.1f}MB/s"], - ["Uptime", f"{node_info['uptime_days']}d {node_info['uptime_hours']}h"] - ] - - layout["metrics"].update(str(metrics_data)) - - # Chains info - if node_info.get("hosted_chains"): - chains_text = f"Hosted Chains: {len(node_info['hosted_chains'])}\n" - for chain_id, chain in list(node_info["hosted_chains"].items())[:5]: - chains_text += f" • {chain_id} ({chain.get('status', 'unknown')})\n" - layout["chains"].update(chains_text) - else: - layout["chains"].update("No chains hosted") - - return layout - except Exception as e: - return f"Error getting node stats: {e}" - - with Live(generate_monitor_layout(), refresh_per_second=1) as live: - try: - while True: - live.update(generate_monitor_layout()) - time.sleep(interval) - except KeyboardInterrupt: - console.click.echo("\n[yellow]Monitoring stopped by user[/yellow]") - else: - # Single snapshot - node_info = asyncio.run(get_node_stats()) - - stats_data = [ - { - "Metric": "CPU Usage", - "Value": f"{node_info['cpu_usage']}%" - }, - { - "Metric": "Memory Usage", - "Value": f"{node_info['memory_usage_mb']:.1f}MB" - }, - { - "Metric": "Disk Usage", - "Value": f"{node_info['disk_usage_mb']:.1f}MB" - }, - { - "Metric": "Network In", - "Value": f"{node_info['network_in_mb']:.1f}MB/s" - }, - { - "Metric": "Network Out", - "Value": f"{node_info['network_out_mb']:.1f}MB/s" - }, - { - "Metric": "Uptime", - "Value": f"{node_info['uptime_days']}d {node_info['uptime_hours']}h" - } - ] - - output(stats_data, ctx.obj.get('output_format', 'table'), title=f"Node Statistics: {node_id}") - - except Exception as e: - error(f"Error during monitoring: {str(e)}") - raise click.Abort() - -@node.command() -@click.argument('node_id') -@click.pass_context -def test(ctx, node_id): - """Test connectivity to a node""" - try: - config = load_multichain_config() - - if node_id not in config.nodes: - error(f"Node {node_id} not found") - raise click.Abort() - - node_config = config.nodes[node_id] - - import asyncio - - async def test_node(): - try: - async with NodeClient(node_config) as client: - node_info = await client.get_node_info() - chains = await client.get_hosted_chains() - - return { - "connected": True, - "node_id": node_info["node_id"], - "status": node_info["status"], - "version": node_info["version"], - "chains_count": len(chains) - } - except Exception as e: - return { - "connected": False, - "error": str(e) - } - - result = asyncio.run(test_node()) - - if result["connected"]: - success(f"Successfully connected to node {node_id}!") - - test_data = [ - { - "Test": "Connection", - "Status": "āœ“ Pass" - }, - { - "Test": "Node ID", - "Status": result["node_id"] - }, - { - "Test": "Status", - "Status": result["status"] - }, - { - "Test": "Version", - "Status": result["version"] - }, - { - "Test": "Chains", - "Status": f"{result['chains_count']} hosted" - } - ] - - output(test_data, ctx.obj.get('output_format', 'table'), title=f"Node Test Results: {node_id}") - else: - error(f"Failed to connect to node {node_id}: {result['error']}") - raise click.Abort() - - except Exception as e: - error(f"Error testing node: {str(e)}") - raise click.Abort() - -# Island management commands -@node.group() -def island(): - """Island management commands for federated mesh""" - pass - -@island.command() -@click.option('--island-id', help='Island ID (UUID), generates new if not provided') -@click.option('--island-name', default='default', help='Human-readable island name') -@click.option('--chain-id', help='Chain ID for this island') -@click.pass_context -def create(ctx, island_id, island_name, chain_id): - """Create a new island""" - try: - if not island_id: - island_id = str(uuid.uuid4()) - - if not chain_id: - chain_id = f"ait-{island_id[:8]}" - - island_info = { - "Island ID": island_id, - "Island Name": island_name, - "Chain ID": chain_id, - "Created": "Now" - } - - output(island_info, ctx.obj.get('output_format', 'table'), title="New Island Created") - success(f"Island {island_name} ({island_id}) created successfully") - - # Note: In a real implementation, this would update the configuration - # and notify the island manager - - except Exception as e: - error(f"Error creating island: {str(e)}") - raise click.Abort() - -@island.command() -@click.argument('island_id') -@click.argument('island_name') -@click.argument('chain_id') -@click.option('--hub', default='hub.aitbc.bubuit.net', help='Hub domain name to connect to') -@click.option('--is-hub', is_flag=True, help='Register this node as a hub for the island') -@click.pass_context -def join(ctx, island_id, island_name, chain_id, hub, is_hub): - """Join an existing island""" - try: - # Get system hostname - hostname = socket.gethostname() - - sys.path.insert(0, '/opt/aitbc/apps/blockchain-node/src') - from aitbc_chain.config import settings as chain_settings - - # Get public key from keystore - keystore_path = '/var/lib/aitbc/keystore/validator_keys.json' - public_key_pem = None - - if os.path.exists(keystore_path): - with open(keystore_path, 'r') as f: - keys = json.load(f) - # Get first key's public key - for key_id, key_data in keys.items(): - public_key_pem = key_data.get('public_key_pem') - break - else: - error(f"Keystore not found at {keystore_path}") - raise click.Abort() - - if not public_key_pem: - error("No public key found in keystore") - raise click.Abort() - - # Generate node_id using hostname-based method - local_address = socket.gethostbyname(hostname) - local_port = chain_settings.p2p_bind_port - content = f"{hostname}:{local_address}:{local_port}:{public_key_pem}" - node_id = hashlib.sha256(content.encode()).hexdigest() - - # Resolve hub domain to IP - hub_ip = socket.gethostbyname(hub) - hub_port = chain_settings.p2p_bind_port - - click.echo(f"Connecting to hub {hub} ({hub_ip}:{hub_port})...") - - # Create P2P network service instance for sending join request - from aitbc_chain.p2p_network import P2PNetworkService - - # Create a minimal P2P service just for sending the join request - p2p_service = P2PNetworkService( - local_address, - local_port, - node_id, - "", - island_id=island_id, - island_name=island_name, - is_hub=is_hub, - island_chain_id=chain_id or chain_settings.island_chain_id or chain_settings.chain_id, - ) - - # Send join request - async def send_join(): - return await p2p_service.send_join_request( - hub_ip, hub_port, island_id, island_name, node_id, public_key_pem - ) - - response = asyncio.run(send_join()) - - if response: - # Store credentials locally - credentials_path = '/var/lib/aitbc/island_credentials.json' - credentials_data = { - "island_id": response.get('island_id'), - "island_name": response.get('island_name'), - "island_chain_id": response.get('island_chain_id'), - "credentials": response.get('credentials'), - "joined_at": datetime.now().isoformat() - } - - with open(credentials_path, 'w') as f: - json.dump(credentials_data, f, indent=2) - - # Display join info - join_info = { - "Island ID": response.get('island_id'), - "Island Name": response.get('island_name'), - "Chain ID": response.get('island_chain_id'), - "Member Count": len(response.get('members', [])), - "Credentials Stored": credentials_path - } - - output(join_info, ctx.obj.get('output_format', 'table'), title=f"Joined Island: {island_name}") - - # Display member list - members = response.get('members', []) - if members: - output(members, ctx.obj.get('output_format', 'table'), title="Island Members") - - # Display credentials - credentials = response.get('credentials', {}) - if credentials: - output(credentials, ctx.obj.get('output_format', 'table'), title="Blockchain Credentials") - - success(f"Successfully joined island {island_name}") - - # If registering as hub - if is_hub: - click.echo("Registering as hub...") - # Hub registration would happen here via the hub register command - click.echo("Run 'aitbc node hub register' to complete hub registration") - else: - error("Failed to join island - no response from hub") - raise click.Abort() - - except Exception as e: - error(f"Error joining island: {str(e)}") - raise click.Abort() - -@island.command() -@click.argument('island_id') -@click.pass_context -def leave(ctx, island_id): - """Leave an island""" - try: - success(f"Successfully left island {island_id}") - - # Note: In a real implementation, this would update the island manager - - except Exception as e: - error(f"Error leaving island: {str(e)}") - raise click.Abort() - -@island.command() -@click.pass_context -def list(ctx): - """List all known islands""" - try: - # Note: In a real implementation, this would query the island manager - islands = [ - { - "Island ID": "550e8400-e29b-41d4-a716-446655440000", - "Island Name": "default", - "Chain ID": "ait-island-default", - "Status": "Active", - "Peer Count": "3" - } - ] - - output(islands, ctx.obj.get('output_format', 'table'), title="Known Islands") - - except Exception as e: - error(f"Error listing islands: {str(e)}") - raise click.Abort() - -@island.command() -@click.argument('island_id') -@click.pass_context -def info(ctx, island_id): - """Show information about a specific island""" - try: - # Note: In a real implementation, this would query the island manager - island_info = { - "Island ID": island_id, - "Island Name": "default", - "Chain ID": "ait-island-default", - "Status": "Active", - "Peer Count": "3", - "Hub Count": "1" - } - - output(island_info, ctx.obj.get('output_format', 'table'), title=f"Island Information: {island_id}") - - except Exception as e: - error(f"Error getting island info: {str(e)}") - raise click.Abort() - -# Hub management commands -@node.group() -def hub(): - """Hub management commands for federated mesh""" - pass - -@hub.command() -@click.option('--public-address', help='Public IP address') -@click.option('--public-port', type=int, help='Public port') -@click.option('--redis-url', default='redis://localhost:6379', help='Redis URL for persistence') -@click.option('--hub-discovery-url', default='hub.aitbc.bubuit.net', help='DNS hub discovery URL') -@click.pass_context -def register(ctx, public_address, public_port, redis_url, hub_discovery_url): - """Register this node as a hub""" - try: - # Get environment variables - island_id = os.getenv('ISLAND_ID', 'default-island-id') - island_name = os.getenv('ISLAND_NAME', 'default') - - # Get system hostname - hostname = socket.gethostname() - - # Get public key from keystore - keystore_path = '/var/lib/aitbc/keystore/validator_keys.json' - public_key_pem = None - - if os.path.exists(keystore_path): - with open(keystore_path, 'r') as f: - keys = json.load(f) - # Get first key's public key - for key_id, key_data in keys.items(): - public_key_pem = key_data.get('public_key_pem') - break - else: - error(f"Keystore not found at {keystore_path}") - raise click.Abort() - - if not public_key_pem: - error("No public key found in keystore") - raise click.Abort() - - # Generate node_id using hostname-based method - local_address = socket.gethostbyname(hostname) - local_port = 7070 # Default hub port - content = f"{hostname}:{local_address}:{local_port}:{public_key_pem}" - node_id = hashlib.sha256(content.encode()).hexdigest() - - # Create HubManager instance - sys.path.insert(0, '/opt/aitbc/apps/blockchain-node/src') - from aitbc_chain.network.hub_manager import HubManager - from aitbc_chain.network.hub_discovery import HubDiscovery - - hub_manager = HubManager( - node_id, - local_address, - local_port, - island_id, - island_name, - redis_url - ) - - # Register as hub (async) - async def register_hub(): - success = await hub_manager.register_as_hub(public_address, public_port) - if success: - # Register with DNS discovery service - hub_discovery = HubDiscovery(hub_discovery_url, local_port) - hub_info_dict = { - "node_id": node_id, - "address": local_address, - "port": local_port, - "island_id": island_id, - "island_name": island_name, - "public_address": public_address, - "public_port": public_port, - "public_key_pem": public_key_pem - } - dns_success = await hub_discovery.register_hub(hub_info_dict) - return success and dns_success - return False - - result = asyncio.run(register_hub()) - - if result: - hub_info = { - "Node ID": node_id, - "Hostname": hostname, - "Address": local_address, - "Port": local_port, - "Island ID": island_id, - "Island Name": island_name, - "Public Address": public_address or "auto-discovered", - "Public Port": public_port or "auto-discovered", - "Status": "Registered" - } - - output(hub_info, ctx.obj.get('output_format', 'table'), title="Hub Registration") - success("Successfully registered as hub") - else: - error("Failed to register as hub") - raise click.Abort() - - except Exception as e: - error(f"Error registering as hub: {str(e)}") - raise click.Abort() - -@hub.command() -@click.option('--redis-url', default='redis://localhost:6379', help='Redis URL for persistence') -@click.option('--hub-discovery-url', default='hub.aitbc.bubuit.net', help='DNS hub discovery URL') -@click.pass_context -def unregister(ctx, redis_url, hub_discovery_url): - """Unregister this node as a hub""" - try: - # Get environment variables - island_id = os.getenv('ISLAND_ID', 'default-island-id') - island_name = os.getenv('ISLAND_NAME', 'default') - - # Get system hostname - hostname = socket.gethostname() - - # Get public key from keystore - keystore_path = '/var/lib/aitbc/keystore/validator_keys.json' - public_key_pem = None - - if os.path.exists(keystore_path): - with open(keystore_path, 'r') as f: - keys = json.load(f) - # Get first key's public key - for key_id, key_data in keys.items(): - public_key_pem = key_data.get('public_key_pem') - break - else: - error(f"Keystore not found at {keystore_path}") - raise click.Abort() - - if not public_key_pem: - error("No public key found in keystore") - raise click.Abort() - - # Generate node_id using hostname-based method - local_address = socket.gethostbyname(hostname) - local_port = 7070 # Default hub port - content = f"{hostname}:{local_address}:{local_port}:{public_key_pem}" - node_id = hashlib.sha256(content.encode()).hexdigest() - - # Create HubManager instance - sys.path.insert(0, '/opt/aitbc/apps/blockchain-node/src') - from aitbc_chain.network.hub_manager import HubManager - from aitbc_chain.network.hub_discovery import HubDiscovery - - hub_manager = HubManager( - node_id, - local_address, - local_port, - island_id, - island_name, - redis_url - ) - - # Unregister as hub (async) - async def unregister_hub(): - success = await hub_manager.unregister_as_hub() - if success: - # Unregister from DNS discovery service - hub_discovery = HubDiscovery(hub_discovery_url, local_port) - dns_success = await hub_discovery.unregister_hub(node_id) - return success and dns_success - return False - - result = asyncio.run(unregister_hub()) - - if result: - hub_info = { - "Node ID": node_id, - "Status": "Unregistered" - } - - output(hub_info, ctx.obj.get('output_format', 'table'), title="Hub Unregistration") - success("Successfully unregistered as hub") - else: - error("Failed to unregister as hub") - raise click.Abort() - - except Exception as e: - error(f"Error unregistering as hub: {str(e)}") - raise click.Abort() - -@hub.command() -@click.option('--redis-url', default='redis://localhost:6379', help='Redis URL for persistence') -@click.pass_context -def list(ctx, redis_url): - """List registered hubs from Redis""" - try: - import redis.asyncio as redis - - async def list_hubs(): - hubs = [] - try: - r = redis.from_url(redis_url) - # Get all hub keys - keys = await r.keys("hub:*") - for key in keys: - value = await r.get(key) - if value: - hub_data = json.loads(value) - hubs.append({ - "Node ID": hub_data.get("node_id"), - "Address": hub_data.get("address"), - "Port": hub_data.get("port"), - "Island ID": hub_data.get("island_id"), - "Island Name": hub_data.get("island_name"), - "Public Address": hub_data.get("public_address", "N/A"), - "Public Port": hub_data.get("public_port", "N/A"), - "Peer Count": hub_data.get("peer_count", 0) - }) - await r.close() - except Exception as e: - error(f"Failed to query Redis: {e}") - return [] - return hubs - - hubs = asyncio.run(list_hubs()) - - if hubs: - output(hubs, ctx.obj.get('output_format', 'table'), title="Registered Hubs") - else: - info("No registered hubs found") - - except Exception as e: - error(f"Error listing hubs: {str(e)}") - raise click.Abort() - -# Bridge management commands -@node.group() -def bridge(): - """Bridge management commands for federated mesh""" - pass - -@bridge.command() -@click.argument('target_island_id') -@click.pass_context -def request(ctx, target_island_id): - """Request a bridge to another island""" - try: - success(f"Bridge request sent to island {target_island_id}") - - # Note: In a real implementation, this would use the bridge manager - - except Exception as e: - error(f"Error requesting bridge: {str(e)}") - raise click.Abort() - -@bridge.command() -@click.argument('request_id') -@click.argument('approving_node_id') -@click.pass_context -def approve(ctx, request_id, approving_node_id): - """Approve a bridge request""" - try: - success(f"Bridge request {request_id} approved") - - # Note: In a real implementation, this would use the bridge manager - - except Exception as e: - error(f"Error approving bridge request: {str(e)}") - raise click.Abort() - -@bridge.command() -@click.argument('request_id') -@click.option('--reason', help='Rejection reason') -@click.pass_context -def reject(ctx, request_id, reason): - """Reject a bridge request""" - try: - success(f"Bridge request {request_id} rejected") - - # Note: In a real implementation, this would use the bridge manager - - except Exception as e: - error(f"Error rejecting bridge request: {str(e)}") - raise click.Abort() - -@bridge.command() -@click.pass_context -def list(ctx): - """List bridge connections""" - try: - # Note: In a real implementation, this would query the bridge manager - bridges = [ - { - "Bridge ID": "bridge-1", - "Source Island": "island-a", - "Target Island": "island-b", - "Status": "Active" - } - ] - - output(bridges, ctx.obj.get('output_format', 'table'), title="Bridge Connections") - - except Exception as e: - error(f"Error listing bridges: {str(e)}") - raise click.Abort() - -# Multi-chain management commands -@node.group() -def chain(): - """Multi-chain management commands for parallel chains""" - pass - -@chain.command() -@click.argument('chain_id') -@click.option('--chain-type', type=click.Choice(['bilateral', 'micro']), default='micro', help='Chain type') -@click.pass_context -def start(ctx, chain_id, chain_type): - """Start a new parallel chain instance""" - try: - chain_info = { - "Chain ID": chain_id, - "Chain Type": chain_type, - "Status": "Starting", - "RPC Port": "auto-allocated", - "P2P Port": "auto-allocated" - } - - output(chain_info, ctx.obj.get('output_format', 'table'), title=f"Starting Chain: {chain_id}") - success(f"Chain {chain_id} started successfully") - - # Note: In a real implementation, this would use the multi-chain manager - - except Exception as e: - error(f"Error starting chain: {str(e)}") - raise click.Abort() - -@chain.command() -@click.argument('chain_id') -@click.pass_context -def stop(ctx, chain_id): - """Stop a parallel chain instance""" - try: - success(f"Chain {chain_id} stopped successfully") - - # Note: In a real implementation, this would use the multi-chain manager - - except Exception as e: - error(f"Error stopping chain: {str(e)}") - raise click.Abort() - -@chain.command() -@click.pass_context -def list(ctx): - """List all active chain instances""" - try: - # Note: In a real implementation, this would query the multi-chain manager - chains = [ - { - "Chain ID": "ait-mainnet", - "Chain Type": "default", - "Status": "Running", - "RPC Port": 8000, - "P2P Port": 7070 - } - ] - - output(chains, ctx.obj.get('output_format', 'table'), title="Active Chains") - - except Exception as e: - error(f"Error listing chains: {str(e)}") - raise click.Abort() diff --git a/cli/src/aitbc_cli/commands/operations.py b/cli/src/aitbc_cli/commands/operations.py deleted file mode 100644 index b602e60d..00000000 --- a/cli/src/aitbc_cli/commands/operations.py +++ /dev/null @@ -1,359 +0,0 @@ -""" -General operations commands for AITBC CLI (marketplace, AI, agents) -""" - -import json -import time -import hashlib -from pathlib import Path -from typing import Optional - -import click - -from ..utils import error, success -from ..utils.wallet import decrypt_private_key -from aitbc import AITBCHTTPClient, NetworkError, KEYSTORE_DIR -from cryptography.hazmat.primitives.asymmetric import ed25519 -from cryptography.hazmat.primitives import serialization - -DEFAULT_RPC_URL = "http://localhost:8006" -DEFAULT_KEYSTORE_DIR = KEYSTORE_DIR - - -@click.group() -def operations(): - """General operations commands""" - pass - - -# Marketplace operations -@operations.group() -def marketplace(): - """Marketplace operations""" - pass - - -@marketplace.command() -@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format') -def list_listings(format: str): - """List marketplace listings""" - try: - http_client = AITBCHTTPClient(base_url="http://localhost:8102", timeout=30) - data = http_client.get("/rpc/marketplace/listings") - listings = data.get("listings", []) - success(f"Marketplace listings: {len(listings)}") - if format == 'json': - click.echo(json.dumps(listings, indent=2)) - else: - for listing in listings: - click.echo(f" - {listing.get('name', 'unknown')}: {listing.get('price', 0)} AIT") - except NetworkError as e: - error(f"Error getting marketplace listings: {e}") - except Exception as e: - error(f"Error: {e}") - - -@marketplace.command() -@click.argument('listing_id') -@click.option('--quantity', type=int, default=1, help='Quantity to purchase') -@click.option('--wallet', help='Wallet name for payment') -def purchase(listing_id: str, quantity: int, wallet: Optional[str]): - """Purchase from marketplace listing""" - success(f"Purchase {quantity} of listing {listing_id}") - # TODO: Implement actual purchase logic with wallet signing - - -@marketplace.command() -@click.option('--wallet-name', required=True, help='Seller wallet name') -@click.option('--item-type', required=True, help='Type of item') -@click.option('--price', type=float, required=True, help='Listing price') -@click.option('--description', help='Item description') -def create_listing(wallet_name: str, item_type: str, price: float, description: Optional[str]): - """Create a marketplace listing""" - try: - # Get wallet address - keystore_path = DEFAULT_KEYSTORE_DIR / f"{wallet_name}.json" - if not keystore_path.exists(): - error(f"Wallet '{wallet_name}' not found") - return None - - with open(keystore_path) as f: - wallet_data = json.load(f) - address = wallet_data['address'] - - # Create listing via RPC - listing_config = { - "seller_address": address, - "item_type": item_type, - "price": price, - "description": description or "" - } - - try: - http_client = AITBCHTTPClient(base_url="http://localhost:8102", timeout=30) - result = http_client.post("/rpc/marketplace/create", json=listing_config) - success(f"Listing created successfully") - click.echo(f"Item: {item_type}") - click.echo(f"Price: {price} AIT") - click.echo(f"Listing ID: {result.get('listing_id', 'unknown')}") - return result - except NetworkError as e: - error(f"Error creating listing: {e}") - return None - except Exception as e: - error(f"Error: {e}") - return None - except Exception as e: - error(f"Error: {e}") - - -# AI operations -@operations.group() -def ai(): - """AI operations""" - pass - - -@ai.command() -@click.option('--wallet-name', required=True, help='Client wallet name') -@click.option('--job-type', required=True, help='Type of AI job') -@click.option('--prompt', required=True, help='AI prompt') -@click.option('--payment', type=float, required=True, help='Payment amount') -@click.option('--model', help='AI model to use') -def submit_job(wallet_name: str, job_type: str, prompt: str, payment: float, model: Optional[str]): - """Submit an AI job""" - try: - # Get wallet address - keystore_path = DEFAULT_KEYSTORE_DIR / f"{wallet_name}.json" - if not keystore_path.exists(): - error(f"Wallet '{wallet_name}' not found") - return None - - with open(keystore_path) as f: - wallet_data = json.load(f) - address = wallet_data['address'] - - # Submit job via coordinator API - job_config = { - "client_address": address, - "job_type": job_type, - "prompt": prompt, - "payment": payment, - "model": model or "default" - } - - try: - http_client = AITBCHTTPClient(base_url="http://localhost:9001", timeout=30) - result = http_client.post("/v1/jobs", json=job_config) - success(f"AI job submitted successfully") - click.echo(f"Job ID: {result.get('job_id', 'unknown')}") - click.echo(f"Type: {job_type}") - click.echo(f"Payment: {payment} AIT") - return result - except NetworkError as e: - error(f"Error submitting AI job: {e}") - return None - except Exception as e: - error(f"Error: {e}") - return None - except Exception as e: - error(f"Error: {e}") - - -@ai.command() -@click.option('--job-id', help='Specific job ID') -@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format') -def status(job_id: Optional[str], format: str): - """Get AI job status""" - try: - http_client = AITBCHTTPClient(base_url="http://localhost:9001", timeout=30) - if job_id: - result = http_client.get(f"/v1/jobs/{job_id}") - success(f"Job status for {job_id}") - else: - result = http_client.get("/v1/jobs") - success(f"All jobs status") - - if format == 'json': - click.echo(json.dumps(result, indent=2)) - else: - if job_id: - click.echo(f"Status: {result.get('state', 'unknown')}") - click.echo(f"Progress: {result.get('progress', '0%')}") - else: - for job in result.get('jobs', []): - click.echo(f" - {job.get('job_id', 'unknown')}: {job.get('state', 'unknown')}") - except NetworkError as e: - error(f"Error getting AI job status: {e}") - except Exception as e: - error(f"Error: {e}") - - -@ai.command() -@click.option('--job-id', help='Specific job ID') -def cancel(job_id: Optional[str]): - """Cancel an AI job""" - if not job_id: - error("Job ID is required") - return - - try: - http_client = AITBCHTTPClient(base_url="http://localhost:9001", timeout=30) - result = http_client.post(f"/v1/jobs/{job_id}/cancel") - success(f"AI job {job_id} cancelled") - except NetworkError as e: - error(f"Error cancelling AI job: {e}") - except Exception as e: - error(f"Error: {e}") - - -# Agent operations -@operations.group() -def agent(): - """Agent operations""" - pass - - -@agent.command() -@click.option('--agent-id', required=True, help='Agent ID') -@click.option('--status', type=click.Choice(['active', 'inactive', 'busy', 'offline']), default='active', help='Agent status') -def register(agent_id: str, status: str): - """Register an agent""" - try: - agent_config = { - "agent_id": agent_id, - "status": status - } - - http_client = AITBCHTTPClient(base_url="http://localhost:9001", timeout=30) - result = http_client.post("/v1/agents/register", json=agent_config) - success(f"Agent {agent_id} registered with status {status}") - except NetworkError as e: - error(f"Error registering agent: {e}") - except Exception as e: - error(f"Error: {e}") - - -@agent.command() -@click.option('--status', help='Filter by status') -@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format') -def list(status: Optional[str], format: str): - """List registered agents""" - try: - import requests - coordinator_url = "http://localhost:9001" - - query = {} - if status: - query["status"] = status - - response = requests.post(f"{coordinator_url}/v1/agents/discover", json=query, timeout=10) - - if response.status_code == 200: - data = response.json() - agents = data.get("agents", []) - success(f"Agents: {len(agents)}") - if format == 'json': - click.echo(json.dumps(agents, indent=2)) - else: - for agent in agents: - click.echo(f" - {agent.get('agent_id', 'unknown')}: {agent.get('status', 'unknown')} - {agent.get('agent_type', 'unknown')}") - else: - error(f"Error listing agents: {response.status_code}") - except Exception as e: - error(f"Error: {e}") - - -@agent.command() -@click.argument('agent_id') -def deregister(agent_id: str): - """Deregister an agent""" - try: - http_client = AITBCHTTPClient(base_url="http://localhost:9001", timeout=30) - result = http_client.post(f"/v1/agents/{agent_id}/deregister") - success(f"Agent {agent_id} deregistered") - except NetworkError as e: - error(f"Error deregistering agent: {e}") - except Exception as e: - error(f"Error: {e}") - - -@agent.command() -@click.option('--agent', required=True, help='Recipient agent address') -@click.option('--message', required=True, help='Message content') -@click.option('--wallet', required=True, help='Wallet name for signing') -@click.option('--password', help='Wallet password') -@click.option('--password-file', help='File containing wallet password') -@click.option('--rpc-url', help='Blockchain RPC URL') -def message(agent: str, message: str, wallet: str, password: Optional[str], password_file: Optional[str], rpc_url: Optional[str]): - """Send message to agent via blockchain transaction""" - if not rpc_url: - rpc_url = DEFAULT_RPC_URL - - # Get password - if password_file: - with open(password_file) as f: - password = f.read().strip() - elif not password: - import getpass - password = getpass.getpass("Enter wallet password: ") - - try: - # Decrypt wallet - keystore_path = DEFAULT_KEYSTORE_DIR / f"{wallet}.json" - private_key_hex = decrypt_private_key(keystore_path, password) - private_key_bytes = bytes.fromhex(private_key_hex) - - # Get sender address - with open(keystore_path) as f: - keystore_data = json.load(f) - sender_address = keystore_data['address'] - - # Create transaction with message as payload - priv_key = ed25519.Ed25519PrivateKey.from_private_bytes(private_key_bytes) - pub_hex = priv_key.public_key().public_bytes( - encoding=serialization.Encoding.Raw, - format=serialization.PublicFormat.Raw - ).hex() - - # Get chain_id - from ..utils.chain_id import get_chain_id - chain_id = get_chain_id(rpc_url) - - # Get actual nonce - try: - http_client = AITBCHTTPClient(base_url=rpc_url, timeout=5) - account_data = http_client.get(f"/rpc/account/{sender_address}") - actual_nonce = account_data.get("nonce", 0) - except Exception: - actual_nonce = 0 - - tx = { - "type": "TRANSFER", - "chain_id": chain_id, - "from": sender_address, - "nonce": actual_nonce, - "fee": 10, - "payload": { - "recipient": agent, - "amount": 0, - "message": message - } - } - - # Sign transaction - tx_string = json.dumps(tx, sort_keys=True) - tx["signature"] = priv_key.sign(tx_string.encode()).hex() - tx["public_key"] = pub_hex - - # Submit transaction - http_client = AITBCHTTPClient(base_url=rpc_url, timeout=30) - result = http_client.post("/rpc/transaction", json=tx) - success(f"Message sent successfully") - click.echo(f"From: {sender_address}") - click.echo(f"To: {agent}") - click.echo(f"Content: {message}") - click.echo(f"TX Hash: {result.get('transaction_hash', 'unknown')}") - except Exception as e: - error(f"Error sending message: {e}") - diff --git a/cli/src/aitbc_cli/commands/resource.py b/cli/src/aitbc_cli/commands/resource.py deleted file mode 100644 index 60b63802..00000000 --- a/cli/src/aitbc_cli/commands/resource.py +++ /dev/null @@ -1,128 +0,0 @@ -""" -Resource management commands for AITBC CLI -""" - -import json -import time -from typing import Optional - -import click - -from ..utils import error, success - - -@click.group() -def resource(): - """Resource management commands (EXPERIMENTAL - use --mock for testing)""" - pass - - -@resource.command() -@click.option('--resource-type', required=True, help='Type of resource (gpu, cpu, storage)') -@click.option('--quantity', type=int, required=True, help='Quantity of resources') -@click.option('--priority', type=click.Choice(['low', 'medium', 'high']), default='medium', help='Allocation priority') -@click.option('--mock', is_flag=True, help='Use mock data for experimental command') -def allocate(resource_type: str, quantity: int, priority: str, mock: bool): - """Allocate resources (EXPERIMENTAL)""" - if not mock: - error("[EXPERIMENTAL] This command uses placeholder logic. Use --mock for testing.") - click.echo("To proceed with mock data, run: aitbc resource allocate --mock") - return 1 - - success(f"Allocate {quantity} {resource_type} with {priority} priority") - # TODO: Implement actual resource allocation via coordinator API - click.echo(f"Allocation ID: alloc_{int(time.time())}") - click.echo(f"Status: Allocated") - click.echo(f"Cost per hour: 25 AIT") - return 0 - - -@resource.command() -@click.option('--resource-id', help='Specific resource ID') -@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format') -@click.option('--mock', is_flag=True, help='Use mock data for experimental command') -def list(resource_id: Optional[str], format: str, mock: bool): - """List allocated resources (EXPERIMENTAL)""" - if not mock: - error("[EXPERIMENTAL] This command uses placeholder logic. Use --mock for testing.") - click.echo("To proceed with mock data, run: aitbc resource list --mock") - return 1 - - success("Allocated resources:") - resources = [ - {"type": "gpu", "allocated": 4, "available": 8, "efficiency": "78.5%"}, - {"type": "cpu", "allocated": "45.2%", "available": "54.8%", "efficiency": "82.1%"}, - {"type": "storage", "allocated": "45GB", "available": "55GB", "efficiency": "90.0%"} - ] - - if format == 'json': - click.echo(json.dumps(resources, indent=2)) - else: - for res in resources: - click.echo(f" - {res['type'].upper()}: {res['allocated']} allocated, {res['available']} available ({res['efficiency']})") - return 0 - - -@resource.command() -@click.argument('resource_id') -@click.option('--mock', is_flag=True, help='Use mock data for experimental command') -def release(resource_id: str, mock: bool): - """Release allocated resources (EXPERIMENTAL)""" - if not mock: - error("[EXPERIMENTAL] This command uses placeholder logic. Use --mock for testing.") - click.echo("To proceed with mock data, run: aitbc resource release --mock") - return 1 - - success(f"Release resource {resource_id}") - # TODO: Implement actual resource release via coordinator API - click.echo("Status: Released") - return 0 - - -@resource.command() -@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format') -@click.option('--mock', is_flag=True, help='Use mock data for experimental command') -def utilization(format: str, mock: bool): - """Get resource utilization metrics (EXPERIMENTAL)""" - if not mock: - error("[EXPERIMENTAL] This command uses placeholder logic. Use --mock for testing.") - click.echo("To proceed with mock data, run: aitbc resource utilization --mock") - return 1 - - success("Resource utilization:") - metrics = { - "cpu_utilization": "45.2%", - "memory_usage": "2.1GB / 8GB (26%)", - "storage_available": "45GB / 100GB", - "network_bandwidth": "120Mbps / 1Gbps", - "active_agents": 3, - "resource_efficiency": "78.5%" - } - - if format == 'json': - click.echo(json.dumps(metrics, indent=2)) - else: - for key, value in metrics.items(): - click.echo(f" {key}: {value}") - return 0 - - -@resource.command() -@click.option('--target', default='all', help='Optimization target (all, cpu, gpu, memory)') -@click.option('--agent-id', help='Specific agent ID') -@click.option('--mock', is_flag=True, help='Use mock data for experimental command') -def optimize(target: str, agent_id: Optional[str], mock: bool): - """Optimize resource allocation (EXPERIMENTAL)""" - if not mock: - error("[EXPERIMENTAL] This command uses placeholder logic. Use --mock for testing.") - click.echo("To proceed with mock data, run: aitbc resource optimize --mock") - return 1 - - success(f"Optimize resources for target: {target}") - if agent_id: - click.echo(f"Agent: {agent_id}") - # TODO: Implement actual optimization logic - click.echo("Optimization score: 85.2%") - click.echo("Improvement: 12.5%") - click.echo("Status: Optimized") - return 0 diff --git a/cli/src/aitbc_cli/commands/simulate.py b/cli/src/aitbc_cli/commands/simulate.py deleted file mode 100644 index e5bb2ae1..00000000 --- a/cli/src/aitbc_cli/commands/simulate.py +++ /dev/null @@ -1,342 +0,0 @@ -#!/usr/bin/env python3 -""" -AITBC CLI - Simulate Command -Simulate blockchain scenarios and test environments -""" - -import click -import json -import time -import random -from typing import Dict, Any, List -import sys -import os - -# Add parent directory to path for imports -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -try: - from utils import output, setup_logging - from config import get_config -except ImportError: - def output(msg, format_type): - click.echo(msg) - def setup_logging(verbose, debug): - return "INFO" - def get_config(config_file=None, role=None): - return {} - - -@click.group() -def simulate(): - """Simulate blockchain scenarios and test environments""" - pass - - -@simulate.command() -@click.option('--blocks', default=10, help='Number of blocks to simulate') -@click.option('--transactions', default=50, help='Number of transactions per block') -@click.option('--delay', default=1.0, help='Delay between blocks (seconds)') -@click.option('--output', default='table', type=click.Choice(['table', 'json', 'yaml'])) -def blockchain(blocks, transactions, delay, output): - """Simulate blockchain block production and transactions""" - click.echo(f"Simulating blockchain with {blocks} blocks, {transactions} transactions per block") - - results = [] - for block_num in range(blocks): - # Simulate block production - block_data = { - 'block_number': block_num + 1, - 'timestamp': time.time(), - 'transactions': [] - } - - # Generate transactions - for tx_num in range(transactions): - tx = { - 'tx_id': f"0x{random.getrandbits(256):064x}", - 'from_address': f"ait{random.getrandbits(160):040x}", - 'to_address': f"ait{random.getrandbits(160):040x}", - 'amount': random.uniform(0.1, 1000.0), - 'fee': random.uniform(0.01, 1.0) - } - block_data['transactions'].append(tx) - - block_data['tx_count'] = len(block_data['transactions']) - block_data['total_amount'] = sum(tx['amount'] for tx in block_data['transactions']) - block_data['total_fees'] = sum(tx['fee'] for tx in block_data['transactions']) - - results.append(block_data) - - # Output block info - if output == 'table': - click.echo(f"Block {block_data['block_number']}: {block_data['tx_count']} txs, " - f"{block_data['total_amount']:.2f} AIT, {block_data['total_fees']:.2f} fees") - else: - click.echo(json.dumps(block_data, indent=2)) - - if delay > 0 and block_num < blocks - 1: - time.sleep(delay) - - # Summary - total_txs = sum(block['tx_count'] for block in results) - total_amount = sum(block['total_amount'] for block in results) - total_fees = sum(block['total_fees'] for block in results) - - click.echo(f"\nSimulation Summary:") - click.echo(f" Total Blocks: {blocks}") - click.echo(f" Total Transactions: {total_txs}") - click.echo(f" Total Amount: {total_amount:.2f} AIT") - click.echo(f" Total Fees: {total_fees:.2f} AIT") - click.echo(f" Average TPS: {total_txs / (blocks * max(delay, 0.1)):.2f}") - - -@simulate.command() -@click.option('--wallets', default=5, help='Number of wallets to create') -@click.option('--balance', default=1000.0, help='Initial balance for each wallet') -@click.option('--transactions', default=20, help='Number of transactions to simulate') -@click.option('--amount-range', default='1.0-100.0', help='Transaction amount range (min-max)') -def wallets(wallets, balance, transactions, amount_range): - """Simulate wallet creation and transactions""" - click.echo(f"Simulating {wallets} wallets with {balance:.2f} AIT initial balance") - - # Parse amount range - try: - min_amount, max_amount = map(float, amount_range.split('-')) - except ValueError: - min_amount, max_amount = 1.0, 100.0 - - # Create wallets - created_wallets = [] - for i in range(wallets): - wallet = { - 'name': f'sim_wallet_{i+1}', - 'address': f"ait{random.getrandbits(160):040x}", - 'balance': balance - } - created_wallets.append(wallet) - click.echo(f"Created wallet {wallet['name']}: {wallet['address']} with {balance:.2f} AIT") - - # Simulate transactions - click.echo(f"\nSimulating {transactions} transactions...") - for i in range(transactions): - # Random sender and receiver - sender = random.choice(created_wallets) - receiver = random.choice([w for w in created_wallets if w != sender]) - - # Random amount - amount = random.uniform(min_amount, max_amount) - - # Check if sender has enough balance - if sender['balance'] >= amount: - sender['balance'] -= amount - receiver['balance'] += amount - - click.echo(f"Tx {i+1}: {sender['name']} -> {receiver['name']}: {amount:.2f} AIT") - else: - click.echo(f"Tx {i+1}: {sender['name']} -> {receiver['name']}: FAILED (insufficient balance)") - - # Final balances - click.echo(f"\nFinal Wallet Balances:") - for wallet in created_wallets: - click.echo(f" {wallet['name']}: {wallet['balance']:.2f} AIT") - - -@simulate.command() -@click.option('--price', default=100.0, help='Starting AIT price') -@click.option('--volatility', default=0.05, help='Price volatility (0.0-1.0)') -@click.option('--timesteps', default=100, help='Number of timesteps to simulate') -@click.option('--delay', default=0.1, help='Delay between timesteps (seconds)') -def price(price, volatility, timesteps, delay): - """Simulate AIT price movements""" - click.echo(f"Simulating AIT price from {price:.2f} with {volatility:.2f} volatility") - - current_price = price - prices = [current_price] - - for step in range(timesteps): - # Random price change - change_percent = random.uniform(-volatility, volatility) - current_price = current_price * (1 + change_percent) - - # Ensure price doesn't go negative - current_price = max(current_price, 0.01) - - prices.append(current_price) - - click.echo(f"Step {step+1}: {current_price:.4f} AIT ({change_percent:+.2%})") - - if delay > 0 and step < timesteps - 1: - time.sleep(delay) - - # Statistics - min_price = min(prices) - max_price = max(prices) - avg_price = sum(prices) / len(prices) - - click.echo(f"\nPrice Statistics:") - click.echo(f" Starting Price: {price:.4f} AIT") - click.echo(f" Ending Price: {current_price:.4f} AIT") - click.echo(f" Minimum Price: {min_price:.4f} AIT") - click.echo(f" Maximum Price: {max_price:.4f} AIT") - click.echo(f" Average Price: {avg_price:.4f} AIT") - click.echo(f" Total Change: {((current_price - price) / price * 100):+.2f}%") - - -@simulate.command() -@click.option('--nodes', default=3, help='Number of nodes to simulate') -@click.option('--network-delay', default=0.1, help='Network delay in seconds') -@click.option('--failure-rate', default=0.05, help='Node failure rate (0.0-1.0)') -def network(nodes, network_delay, failure_rate): - """Simulate network topology and node failures""" - click.echo(f"Simulating network with {nodes} nodes, {network_delay}s delay, {failure_rate:.2f} failure rate") - - # Create nodes - network_nodes = [] - for i in range(nodes): - node = { - 'id': f'node_{i+1}', - 'address': f"10.1.223.{90+i}", - 'status': 'active', - 'height': 0, - 'connected_to': [] - } - network_nodes.append(node) - - # Create network topology (ring + mesh) - for i, node in enumerate(network_nodes): - # Connect to next node (ring) - next_node = network_nodes[(i + 1) % len(network_nodes)] - node['connected_to'].append(next_node['id']) - - # Connect to random nodes (mesh) - if len(network_nodes) > 2: - mesh_connections = random.sample([n['id'] for n in network_nodes if n['id'] != node['id']], - min(2, len(network_nodes) - 1)) - for conn in mesh_connections: - if conn not in node['connected_to']: - node['connected_to'].append(conn) - - # Display network topology - click.echo(f"\nNetwork Topology:") - for node in network_nodes: - click.echo(f" {node['id']} ({node['address']}): connected to {', '.join(node['connected_to'])}") - - # Simulate network operations - click.echo(f"\nSimulating network operations...") - active_nodes = network_nodes.copy() - - for step in range(10): - # Simulate failures - for node in active_nodes: - if random.random() < failure_rate: - node['status'] = 'failed' - click.echo(f"Step {step+1}: {node['id']} failed") - - # Remove failed nodes - active_nodes = [n for n in active_nodes if n['status'] == 'active'] - - # Simulate block propagation - if active_nodes: - # Random node produces block - producer = random.choice(active_nodes) - producer['height'] += 1 - - # Propagate to connected nodes - for node in active_nodes: - if node['id'] != producer['id'] and node['id'] in producer['connected_to']: - node['height'] = max(node['height'], producer['height'] - 1) - - click.echo(f"Step {step+1}: {producer['id']} produced block {producer['height']}, " - f"{len(active_nodes)} nodes active") - - time.sleep(network_delay) - - # Final network status - click.echo(f"\nFinal Network Status:") - for node in network_nodes: - status_icon = "āœ…" if node['status'] == 'active' else "āŒ" - click.echo(f" {status_icon} {node['id']}: height {node['height']}, " - f"connections: {len(node['connected_to'])}") - - -@simulate.command() -@click.option('--jobs', default=10, help='Number of AI jobs to simulate') -@click.option('--models', default='text-generation,image-generation', help='Available models (comma-separated)') -@click.option('--duration-range', default='30-300', help='Job duration range in seconds (min-max)') -def ai_jobs(jobs, models, duration_range): - """Simulate AI job submission and processing""" - click.echo(f"Simulating {jobs} AI jobs with models: {models}") - - # Parse models - model_list = [m.strip() for m in models.split(',')] - - # Parse duration range - try: - min_duration, max_duration = map(int, duration_range.split('-')) - except ValueError: - min_duration, max_duration = 30, 300 - - # Simulate job submission - submitted_jobs = [] - for i in range(jobs): - job = { - 'job_id': f"job_{i+1:03d}", - 'model': random.choice(model_list), - 'status': 'queued', - 'submit_time': time.time(), - 'duration': random.randint(min_duration, max_duration), - 'wallet': f"wallet_{random.randint(1, 5):03d}" - } - submitted_jobs.append(job) - - click.echo(f"Submitted job {job['job_id']}: {job['model']} (est. {job['duration']}s)") - - # Simulate job processing - click.echo(f"\nSimulating job processing...") - processing_jobs = submitted_jobs.copy() - completed_jobs = [] - - current_time = time.time() - while processing_jobs and current_time < time.time() + 600: # Max 10 minutes - current_time = time.time() - - for job in processing_jobs[:]: - if job['status'] == 'queued' and current_time - job['submit_time'] > 5: - job['status'] = 'running' - job['start_time'] = current_time - click.echo(f"Started {job['job_id']}") - - elif job['status'] == 'running': - if current_time - job['start_time'] >= job['duration']: - job['status'] = 'completed' - job['end_time'] = current_time - job['actual_duration'] = job['end_time'] - job['start_time'] - processing_jobs.remove(job) - completed_jobs.append(job) - click.echo(f"Completed {job['job_id']} in {job['actual_duration']:.1f}s") - - time.sleep(1) # Check every second - - # Job statistics - click.echo(f"\nJob Statistics:") - click.echo(f" Total Jobs: {jobs}") - click.echo(f" Completed Jobs: {len(completed_jobs)}") - click.echo(f" Failed Jobs: {len(processing_jobs)}") - - if completed_jobs: - avg_duration = sum(job['actual_duration'] for job in completed_jobs) / len(completed_jobs) - click.echo(f" Average Duration: {avg_duration:.1f}s") - - # Model statistics - model_stats = {} - for job in completed_jobs: - model_stats[job['model']] = model_stats.get(job['model'], 0) + 1 - - click.echo(f" Model Usage:") - for model, count in model_stats.items(): - click.echo(f" {model}: {count} jobs") - - -if __name__ == '__main__': - simulate() diff --git a/cli/src/aitbc_cli/commands/system.py b/cli/src/aitbc_cli/commands/system.py deleted file mode 100644 index 51bb0e89..00000000 --- a/cli/src/aitbc_cli/commands/system.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python3 -""" -System commands for AITBC CLI -""" - -import click -import os - -@click.group() -def system(): - """System management commands""" - pass - -@system.command() -def architect(): - """System architecture analysis""" - click.echo("=== AITBC System Architecture ===") - click.echo("āœ… Data: /var/lib/aitbc/data") - click.echo("āœ… Config: /etc/aitbc") - click.echo("āœ… Logs: /var/log/aitbc") - click.echo("āœ… Repository: Clean") - -@system.command() -def audit(): - """Audit system compliance""" - click.echo("=== System Audit ===") - click.echo("FHS Compliance: āœ…") - click.echo("Repository Clean: āœ…") - click.echo("Service Health: āœ…") - - -@system.command() -@click.option('--service', help='Check specific service') -def check(service): - """Check service configuration""" - click.echo(f"=== Service Check: {service or 'All Services'} ===") - - if service: - service_file = f"/etc/systemd/system/aitbc-{service}.service" - if os.path.exists(service_file): - click.echo(f"āœ… Service file exists: {service_file}") - else: - click.echo(f"āŒ Service file missing: {service_file}") - else: - services = ['marketplace', 'mining-blockchain', 'hermes-ai', 'blockchain-node'] - for svc in services: - service_file = f"/etc/systemd/system/aitbc-{svc}.service" - if os.path.exists(service_file): - click.echo(f"āœ… {svc}: {service_file}") - else: - click.echo(f"āŒ {svc}: {service_file}") - -if __name__ == '__main__': - system() diff --git a/cli/src/aitbc_cli/commands/system_architect.py b/cli/src/aitbc_cli/commands/system_architect.py deleted file mode 100644 index 877e595a..00000000 --- a/cli/src/aitbc_cli/commands/system_architect.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python3 -""" -AITBC CLI System Architect Command -""" - -import click - -@click.group() -def system_architect(): - """System architecture analysis and FHS compliance management""" - pass - -@system_architect.command() -def audit(): - """Audit system architecture compliance""" - click.echo("=== AITBC System Architecture Audit ===") - click.echo("āœ… Data: /var/lib/aitbc/data") - click.echo("āœ… Config: /etc/aitbc") - click.echo("āœ… Logs: /var/log/aitbc") - click.echo("āœ… Repository: Clean") - -@system_architect.command() -def paths(): - """Show system architecture paths""" - click.echo("=== AITBC System Architecture Paths ===") - click.echo("Data: /var/lib/aitbc/data") - click.echo("Config: /etc/aitbc") - click.echo("Logs: /var/log/aitbc") - click.echo("Repository: /opt/aitbc (code only)") - -@system_architect.command() -@click.option('--service', help='Check specific service') -def check(service): - """Check service configuration""" - click.echo(f"=== Service Check: {service or 'All Services'} ===") - if service: - click.echo(f"Checking service: {service}") - else: - click.echo("Checking all services") - -if __name__ == '__main__': - system_architect() diff --git a/cli/src/aitbc_cli/commands/transactions.py b/cli/src/aitbc_cli/commands/transactions.py deleted file mode 100644 index a02e49ed..00000000 --- a/cli/src/aitbc_cli/commands/transactions.py +++ /dev/null @@ -1,273 +0,0 @@ -""" -Transaction commands for AITBC CLI -""" - -import json -from pathlib import Path -from typing import Optional, Dict, Any, List - -import click - -from ..utils import error, success -from ..utils.wallet import decrypt_private_key -from aitbc import AITBCHTTPClient, NetworkError, KEYSTORE_DIR, get_logger -from aitbc.exceptions import ValidationError -from aitbc.utils.validation import validate_address -from cryptography.hazmat.primitives.asymmetric import ed25519 - -logger = get_logger(__name__) - -DEFAULT_RPC_URL = "http://localhost:8006" -DEFAULT_KEYSTORE_DIR = KEYSTORE_DIR - - -@click.group() -def transactions(): - """Transaction management commands""" - pass - - -def _send_transaction_impl(from_wallet: str, to_address: str, amount: float, fee: float, - password: str, keystore_dir: Path = DEFAULT_KEYSTORE_DIR, - rpc_url: str = DEFAULT_RPC_URL) -> Optional[str]: - """Send transaction from one wallet to another""" - - # Validate recipient address - try: - validate_address(to_address) - except ValidationError as e: - logger.error(f"Invalid recipient address: {e}") - error(f"Invalid recipient address: {e}") - return None - - # Validate amount - if amount <= 0: - logger.error(f"Invalid amount: {amount} must be positive") - error("Amount must be positive") - return None - - # Ensure keystore_dir is a Path object - if keystore_dir is None: - keystore_dir = DEFAULT_KEYSTORE_DIR - if isinstance(keystore_dir, str): - keystore_dir = Path(keystore_dir) - - # Get sender wallet info - sender_keystore = keystore_dir / f"{from_wallet}.json" - if not sender_keystore.exists(): - error(f"Wallet '{from_wallet}' not found") - return None - - with open(sender_keystore) as f: - sender_data = json.load(f) - - sender_address = sender_data['address'] - - # Decrypt private key - try: - private_key_hex = decrypt_private_key(sender_keystore, password) - private_key = ed25519.Ed25519PrivateKey.from_private_bytes(bytes.fromhex(private_key_hex)) - except Exception as e: - error(f"Error decrypting wallet: {e}") - return None - - # Get chain_id from RPC health endpoint or use override - from ..utils.chain_id import get_chain_id - chain_id = get_chain_id(rpc_url, override=None, timeout=5) - - # Get actual nonce from blockchain - actual_nonce = 0 - try: - http_client = AITBCHTTPClient(base_url=rpc_url, timeout=5) - account_data = http_client.get(f"/rpc/account/{sender_address}") - actual_nonce = account_data.get("nonce", 0) - except NetworkError: - actual_nonce = 0 - except Exception: - actual_nonce = 0 - - # Create transaction - transaction = { - "type": "TRANSFER", - "chain_id": chain_id, - "from": sender_address, - "nonce": actual_nonce, - "fee": int(fee), - "payload": { - "recipient": to_address, - "amount": int(amount) - } - } - - # Sign transaction - message = json.dumps(transaction, sort_keys=True).encode() - signature = private_key.sign(message) - transaction["signature"] = signature.hex() - - # Submit to blockchain - try: - http_client = AITBCHTTPClient(base_url=rpc_url, timeout=30) - result = http_client.post("/rpc/transaction", json=transaction) - tx_hash = result.get("transaction_hash") - success(f"Transaction submitted: {tx_hash}") - logger.info(f"Transaction submitted: {tx_hash} from {from_wallet} to {to_address}") - return tx_hash - except NetworkError as e: - logger.error(f"Network error submitting transaction: {e}") - error(f"Error submitting transaction: {e}") - return None - except Exception as e: - logger.error(f"Error submitting transaction: {e}") - error(f"Error: {e}") - return None - - -@transactions.command() -@click.option('--from', 'from_wallet', required=True, help='From wallet name') -@click.option('--to', 'to_address', required=True, help='To address') -@click.option('--amount', type=float, required=True, help='Amount to send') -@click.option('--fee', type=float, default=0.001, help='Transaction fee') -@click.option('--password', help='Wallet password') -@click.option('--password-file', help='File containing wallet password') -@click.option('--rpc-url', help='Blockchain RPC URL') -def send(from_wallet: str, to_address: str, amount: float, fee: float, password: Optional[str], password_file: Optional[str], rpc_url: Optional[str]): - """Send transaction from one wallet to another""" - if password_file: - with open(password_file) as f: - password = f.read().strip() - elif not password: - import getpass - password = getpass.getpass("Enter wallet password: ") - - if not rpc_url: - rpc_url = DEFAULT_RPC_URL - - tx_hash = _send_transaction_impl(from_wallet, to_address, amount, fee, password, rpc_url=rpc_url) - if tx_hash: - success(f"Transaction sent: {tx_hash}") - - -@transactions.command() -@click.option('--transactions-file', required=True, help='JSON file with batch transactions') -@click.option('--password', help='Wallet password') -@click.option('--password-file', help='File containing wallet password') -@click.option('--rpc-url', help='Blockchain RPC URL') -def batch(transactions_file: str, password: Optional[str], password_file: Optional[str], rpc_url: Optional[str]): - """Send batch transactions""" - if password_file: - with open(password_file) as f: - password = f.read().strip() - elif not password: - import getpass - password = getpass.getpass("Enter wallet password: ") - - if not rpc_url: - rpc_url = DEFAULT_RPC_URL - - with open(transactions_file) as f: - transactions_data = json.load(f) - - results = [] - for tx in transactions_data: - try: - tx_hash = _send_transaction_impl( - tx['from_wallet'], - tx['to_address'], - tx['amount'], - tx.get('fee', 10.0), - password, - rpc_url=rpc_url - ) - results.append({ - 'transaction': tx, - 'hash': tx_hash, - 'success': tx_hash is not None - }) - - if tx_hash: - success(f"Transaction sent: {tx['from_wallet']} → {tx['to_address']} ({tx['amount']} AIT)") - else: - error(f"Transaction failed: {tx['from_wallet']} → {tx['to_address']}") - - except Exception as e: - results.append({ - 'transaction': tx, - 'hash': None, - 'success': False, - 'error': str(e) - }) - error(f"Transaction error: {e}") - - success(f"Batch completed: {len([r for r in results if r['success']])}/{len(results)} successful") - - -@transactions.command() -@click.argument('tx_hash') -@click.option('--rpc-url', help='Blockchain RPC URL') -def status(tx_hash: str, rpc_url: Optional[str]): - """Get transaction status""" - if not rpc_url: - rpc_url = DEFAULT_RPC_URL - - try: - http_client = AITBCHTTPClient(base_url=rpc_url, timeout=30) - result = http_client.get(f"/rpc/transaction/{tx_hash}") - success(f"Transaction status for {tx_hash}") - click.echo(json.dumps(result, indent=2)) - except NetworkError as e: - error(f"Error getting transaction status: {e}") - except Exception as e: - error(f"Error: {e}") - - -@transactions.command() -@click.option('--rpc-url', help='Blockchain RPC URL') -def pending(rpc_url: Optional[str]): - """Get pending transactions""" - if not rpc_url: - rpc_url = DEFAULT_RPC_URL - - try: - http_client = AITBCHTTPClient(base_url=rpc_url, timeout=30) - data = http_client.get("/rpc/pending") - transactions = data.get("transactions", []) - success(f"Pending transactions: {len(transactions)}") - for tx in transactions: - click.echo(f" - {tx.get('hash', 'unknown')}: {tx.get('amount', 0)} AIT") - except NetworkError as e: - error(f"Error getting pending transactions: {e}") - except Exception as e: - error(f"Error: {e}") - - -@transactions.command() -@click.option('--from', 'from_wallet', required=True, help='From wallet name') -@click.option('--to', 'to_address', required=True, help='To address') -@click.option('--amount', type=float, required=True, help='Amount to send') -@click.option('--rpc-url', help='Blockchain RPC URL') -def estimate_fee(from_wallet: str, to_address: str, amount: float, rpc_url: Optional[str]): - """Estimate transaction fee""" - if not rpc_url: - rpc_url = DEFAULT_RPC_URL - - try: - test_tx = { - "sender": "", - "recipient": to_address, - "value": int(amount), - "fee": 10, - "nonce": 0, - "type": "transfer", - "payload": {} - } - - try: - http_client = AITBCHTTPClient(base_url=rpc_url, timeout=10) - fee_data = http_client.post("/rpc/estimateFee", json=test_tx) - estimated_fee = fee_data.get("estimated_fee", 10.0) - success(f"Estimated fee: {estimated_fee} AIT") - except NetworkError: - success(f"Estimated fee: 10.0 AIT (default)") - except Exception as e: - error(f"Error estimating fee: {e}") - success(f"Estimated fee: 10.0 AIT (default)") diff --git a/cli/src/aitbc_cli/commands/wallet.py b/cli/src/aitbc_cli/commands/wallet.py deleted file mode 100644 index d6f9cf34..00000000 --- a/cli/src/aitbc_cli/commands/wallet.py +++ /dev/null @@ -1,1537 +0,0 @@ -"""Wallet commands for AITBC CLI""" - -import click -import json -import os -import shutil -import yaml -from pathlib import Path -from typing import Optional, Dict, Any, List -from datetime import datetime, timezone, timedelta -from ..utils import output, error, success -from ..config import get_config -import getpass - -# Import shared modules -from aitbc import get_logger, AITBCHTTPClient, NetworkError, KEYSTORE_DIR - -# Initialize logger -logger = get_logger(__name__) - - -def encrypt_value(value: str, password: str) -> str: - """Simple encryption for wallet data (placeholder)""" - # For now, return the value as-is since daemon mode doesn't need this - return value - - -def decrypt_value(encrypted: str, password: str) -> str: - """Simple decryption for wallet data (placeholder)""" - # For now, return the value as-is since daemon mode doesn't need this - return encrypted - - -def _get_wallet_password(wallet_name: str) -> str: - """Get or prompt for wallet encryption password""" - # Try to get from keyring first - try: - import keyring - - password = keyring.get_password("aitbc-wallet", wallet_name) - if password: - return password - except Exception: - pass - - # Prompt for password - while True: - password = getpass.getpass(f"Enter password for wallet '{wallet_name}': ") - if not password: - error("Password cannot be empty") - continue - - confirm = getpass.getpass("Confirm password: ") - if password != confirm: - error("Passwords do not match") - continue - - # Store in keyring for future use - try: - import keyring - - keyring.set_password("aitbc-wallet", wallet_name, password) - except Exception: - pass - - return password - - -def _save_wallet(wallet_path: Path, wallet_data: Dict[str, Any], password: str = None): - """Save wallet with encrypted private key""" - # Encrypt private key if provided - if password and "private_key" in wallet_data: - wallet_data["private_key"] = encrypt_value(wallet_data["private_key"], password) - wallet_data["encrypted"] = True - - # Save wallet - with open(wallet_path, "w") as f: - json.dump(wallet_data, f, indent=2) - - -def _load_wallet(wallet_path: Path, wallet_name: str) -> Dict[str, Any]: - """Load wallet and decrypt private key if needed""" - with open(wallet_path, "r") as f: - wallet_data = json.load(f) - - # Decrypt private key if encrypted - if wallet_data.get("encrypted") and "private_key" in wallet_data: - password = _get_wallet_password(wallet_name) - try: - wallet_data["private_key"] = decrypt_value( - wallet_data["private_key"], password - ) - except Exception: - error("Invalid password for wallet") - raise click.Abort() - - return wallet_data - - -@click.group() -@click.option("--wallet-name", help="Name of the wallet to use") -@click.option( - "--wallet-path", help="Direct path to wallet file (overrides --wallet-name)" -) -@click.option("--use-daemon", is_flag=True, default=True, help="Use wallet daemon for operations") -@click.option("--chain-id", help="Chain ID for multichain operations (e.g., ait-mainnet, ait-devnet)") -@click.pass_context -def wallet(ctx, wallet_name: Optional[str], wallet_path: Optional[str], use_daemon: bool, chain_id: Optional[str]): - """Manage your AITBC wallets and transactions""" - # Ensure wallet object exists - ctx.ensure_object(dict) - - # Set daemon mode - ctx.obj["use_daemon"] = use_daemon - - # Handle chain_id with auto-detection - from ..utils.chain_id import get_chain_id - config = get_config() - default_rpc_url = config.blockchain_rpc_url if hasattr(config, 'blockchain_rpc_url') else 'http://localhost:8006' - ctx.obj["chain_id"] = get_chain_id(default_rpc_url, override=chain_id) - - # Initialize dual-mode adapter - import sys - from pathlib import Path - sys.path.insert(0, str(Path(__file__).parent.parent.parent)) - from utils.dual_mode_wallet_adapter import DualModeWalletAdapter - - config = get_config() - adapter = DualModeWalletAdapter(config, use_daemon=use_daemon) - ctx.obj["wallet_adapter"] = adapter - - # If direct wallet path is provided, use it - if wallet_path: - wp = Path(wallet_path) - wp.parent.mkdir(parents=True, exist_ok=True) - ctx.obj["wallet_name"] = wp.stem - ctx.obj["wallet_dir"] = wp.parent - ctx.obj["wallet_path"] = wp - return - - # Set wallet directory - wallet_dir = Path.home() / ".aitbc" / "wallets" - wallet_dir.mkdir(parents=True, exist_ok=True) - - # Set active wallet - if not wallet_name: - # Try to get from config or use 'default' - config_file = Path.home() / ".aitbc" / "config.yaml" - if config_file.exists(): - with open(config_file, "r") as f: - config = yaml.safe_load(f) - if config: - wallet_name = config.get("active_wallet", "default") - else: - wallet_name = "default" - else: - wallet_name = "default" - - ctx.obj["wallet_name"] = wallet_name - ctx.obj["wallet_dir"] = wallet_dir - ctx.obj["wallet_path"] = wallet_dir / f"{wallet_name}.json" - - -@wallet.command() -@click.argument("name") -@click.option("--type", "wallet_type", default="hd", help="Wallet type (hd, simple)") -@click.option( - "--no-encrypt", is_flag=True, help="Skip wallet encryption (not recommended)" -) -@click.pass_context -def create(ctx, name: str, wallet_type: str, no_encrypt: bool): - """Create a new wallet""" - wallet_dir = ctx.obj["wallet_dir"] - wallet_path = wallet_dir / f"{name}.json" - - if wallet_path.exists(): - error(f"Wallet '{name}' already exists") - return - - # Generate new wallet - if wallet_type == "hd": - # Hierarchical Deterministic wallet - import secrets - from cryptography.hazmat.primitives import hashes - from cryptography.hazmat.primitives.asymmetric import ec - from cryptography.hazmat.primitives.serialization import ( - Encoding, - PublicFormat, - NoEncryption, - PrivateFormat, - ) - import base64 - - # Generate private key - private_key_bytes = secrets.token_bytes(32) - private_key = f"0x{private_key_bytes.hex()}" - - # Derive public key from private key using ECDSA - priv_key = ec.derive_private_key( - int.from_bytes(private_key_bytes, "big"), ec.SECP256K1() - ) - pub_key = priv_key.public_key() - pub_key_bytes = pub_key.public_bytes( - encoding=Encoding.X962, format=PublicFormat.UncompressedPoint - ) - public_key = f"0x{pub_key_bytes.hex()}" - - # Generate address from public key (simplified) - digest = hashes.Hash(hashes.SHA256()) - digest.update(pub_key_bytes) - address_hash = digest.finalize() - address = f"aitbc1{address_hash[:20].hex()}" - else: - # Simple wallet - import secrets - - private_key = f"0x{secrets.token_hex(32)}" - public_key = f"0x{secrets.token_hex(32)}" - address = f"aitbc1{secrets.token_hex(20)}" - - wallet_data = { - "wallet_id": name, - "type": wallet_type, - "address": address, - "public_key": public_key, - "private_key": private_key, - "created_at": datetime.now(timezone.utc).isoformat() + "Z", - "balance": 0, - "transactions": [], - } - - # Get password for encryption unless skipped - password = None - if not no_encrypt: - success( - "Wallet encryption is enabled. Your private key will be encrypted at rest." - ) - password = _get_wallet_password(name) - - # Save wallet - _save_wallet(wallet_path, wallet_data, password) - - success(f"Wallet '{name}' created successfully") - output( - { - "name": name, - "type": wallet_type, - "address": address, - "path": str(wallet_path), - }, - ctx.obj.get("output_format", "table"), - ) - - -@wallet.command() -@click.pass_context -def list(ctx): - """List all wallets""" - adapter = ctx.obj["wallet_adapter"] - use_daemon = ctx.obj["use_daemon"] - - # Check if using daemon mode and daemon is available - if use_daemon and not adapter.is_daemon_available(): - error("Wallet daemon is not available. Falling back to file-based wallet listing.") - # Switch to file mode - from ..config import get_config - import sys - sys.path.insert(0, '/opt/aitbc/cli') - from utils.dual_mode_wallet_adapter import DualModeWalletAdapter - config = get_config() - adapter = DualModeWalletAdapter(config, use_daemon=False) - - try: - wallets = adapter.list_wallets() - - if not wallets: - output("No wallets found") - return - - # Format output - output_format = ctx.obj.get("output_format", "table") - if output_format == "json": - import json - output(json.dumps(wallets, indent=2)) - elif output_format == "yaml": - import yaml - output(yaml.dump(wallets, default_flow_style=False)) - else: - # Table format - for wallet in wallets: - wallet_name = wallet.get("wallet_name", wallet.get("name", "unknown")) - wallet_address = wallet.get("address", "") - output(f"{wallet_name}: {wallet_address}") - except Exception as e: - error(f"Failed to list wallets: {str(e)}") - - -@wallet.command() -@click.argument("name") -@click.pass_context -def switch(ctx, name: str): - """Switch to a different wallet""" - wallet_dir = ctx.obj["wallet_dir"] - wallet_path = wallet_dir / f"{name}.json" - - if not wallet_path.exists(): - error(f"Wallet '{name}' does not exist") - return - - # Update config - config_file = Path.home() / ".aitbc" / "config.yaml" - config = {} - - if config_file.exists(): - import yaml - - with open(config_file, "r") as f: - config = yaml.safe_load(f) or {} - - config["active_wallet"] = name - - # Save config - config_file.parent.mkdir(parents=True, exist_ok=True) - with open(config_file, "w") as f: - yaml.dump(config, f, default_flow_style=False) - - success(f"Switched to wallet '{name}'") - # Load wallet to get address (will handle encryption) - wallet_data = _load_wallet(wallet_path, name) - output( - {"active_wallet": name, "address": wallet_data["address"]}, - ctx.obj.get("output_format", "table"), - ) - - -@wallet.command() -@click.argument("name") -@click.option("--confirm", is_flag=True, help="Skip confirmation prompt") -@click.pass_context -def delete(ctx, name: str, confirm: bool): - """Delete a wallet""" - wallet_dir = ctx.obj["wallet_dir"] - wallet_path = wallet_dir / f"{name}.json" - - if not wallet_path.exists(): - error(f"Wallet '{name}' does not exist") - return - - if not confirm: - if not click.confirm( - f"Are you sure you want to delete wallet '{name}'? This cannot be undone." - ): - return - - wallet_path.unlink() - success(f"Wallet '{name}' deleted") - - # If deleted wallet was active, reset to default - config_file = Path.home() / ".aitbc" / "config.yaml" - if config_file.exists(): - import yaml - - with open(config_file, "r") as f: - config = yaml.safe_load(f) or {} - - if config.get("active_wallet") == name: - config["active_wallet"] = "default" - with open(config_file, "w") as f: - yaml.dump(config, f, default_flow_style=False) - - -@wallet.command() -@click.argument("name") -@click.option("--destination", help="Destination path for backup file") -@click.pass_context -def backup(ctx, name: str, destination: Optional[str]): - """Backup a wallet""" - wallet_dir = ctx.obj["wallet_dir"] - wallet_path = wallet_dir / f"{name}.json" - - if not wallet_path.exists(): - error(f"Wallet '{name}' does not exist") - return - - if not destination: - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - destination = f"{name}_backup_{timestamp}.json" - - # Copy wallet file - shutil.copy2(wallet_path, destination) - success(f"Wallet '{name}' backed up to '{destination}'") - output( - { - "wallet": name, - "backup_path": destination, - "timestamp": datetime.now(timezone.utc).isoformat() + "Z", - } - ) - - -@wallet.command() -@click.argument("backup_path") -@click.argument("name") -@click.option("--force", is_flag=True, help="Override existing wallet") -@click.pass_context -def restore(ctx, backup_path: str, name: str, force: bool): - """Restore a wallet from backup""" - wallet_dir = ctx.obj["wallet_dir"] - wallet_path = wallet_dir / f"{name}.json" - - if wallet_path.exists() and not force: - error(f"Wallet '{name}' already exists. Use --force to override.") - return - - if not Path(backup_path).exists(): - error(f"Backup file '{backup_path}' not found") - return - - # Load and verify backup - with open(backup_path, "r") as f: - wallet_data = json.load(f) - - # Update wallet name if needed - wallet_data["wallet_id"] = name - wallet_data["restored_at"] = datetime.now(timezone.utc).isoformat() + "Z" - - # Save restored wallet (preserve encryption state) - # If wallet was encrypted, we save it as-is (still encrypted with original password) - with open(wallet_path, "w") as f: - json.dump(wallet_data, f, indent=2) - - success(f"Wallet '{name}' restored from backup") - output( - { - "wallet": name, - "restored_from": backup_path, - "address": wallet_data["address"], - } - ) - - -@wallet.command() -@click.pass_context -def info(ctx): - """Show current wallet information""" - wallet_name = ctx.obj["wallet_name"] - wallet_path = ctx.obj["wallet_path"] - config_file = Path.home() / ".aitbc" / "config.yaml" - - if not wallet_path.exists(): - error( - f"Wallet '{wallet_name}' not found. Use 'aitbc wallet create' to create one." - ) - return - - wallet_data = _load_wallet(wallet_path, wallet_name) - - # Get active wallet from config - active_wallet = "default" - if config_file.exists(): - import yaml - - with open(config_file, "r") as f: - config = yaml.safe_load(f) - active_wallet = config.get("active_wallet", "default") - - wallet_info = { - "name": wallet_data["wallet_id"], - "type": wallet_data.get("type", "simple"), - "address": wallet_data["address"], - "public_key": wallet_data["public_key"], - "created_at": wallet_data["created_at"], - "active": wallet_data["wallet_id"] == active_wallet, - "path": str(wallet_path), - } - - if "balance" in wallet_data: - wallet_info["balance"] = wallet_data["balance"] - - output(wallet_info, ctx.obj.get("output_format", "table")) - - -@wallet.command() -@click.pass_context -def balance(ctx): - """Check wallet balance""" - wallet_name = ctx.obj["wallet_name"] - wallet_path = ctx.obj["wallet_path"] - config = ctx.obj.get("config") - - # Auto-create wallet if it doesn't exist - if not wallet_path.exists(): - import secrets - from cryptography.hazmat.primitives import hashes - from cryptography.hazmat.primitives.asymmetric import ec - from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat - - # Generate proper key pair - private_key_bytes = secrets.token_bytes(32) - private_key = f"0x{private_key_bytes.hex()}" - - # Derive public key from private key - priv_key = ec.derive_private_key( - int.from_bytes(private_key_bytes, "big"), ec.SECP256K1() - ) - pub_key = priv_key.public_key() - pub_key_bytes = pub_key.public_bytes( - encoding=Encoding.X962, format=PublicFormat.UncompressedPoint - ) - public_key = f"0x{pub_key_bytes.hex()}" - - # Generate address from public key - digest = hashes.Hash(hashes.SHA256()) - digest.update(pub_key_bytes) - address_hash = digest.finalize() - address = f"aitbc1{address_hash[:20].hex()}" - - wallet_data = { - "wallet_id": wallet_name, - "type": "simple", - "address": address, - "public_key": public_key, - "private_key": private_key, - "created_at": datetime.now(timezone.utc).isoformat() + "Z", - "balance": 0.0, - "transactions": [], - } - wallet_path.parent.mkdir(parents=True, exist_ok=True) - # Auto-create without prompt in balance command - if ctx.obj.get("output_format", "table") == "table": - success("Creating new wallet") - _save_wallet(wallet_path, wallet_data, None) - else: - wallet_data = _load_wallet(wallet_path, wallet_name) - - # Try to get balance from blockchain if available - if config: - try: - http_client = AITBCHTTPClient( - base_url=config.coordinator_url.replace('/api', ''), - timeout=5 - ) - chain_id = ctx.obj.get("chain_id", "ait-mainnet") - blockchain_balance = http_client.get(f"/rpc/balance/{wallet_data['address']}?chain_id={chain_id}") - output( - { - "wallet": wallet_name, - "address": wallet_data["address"], - "local_balance": wallet_data.get("balance", 0), - "blockchain_balance": blockchain_balance, - "synced": wallet_data.get("balance", 0) - == blockchain_balance, - }, - ctx.obj.get("output_format", "table"), - ) - return - except Exception: - pass - - # Fallback to local balance only - output( - { - "wallet": wallet_name, - "address": wallet_data["address"], - "balance": wallet_data.get("balance", 0), - "note": "Local balance only (blockchain not accessible)", - }, - ctx.obj.get("output_format", "table"), - ) - - -@wallet.command() -@click.option("--limit", type=int, default=10, help="Number of transactions to show") -@click.pass_context -def history(ctx, limit: int): - """Show transaction history""" - wallet_name = ctx.obj["wallet_name"] - wallet_path = ctx.obj["wallet_path"] - - if not wallet_path.exists(): - error(f"Wallet '{wallet_name}' not found") - return - - wallet_data = _load_wallet(wallet_path, wallet_name) - - transactions = wallet_data.get("transactions", [])[-limit:] - - # Format transactions - formatted_txs = [] - for tx in transactions: - formatted_txs.append( - { - "type": tx["type"], - "amount": tx["amount"], - "description": tx.get("description", ""), - "timestamp": tx["timestamp"], - } - ) - - output( - { - "wallet": wallet_name, - "address": wallet_data["address"], - "transactions": formatted_txs, - }, - ctx.obj.get("output_format", "table"), - ) - - -@wallet.command() -@click.argument("amount", type=float) -@click.argument("job_id") -@click.option("--desc", help="Description of the work") -@click.pass_context -def earn(ctx, amount: float, job_id: str, desc: Optional[str]): - """Add earnings from completed job""" - wallet_name = ctx.obj["wallet_name"] - wallet_path = ctx.obj["wallet_path"] - - if not wallet_path.exists(): - error(f"Wallet '{wallet_name}' not found") - return - - wallet_data = _load_wallet(wallet_path, wallet_name) - - # Add transaction - transaction = { - "type": "earn", - "amount": amount, - "job_id": job_id, - "description": desc or f"Job {job_id}", - "timestamp": datetime.now().isoformat(), - } - - wallet_data["transactions"].append(transaction) - wallet_data["balance"] = wallet_data.get("balance", 0) + amount - - # Save wallet with encryption - password = None - if wallet_data.get("encrypted"): - password = _get_wallet_password(wallet_name) - _save_wallet(wallet_path, wallet_data, password) - - success(f"Earnings added: {amount} AITBC") - output( - { - "wallet": wallet_name, - "amount": amount, - "job_id": job_id, - "new_balance": wallet_data["balance"], - }, - ctx.obj.get("output_format", "table"), - ) - - -@wallet.command() -@click.argument("amount", type=float) -@click.argument("description") -@click.pass_context -def spend(ctx, amount: float, description: str): - """Spend AITBC""" - wallet_name = ctx.obj["wallet_name"] - wallet_path = ctx.obj["wallet_path"] - - if not wallet_path.exists(): - error(f"Wallet '{wallet_name}' not found") - return - - wallet_data = _load_wallet(wallet_path, wallet_name) - - balance = wallet_data.get("balance", 0) - if balance < amount: - error(f"Insufficient balance. Available: {balance}, Required: {amount}") - ctx.exit(1) - return - - # Add transaction - transaction = { - "type": "spend", - "amount": -amount, - "description": description, - "timestamp": datetime.now().isoformat(), - } - - wallet_data["transactions"].append(transaction) - wallet_data["balance"] = balance - amount - - # Save wallet with encryption - password = None - if wallet_data.get("encrypted"): - password = _get_wallet_password(wallet_name) - _save_wallet(wallet_path, wallet_data, password) - - success(f"Spent: {amount} AITBC") - output( - { - "wallet": wallet_name, - "amount": amount, - "description": description, - "new_balance": wallet_data["balance"], - }, - ctx.obj.get("output_format", "table"), - ) - - -@wallet.command() -@click.pass_context -def address(ctx): - """Show wallet address""" - wallet_name = ctx.obj["wallet_name"] - wallet_path = ctx.obj["wallet_path"] - - if not wallet_path.exists(): - error(f"Wallet '{wallet_name}' not found") - return - - wallet_data = _load_wallet(wallet_path, wallet_name) - - output( - {"wallet": wallet_name, "address": wallet_data["address"]}, - ctx.obj.get("output_format", "table"), - ) - - -@wallet.command() -@click.argument("to_address") -@click.argument("amount", type=float) -@click.option("--fee", type=float, default=10, help="Transaction fee") -@click.option("--password", help="Wallet password for signing") -@click.option("--rpc-url", help="Blockchain RPC URL") -@click.pass_context -def send(ctx, to_address: str, amount: float, fee: float, password: Optional[str], rpc_url: Optional[str]): - """Send AITBC to another address""" - wallet_name = ctx.obj["wallet_name"] - wallet_path = ctx.obj["wallet_path"] - - if not wallet_path.exists(): - error(f"Wallet '{wallet_name}' not found") - return - - wallet_data = _load_wallet(wallet_path, wallet_name) - sender_address = wallet_data["address"] - - # Get RPC URL from context or parameter - if not rpc_url: - from ..config import get_config - config = get_config() - rpc_url = getattr(config, 'blockchain_rpc_url', 'http://localhost:8006') - - # Get chain_id from RPC - try: - from ..utils.chain_id import get_chain_id - chain_id = get_chain_id(rpc_url, override=None, timeout=5) - except Exception: - chain_id = "ait-testnet" - - # Get actual nonce from blockchain - actual_nonce = 0 - try: - http_client = AITBCHTTPClient(base_url=rpc_url, timeout=5) - account_data = http_client.get(f"/rpc/account/{sender_address}") - actual_nonce = account_data.get("nonce", 0) - except Exception: - actual_nonce = 0 - - # Get private key for signing - try: - from cryptography.hazmat.primitives.asymmetric import ed25519 - private_key_hex = wallet_data.get("private_key") - if not private_key_hex: - error("Wallet does not contain private key") - return - - private_key = ed25519.Ed25519PrivateKey.from_private_bytes(bytes.fromhex(private_key_hex)) - except Exception as e: - error(f"Error loading private key: {e}") - return - - # Create transaction with modern payload format - transaction = { - "type": "TRANSFER", - "chain_id": chain_id, - "from": sender_address, - "nonce": actual_nonce, - "fee": int(fee), - "payload": { - "recipient": to_address, - "amount": int(amount) - } - } - - # Sign transaction - import json - message = json.dumps(transaction, sort_keys=True).encode() - signature = private_key.sign(message) - transaction["signature"] = signature.hex() - - # Submit to blockchain - try: - http_client = AITBCHTTPClient(base_url=rpc_url, timeout=30) - result = http_client.post("/rpc/transaction", json=transaction) - tx_hash = result.get("transaction_hash") - success(f"Transaction submitted: {tx_hash}") - output({ - "transaction_hash": tx_hash, - "from": sender_address, - "to": to_address, - "amount": amount, - "fee": fee, - "chain_id": chain_id - }, ctx.obj.get("output_format", "table")) - return tx_hash - except Exception as e: - error(f"Error submitting transaction: {e}") - return None - - -@wallet.command() -@click.argument("to_address") -@click.argument("amount", type=float) -@click.option("--description", help="Transaction description") -@click.pass_context -def request_payment(ctx, to_address: str, amount: float, description: Optional[str]): - """Request payment from another address""" - wallet_name = ctx.obj["wallet_name"] - wallet_path = ctx.obj["wallet_path"] - - if not wallet_path.exists(): - error(f"Wallet '{wallet_name}' not found") - return - - wallet_data = _load_wallet(wallet_path, wallet_name) - - # Create payment request - request = { - "from_address": to_address, - "to_address": wallet_data["address"], - "amount": amount, - "description": description or "", - "timestamp": datetime.now().isoformat(), - } - - output( - { - "wallet": wallet_name, - "payment_request": request, - "note": "Share this with the payer to request payment", - }, - ctx.obj.get("output_format", "table"), - ) - - -@wallet.command() -@click.pass_context -def stats(ctx): - """Show wallet statistics""" - wallet_name = ctx.obj["wallet_name"] - wallet_path = ctx.obj["wallet_path"] - - if not wallet_path.exists(): - error(f"Wallet '{wallet_name}' not found") - return - - wallet_data = _load_wallet(wallet_path, wallet_name) - - transactions = wallet_data.get("transactions", []) - - # Calculate stats - total_earned = sum( - tx["amount"] for tx in transactions if tx["type"] == "earn" and tx["amount"] > 0 - ) - total_spent = sum( - abs(tx["amount"]) - for tx in transactions - if tx["type"] in ["spend", "send"] and tx["amount"] < 0 - ) - jobs_completed = len([tx for tx in transactions if tx["type"] == "earn"]) - - output( - { - "wallet": wallet_name, - "address": wallet_data["address"], - "current_balance": wallet_data.get("balance", 0), - "total_earned": total_earned, - "total_spent": total_spent, - "jobs_completed": jobs_completed, - "transaction_count": len(transactions), - "wallet_created": wallet_data.get("created_at"), - }, - ctx.obj.get("output_format", "table"), - ) - - -@wallet.command() -@click.argument("amount", type=float) -@click.option("--duration", type=int, default=30, help="Staking duration in days") -@click.pass_context -def stake(ctx, amount: float, duration: int): - """Stake AITBC tokens""" - wallet_name = ctx.obj["wallet_name"] - wallet_path = ctx.obj["wallet_path"] - - if not wallet_path.exists(): - error(f"Wallet '{wallet_name}' not found") - return - - wallet_data = _load_wallet(wallet_path, wallet_name) - - balance = wallet_data.get("balance", 0) - if balance < amount: - error(f"Insufficient balance. Available: {balance}, Required: {amount}") - ctx.exit(1) - return - - # Record stake - stake_id = f"stake_{int(datetime.now().timestamp())}" - stake_record = { - "stake_id": stake_id, - "amount": amount, - "duration_days": duration, - "start_date": datetime.now().isoformat(), - "end_date": (datetime.now() + timedelta(days=duration)).isoformat(), - "status": "active", - "apy": 5.0 + (duration / 30) * 1.5, # Higher APY for longer stakes - } - - staking = wallet_data.setdefault("staking", []) - staking.append(stake_record) - wallet_data["balance"] = balance - amount - - # Add transaction - wallet_data["transactions"].append( - { - "type": "stake", - "amount": -amount, - "stake_id": stake_id, - "description": f"Staked {amount} AITBC for {duration} days", - "timestamp": datetime.now().isoformat(), - } - ) - - # Save wallet with encryption - password = None - if wallet_data.get("encrypted"): - password = _get_wallet_password(wallet_name) - _save_wallet(wallet_path, wallet_data, password) - - success(f"Staked {amount} AITBC for {duration} days") - output( - { - "wallet": wallet_name, - "stake_id": stake_id, - "amount": amount, - "duration_days": duration, - "apy": stake_record["apy"], - "new_balance": wallet_data["balance"], - }, - ctx.obj.get("output_format", "table"), - ) - - -@wallet.command() -@click.argument("stake_id") -@click.pass_context -def unstake(ctx, stake_id: str): - """Unstake AITBC tokens""" - wallet_name = ctx.obj["wallet_name"] - wallet_path = ctx.obj["wallet_path"] - - if not wallet_path.exists(): - error(f"Wallet '{wallet_name}' not found") - return - - with open(wallet_path, "r") as f: - wallet_data = json.load(f) - - staking = wallet_data.get("staking", []) - stake_record = next( - (s for s in staking if s["stake_id"] == stake_id and s["status"] == "active"), - None, - ) - - if not stake_record: - error(f"Active stake '{stake_id}' not found") - ctx.exit(1) - return - - # Calculate rewards - start = datetime.fromisoformat(stake_record["start_date"]) - days_staked = max(1, (datetime.now() - start).days) - daily_rate = stake_record["apy"] / 100 / 365 - rewards = stake_record["amount"] * daily_rate * days_staked - - # Return principal + rewards - returned = stake_record["amount"] + rewards - wallet_data["balance"] = wallet_data.get("balance", 0) + returned - stake_record["status"] = "completed" - stake_record["rewards"] = rewards - stake_record["completed_date"] = datetime.now().isoformat() - - # Add transaction - wallet_data["transactions"].append( - { - "type": "unstake", - "amount": returned, - "stake_id": stake_id, - "rewards": rewards, - "description": f"Unstaked {stake_record['amount']} AITBC + {rewards:.4f} rewards", - "timestamp": datetime.now().isoformat(), - } - ) - - # Save wallet with encryption - password = None - if wallet_data.get("encrypted"): - password = _get_wallet_password(wallet_name) - _save_wallet(wallet_path, wallet_data, password) - - success(f"Unstaked {stake_record['amount']} AITBC + {rewards:.4f} rewards") - output( - { - "wallet": wallet_name, - "stake_id": stake_id, - "principal": stake_record["amount"], - "rewards": rewards, - "total_returned": returned, - "days_staked": days_staked, - "new_balance": wallet_data["balance"], - }, - ctx.obj.get("output_format", "table"), - ) - - -@wallet.command(name="staking-info") -@click.pass_context -def staking_info(ctx): - """Show staking information""" - wallet_name = ctx.obj["wallet_name"] - wallet_path = ctx.obj["wallet_path"] - - if not wallet_path.exists(): - error(f"Wallet '{wallet_name}' not found") - return - - wallet_data = _load_wallet(wallet_path, wallet_name) - - staking = wallet_data.get("staking", []) - active_stakes = [s for s in staking if s["status"] == "active"] - completed_stakes = [s for s in staking if s["status"] == "completed"] - - total_staked = sum(s["amount"] for s in active_stakes) - total_rewards = sum(s.get("rewards", 0) for s in completed_stakes) - - output( - { - "wallet": wallet_name, - "total_staked": total_staked, - "total_rewards_earned": total_rewards, - "active_stakes": len(active_stakes), - "completed_stakes": len(completed_stakes), - "stakes": [ - { - "stake_id": s["stake_id"], - "amount": s["amount"], - "apy": s["apy"], - "duration_days": s["duration_days"], - "status": s["status"], - "start_date": s["start_date"], - } - for s in staking - ], - }, - ctx.obj.get("output_format", "table"), - ) - - -@wallet.command(name="multisig-create") -@click.argument("signers", nargs=-1, required=True) -@click.option( - "--threshold", type=int, required=True, help="Required signatures to approve" -) -@click.option("--name", required=True, help="Multisig wallet name") -@click.pass_context -def multisig_create(ctx, signers: tuple, threshold: int, name: str): - """Create a multi-signature wallet""" - wallet_dir = ctx.obj.get("wallet_dir", Path.home() / ".aitbc" / "wallets") - wallet_dir.mkdir(parents=True, exist_ok=True) - multisig_path = wallet_dir / f"{name}_multisig.json" - - if multisig_path.exists(): - error(f"Multisig wallet '{name}' already exists") - return - - if threshold > len(signers): - error( - f"Threshold ({threshold}) cannot exceed number of signers ({len(signers)})" - ) - return - - import secrets - - multisig_data = { - "wallet_id": name, - "type": "multisig", - "address": f"aitbc1ms{secrets.token_hex(18)}", - "signers": list(signers), - "threshold": threshold, - "created_at": datetime.now().isoformat(), - "balance": 0.0, - "transactions": [], - "pending_transactions": [], - } - - with open(multisig_path, "w") as f: - json.dump(multisig_data, f, indent=2) - - success(f"Multisig wallet '{name}' created ({threshold}-of-{len(signers)})") - output( - { - "name": name, - "address": multisig_data["address"], - "signers": list(signers), - "threshold": threshold, - }, - ctx.obj.get("output_format", "table"), - ) - - -@wallet.command(name="multisig-propose") -@click.option("--wallet", "wallet_name", required=True, help="Multisig wallet name") -@click.argument("to_address") -@click.argument("amount", type=float) -@click.option("--description", help="Transaction description") -@click.pass_context -def multisig_propose( - ctx, wallet_name: str, to_address: str, amount: float, description: Optional[str] -): - """Propose a multisig transaction""" - wallet_dir = ctx.obj.get("wallet_dir", Path.home() / ".aitbc" / "wallets") - multisig_path = wallet_dir / f"{wallet_name}_multisig.json" - - if not multisig_path.exists(): - error(f"Multisig wallet '{wallet_name}' not found") - return - - with open(multisig_path) as f: - ms_data = json.load(f) - - if ms_data.get("balance", 0) < amount: - error( - f"Insufficient balance. Available: {ms_data['balance']}, Required: {amount}" - ) - ctx.exit(1) - return - - import secrets - - tx_id = f"mstx_{secrets.token_hex(8)}" - pending_tx = { - "tx_id": tx_id, - "to": to_address, - "amount": amount, - "description": description or "", - "proposed_at": datetime.now().isoformat(), - "proposed_by": os.environ.get("USER", "unknown"), - "signatures": [], - "status": "pending", - } - - ms_data.setdefault("pending_transactions", []).append(pending_tx) - with open(multisig_path, "w") as f: - json.dump(ms_data, f, indent=2) - - success(f"Transaction proposed: {tx_id}") - output( - { - "tx_id": tx_id, - "to": to_address, - "amount": amount, - "signatures_needed": ms_data["threshold"], - "status": "pending", - }, - ctx.obj.get("output_format", "table"), - ) - - -@wallet.command(name="multisig-sign") -@click.option("--wallet", "wallet_name", required=True, help="Multisig wallet name") -@click.argument("tx_id") -@click.option("--signer", required=True, help="Signer address") -@click.pass_context -def multisig_sign(ctx, wallet_name: str, tx_id: str, signer: str): - """Sign a pending multisig transaction""" - wallet_dir = ctx.obj.get("wallet_dir", Path.home() / ".aitbc" / "wallets") - multisig_path = wallet_dir / f"{wallet_name}_multisig.json" - - if not multisig_path.exists(): - error(f"Multisig wallet '{wallet_name}' not found") - return - - with open(multisig_path) as f: - ms_data = json.load(f) - - if signer not in ms_data.get("signers", []): - error(f"'{signer}' is not an authorized signer") - ctx.exit(1) - return - - pending = ms_data.get("pending_transactions", []) - tx = next( - (t for t in pending if t["tx_id"] == tx_id and t["status"] == "pending"), None - ) - - if not tx: - error(f"Pending transaction '{tx_id}' not found") - ctx.exit(1) - return - - if signer in tx["signatures"]: - error(f"'{signer}' has already signed this transaction") - return - - tx["signatures"].append(signer) - - # Check if threshold met - if len(tx["signatures"]) >= ms_data["threshold"]: - tx["status"] = "approved" - # Execute the transaction - ms_data["balance"] = ms_data.get("balance", 0) - tx["amount"] - ms_data["transactions"].append( - { - "type": "multisig_send", - "amount": -tx["amount"], - "to": tx["to"], - "tx_id": tx["tx_id"], - "signatures": tx["signatures"], - "timestamp": datetime.now().isoformat(), - } - ) - success(f"Transaction {tx_id} approved and executed!") - else: - success( - f"Signed. {len(tx['signatures'])}/{ms_data['threshold']} signatures collected" - ) - - with open(multisig_path, "w") as f: - json.dump(ms_data, f, indent=2) - - output( - { - "tx_id": tx_id, - "signatures": tx["signatures"], - "threshold": ms_data["threshold"], - "status": tx["status"], - }, - ctx.obj.get("output_format", "table"), - ) - - -@wallet.command(name="liquidity-stake") -@click.argument("amount", type=float) -@click.option("--pool", default="main", help="Liquidity pool name") -@click.option( - "--lock-days", type=int, default=0, help="Lock period in days (higher APY)" -) -@click.pass_context -def liquidity_stake(ctx, amount: float, pool: str, lock_days: int): - """Stake tokens into a liquidity pool""" - wallet_name = ctx.obj["wallet_name"] - wallet_path = ctx.obj.get("wallet_path") - if not wallet_path or not Path(wallet_path).exists(): - error("Wallet not found") - ctx.exit(1) - return - - wallet_data = _load_wallet(Path(wallet_path), wallet_name) - - balance = wallet_data.get("balance", 0) - if balance < amount: - error(f"Insufficient balance. Available: {balance}, Required: {amount}") - ctx.exit(1) - return - - # APY tiers based on lock period - if lock_days >= 90: - apy = 12.0 - tier = "platinum" - elif lock_days >= 30: - apy = 8.0 - tier = "gold" - elif lock_days >= 7: - apy = 5.0 - tier = "silver" - else: - apy = 3.0 - tier = "bronze" - - import secrets - - stake_id = f"liq_{secrets.token_hex(6)}" - now = datetime.now() - - liq_record = { - "stake_id": stake_id, - "pool": pool, - "amount": amount, - "apy": apy, - "tier": tier, - "lock_days": lock_days, - "start_date": now.isoformat(), - "unlock_date": (now + timedelta(days=lock_days)).isoformat() - if lock_days > 0 - else None, - "status": "active", - } - - wallet_data.setdefault("liquidity", []).append(liq_record) - wallet_data["balance"] = balance - amount - - wallet_data["transactions"].append( - { - "type": "liquidity_stake", - "amount": -amount, - "pool": pool, - "stake_id": stake_id, - "timestamp": now.isoformat(), - } - ) - - # Save wallet with encryption - password = None - if wallet_data.get("encrypted"): - password = _get_wallet_password(wallet_name) - _save_wallet(Path(wallet_path), wallet_data, password) - - success(f"Staked {amount} AITBC into '{pool}' pool ({tier} tier, {apy}% APY)") - output( - { - "stake_id": stake_id, - "pool": pool, - "amount": amount, - "apy": apy, - "tier": tier, - "lock_days": lock_days, - "new_balance": wallet_data["balance"], - }, - ctx.obj.get("output_format", "table"), - ) - - -@wallet.command(name="liquidity-unstake") -@click.argument("stake_id") -@click.pass_context -def liquidity_unstake(ctx, stake_id: str): - """Withdraw from a liquidity pool with rewards""" - wallet_name = ctx.obj["wallet_name"] - wallet_path = ctx.obj.get("wallet_path") - if not wallet_path or not Path(wallet_path).exists(): - error("Wallet not found") - ctx.exit(1) - return - - wallet_data = _load_wallet(Path(wallet_path), wallet_name) - - liquidity = wallet_data.get("liquidity", []) - record = next( - (r for r in liquidity if r["stake_id"] == stake_id and r["status"] == "active"), - None, - ) - - if not record: - error(f"Active liquidity stake '{stake_id}' not found") - ctx.exit(1) - return - - # Check lock period - if record.get("unlock_date"): - unlock = datetime.fromisoformat(record["unlock_date"]) - if datetime.now() < unlock: - error(f"Stake is locked until {record['unlock_date']}") - ctx.exit(1) - return - - # Calculate rewards - start = datetime.fromisoformat(record["start_date"]) - days_staked = max((datetime.now() - start).total_seconds() / 86400, 0.001) - rewards = record["amount"] * (record["apy"] / 100) * (days_staked / 365) - total = record["amount"] + rewards - - record["status"] = "completed" - record["end_date"] = datetime.now().isoformat() - record["rewards"] = round(rewards, 6) - - wallet_data["balance"] = wallet_data.get("balance", 0) + total - - wallet_data["transactions"].append( - { - "type": "liquidity_unstake", - "amount": total, - "principal": record["amount"], - "rewards": round(rewards, 6), - "pool": record["pool"], - "stake_id": stake_id, - "timestamp": datetime.now().isoformat(), - } - ) - - # Save wallet with encryption - password = None - if wallet_data.get("encrypted"): - password = _get_wallet_password(wallet_name) - _save_wallet(Path(wallet_path), wallet_data, password) - - success( - f"Withdrawn {total:.6f} AITBC (principal: {record['amount']}, rewards: {rewards:.6f})" - ) - output( - { - "stake_id": stake_id, - "pool": record["pool"], - "principal": record["amount"], - "rewards": round(rewards, 6), - "total_returned": round(total, 6), - "days_staked": round(days_staked, 2), - "apy": record["apy"], - "new_balance": round(wallet_data["balance"], 6), - }, - ctx.obj.get("output_format", "table"), - ) - - -@wallet.command() -@click.pass_context -def rewards(ctx): - """View all earned rewards (staking + liquidity)""" - wallet_name = ctx.obj["wallet_name"] - wallet_path = ctx.obj.get("wallet_path") - if not wallet_path or not Path(wallet_path).exists(): - error("Wallet not found") - ctx.exit(1) - return - - wallet_data = _load_wallet(Path(wallet_path), wallet_name) - - staking = wallet_data.get("staking", []) - liquidity = wallet_data.get("liquidity", []) - - # Staking rewards - staking_rewards = sum( - s.get("rewards", 0) for s in staking if s.get("status") == "completed" - ) - active_staking = sum(s["amount"] for s in staking if s.get("status") == "active") - - # Liquidity rewards - liq_rewards = sum( - r.get("rewards", 0) for r in liquidity if r.get("status") == "completed" - ) - active_liquidity = sum( - r["amount"] for r in liquidity if r.get("status") == "active" - ) - - # Estimate pending rewards for active positions - pending_staking = 0 - for s in staking: - if s.get("status") == "active": - start = datetime.fromisoformat(s["start_date"]) - days = max((datetime.now() - start).total_seconds() / 86400, 0) - pending_staking += s["amount"] * (s["apy"] / 100) * (days / 365) - - pending_liquidity = 0 - for r in liquidity: - if r.get("status") == "active": - start = datetime.fromisoformat(r["start_date"]) - days = max((datetime.now() - start).total_seconds() / 86400, 0) - pending_liquidity += r["amount"] * (r["apy"] / 100) * (days / 365) - - output( - { - "staking_rewards_earned": round(staking_rewards, 6), - "staking_rewards_pending": round(pending_staking, 6), - "staking_active_amount": active_staking, - "liquidity_rewards_earned": round(liq_rewards, 6), - "liquidity_rewards_pending": round(pending_liquidity, 6), - "liquidity_active_amount": active_liquidity, - "total_earned": round(staking_rewards + liq_rewards, 6), - "total_pending": round(pending_staking + pending_liquidity, 6), - "total_staked": active_staking + active_liquidity, - }, - ctx.obj.get("output_format", "table"), - ) - - -@wallet.command() -@click.argument("address") -@click.option("--amount", default=1000000, help="Amount to request from faucet (default: 1000000)") -@click.option("--chain-id", help="Chain ID (defaults to node's chain)") -@click.pass_context -def fund(ctx, address: str, amount: int, chain_id: str): - """Fund wallet using blockchain faucet""" - import httpx - from ..utils.chain_id import get_chain_id - from ..config import get_config - - config = get_config() - rpc_url = config.blockchain_rpc_url if hasattr(config, 'blockchain_rpc_url') else 'http://localhost:8006' - - # Get chain_id - if not chain_id: - chain_id = get_chain_id(rpc_url) - - # Normalize address - address = address.lower().strip() - if not address.startswith("0x"): - address = "0x" + address - - # Call faucet endpoint - faucet_url = f"{rpc_url}/faucet" - faucet_data = { - "address": address, - "amount": amount, - "chain_id": chain_id - } - - try: - response = httpx.post(faucet_url, json=faucet_data, timeout=10) - response.raise_for_status() - result = response.json() - - if result.get("success"): - success(f"Successfully funded wallet {address} with {amount} units") - output(result, ctx.obj.get("output_format", "table")) - else: - error(f"Failed to fund wallet: {result.get('message', 'Unknown error')}") - except httpx.HTTPError as e: - error(f"HTTP error calling faucet: {e}") - except Exception as e: - error(f"Error funding wallet: {e}") diff --git a/cli/src/aitbc_cli/commands/workflow.py b/cli/src/aitbc_cli/commands/workflow.py deleted file mode 100644 index 6b508612..00000000 --- a/cli/src/aitbc_cli/commands/workflow.py +++ /dev/null @@ -1,73 +0,0 @@ -""" -Workflow commands for AITBC CLI -""" - -import json -import time -from typing import Optional - -import click - -from ..utils import error, success - - -@click.group() -def workflow(): - """Workflow management commands""" - pass - - -@workflow.command() -@click.argument('workflow_name') -@click.option('--config', help='Workflow configuration file') -@click.option('--dry-run', is_flag=True, help='Dry run without executing') -def run(workflow_name: str, config: Optional[str], dry_run: bool): - """Run a workflow""" - if dry_run: - success(f"Dry run for workflow {workflow_name}") - click.echo("Would execute workflow without making changes") - return - - success(f"Run workflow {workflow_name}") - if config: - click.echo(f"Using config: {config}") - - # TODO: Implement actual workflow execution logic - click.echo(f"Execution ID: wf_exec_{int(time.time())}") - click.echo("Status: Running") - - -@workflow.command() -@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format') -def list(format: str): - """List available workflows""" - success("Available workflows:") - workflows = [ - {"name": "gpu-marketplace", "status": "active", "steps": 5}, - {"name": "ai-job-processing", "status": "active", "steps": 3}, - {"name": "mining-optimization", "status": "inactive", "steps": 4} - ] - - if format == 'json': - click.echo(json.dumps(workflows, indent=2)) - else: - for wf in workflows: - click.echo(f" - {wf['name']}: {wf['status']} ({wf['steps']} steps)") - - -@workflow.command() -@click.argument('workflow_name') -def status(workflow_name: str): - """Get workflow status""" - success(f"Get status for workflow {workflow_name}") - # TODO: Implement actual status check from workflow engine - click.echo("Status: Not running") - click.echo("Last execution: Never") - - -@workflow.command() -@click.argument('workflow_name') -def stop(workflow_name: str): - """Stop a running workflow""" - success(f"Stop workflow {workflow_name}") - # TODO: Implement actual stop command via workflow engine diff --git a/cli/src/aitbc_cli/config.py b/cli/src/aitbc_cli/config.py deleted file mode 100644 index 4889abc8..00000000 --- a/cli/src/aitbc_cli/config.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Configuration module for AITBC CLI""" - -import os -from pathlib import Path -from typing import Optional -from pydantic import Field -from pydantic_settings import BaseSettings, SettingsConfigDict - -from aitbc.config import BaseAITBCConfig -from aitbc.constants import BLOCKCHAIN_RPC_PORT, BLOCKCHAIN_P2P_PORT - - -class CLIConfig(BaseAITBCConfig): - """CLI-specific configuration inheriting from shared BaseAITBCConfig""" - - model_config = SettingsConfigDict( - env_file=str(Path("/etc/aitbc/.env")), - env_file_encoding="utf-8", - case_sensitive=False, - extra="ignore" - ) - - # CLI-specific settings - app_name: str = Field(default="AITBC CLI", description="CLI application name") - app_version: str = Field(default="2.1.0", description="CLI version") - - # Service URLs - exchange_service_url: str = Field(default="http://localhost:8001/api/v1", description="Exchange Service URL") - gpu_service_url: str = Field(default="http://localhost:8101", description="GPU Service URL") - marketplace_service_url: str = Field(default="http://localhost:8102", description="Marketplace Service URL") - trading_service_url: str = Field(default="http://localhost:8104", description="Trading Service URL") - governance_service_url: str = Field(default="http://localhost:8105", description="Governance Service URL") - ai_service_url: str = Field(default="http://localhost:8106", description="AI Service URL") - monitoring_service_url: str = Field(default="http://localhost:8107", description="Monitoring Service URL") - hermes_service_url: str = Field(default="http://localhost:8108", description="hermes Service URL") - plugin_service_url: str = Field(default="http://localhost:8109", description="Plugin Service URL") - edge_api_host: str = Field(default="localhost", description="Edge API host") - edge_api_port: int = Field(default=8103, description="Edge API port") - wallet_daemon_url: str = Field(default="http://localhost:8003", description="Wallet daemon URL") - wallet_url: str = Field(default="http://localhost:8003", description="Wallet daemon URL (alias for compatibility)") - blockchain_rpc_url: str = Field(default=f"http://localhost:{BLOCKCHAIN_RPC_PORT}", description="Blockchain RPC URL") - - # Legacy coordinator URL (deprecated, kept for backward compatibility during migration) - coordinator_url: str = Field(default="http://localhost:8011", description="Coordinator API URL (deprecated)") - - # Chain configuration - chain_id: str = Field(default="ait-mainnet", description="Default chain ID for multichain operations") - - # Authentication - api_key: Optional[str] = Field(default=None, description="API key for authentication") - - # Request settings - timeout: int = Field(default=30, description="Request timeout in seconds") - - # Config file path (for backward compatibility) - config_file: Optional[str] = Field(default=None, description="Path to config file") - - -def get_config(config_file: Optional[str] = None) -> CLIConfig: - """Load CLI configuration from shared config system""" - # For backward compatibility, allow config_file override - if config_file: - config_path = Path(config_file) - if config_path.exists(): - import yaml - with open(config_path) as f: - config_data = yaml.safe_load(f) or {} - - # Override with config file values - return CLIConfig( - coordinator_url=config_data.get("coordinator_url", "http://localhost:8011"), - wallet_daemon_url=config_data.get("wallet_url", "http://localhost:8003"), - api_key=config_data.get("api_key"), - timeout=config_data.get("timeout", 30) - ) - - # Use shared config system with environment variables - return CLIConfig() - diff --git a/cli/src/aitbc_cli/core/__init__.py b/cli/src/aitbc_cli/core/__init__.py deleted file mode 100755 index b158b352..00000000 --- a/cli/src/aitbc_cli/core/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""AITBC CLI - Command Line Interface for AITBC Network""" - -__version__ = "0.1.0" -__author__ = "AITBC Team" -__email__ = "team@aitbc.net" diff --git a/cli/src/aitbc_cli/core/__version__.py b/cli/src/aitbc_cli/core/__version__.py deleted file mode 100644 index c0262ddc..00000000 --- a/cli/src/aitbc_cli/core/__version__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""AITBC CLI Version Information""" - -__version__ = "0.2.2" diff --git a/cli/src/aitbc_cli/core/agent_communication.py b/cli/src/aitbc_cli/core/agent_communication.py deleted file mode 100755 index a02908ed..00000000 --- a/cli/src/aitbc_cli/core/agent_communication.py +++ /dev/null @@ -1,525 +0,0 @@ -""" -Cross-chain agent communication system -""" - -import asyncio -import json -import hashlib -import time -from datetime import datetime, timedelta -from typing import Dict, List, Optional, Any, Set -from dataclasses import dataclass, asdict -from enum import Enum -import uuid -from collections import defaultdict - -from .config import MultiChainConfig -from .node_client import NodeClient -import logging -logger = logging.getLogger(__name__) - - -class MessageType(Enum): - """Agent message types""" - DISCOVERY = "discovery" - ROUTING = "routing" - COMMUNICATION = "communication" - COLLABORATION = "collaboration" - PAYMENT = "payment" - REPUTATION = "reputation" - GOVERNANCE = "governance" - -class AgentStatus(Enum): - """Agent status""" - ACTIVE = "active" - INACTIVE = "inactive" - BUSY = "busy" - OFFLINE = "offline" - -@dataclass -class AgentInfo: - """Agent information""" - agent_id: str - name: str - chain_id: str - node_id: str - status: AgentStatus - capabilities: List[str] - reputation_score: float - last_seen: datetime - endpoint: str - version: str - -@dataclass -class AgentMessage: - """Agent communication message""" - message_id: str - sender_id: str - receiver_id: str - message_type: MessageType - chain_id: str - target_chain_id: Optional[str] - payload: Dict[str, Any] - timestamp: datetime - signature: str - priority: int - ttl_seconds: int - -@dataclass -class AgentCollaboration: - """Agent collaboration record""" - collaboration_id: str - agent_ids: List[str] - chain_ids: List[str] - collaboration_type: str - status: str - created_at: datetime - updated_at: datetime - shared_resources: Dict[str, Any] - governance_rules: Dict[str, Any] - -@dataclass -class AgentReputation: - """Agent reputation record""" - agent_id: str - chain_id: str - reputation_score: float - successful_interactions: int - failed_interactions: int - total_interactions: int - last_updated: datetime - feedback_scores: List[float] - -class CrossChainAgentCommunication: - """Cross-chain agent communication system""" - - def __init__(self, config: MultiChainConfig): - self.config = config - self.agents: Dict[str, AgentInfo] = {} - self.messages: Dict[str, AgentMessage] = {} - self.collaborations: Dict[str, AgentCollaboration] = {} - self.reputations: Dict[str, AgentReputation] = {} - self.routing_table: Dict[str, List[str]] = {} - self.discovery_cache: Dict[str, List[AgentInfo]] = {} - self.message_queue: Dict[str, List[AgentMessage]] = defaultdict(list) - - # Communication thresholds - self.thresholds = { - 'max_message_size': 1048576, # 1MB - 'max_ttl_seconds': 3600, # 1 hour - 'max_queue_size': 1000, - 'min_reputation_score': 0.5, - 'max_collaboration_size': 10 - } - - async def register_agent(self, agent_info: AgentInfo) -> bool: - """Register an agent in the cross-chain network""" - try: - # Validate agent info - if not self._validate_agent_info(agent_info): - return False - - # Check if agent already exists - if agent_info.agent_id in self.agents: - # Update existing agent - self.agents[agent_info.agent_id] = agent_info - else: - # Register new agent - self.agents[agent_info.agent_id] = agent_info - - # Initialize reputation - if agent_info.agent_id not in self.reputations: - self.reputations[agent_info.agent_id] = AgentReputation( - agent_id=agent_info.agent_id, - chain_id=agent_info.chain_id, - reputation_score=agent_info.reputation_score, - successful_interactions=0, - failed_interactions=0, - total_interactions=0, - last_updated=datetime.now(), - feedback_scores=[] - ) - - # Update routing table - self._update_routing_table(agent_info) - - # Clear discovery cache - self.discovery_cache.clear() - - return True - - except Exception as e: - logger.error(f"Error registering agent {agent_info.agent_id}: {e}") - return False - - async def discover_agents(self, chain_id: str, capabilities: Optional[List[str]] = None) -> List[AgentInfo]: - """Discover agents on a specific chain""" - cache_key = f"{chain_id}:{'_'.join(capabilities or [])}" - - # Check cache first - if cache_key in self.discovery_cache: - cached_time = self.discovery_cache[cache_key][0].last_seen if self.discovery_cache[cache_key] else None - if cached_time and (datetime.now() - cached_time).seconds < 300: # 5 minute cache - return self.discovery_cache[cache_key] - - # Discover agents from chain - agents = [] - - for agent_id, agent_info in self.agents.items(): - if agent_info.chain_id == chain_id and agent_info.status == AgentStatus.ACTIVE: - if capabilities: - # Check if agent has required capabilities - if any(cap in agent_info.capabilities for cap in capabilities): - agents.append(agent_info) - else: - agents.append(agent_info) - - # Cache results - self.discovery_cache[cache_key] = agents - - return agents - - async def send_message(self, message: AgentMessage) -> bool: - """Send a message to an agent""" - try: - # Validate message - if not self._validate_message(message): - return False - - # Check if receiver exists - if message.receiver_id not in self.agents: - return False - - # Check receiver reputation - receiver_reputation = self.reputations.get(message.receiver_id) - if receiver_reputation and receiver_reputation.reputation_score < self.thresholds['min_reputation_score']: - return False - - # Add message to queue - self.message_queue[message.receiver_id].append(message) - self.messages[message.message_id] = message - - # Attempt immediate delivery - await self._deliver_message(message) - - return True - - except Exception as e: - logger.error(f"Error sending message {message.message_id}: {e}") - return False - - async def _deliver_message(self, message: AgentMessage) -> bool: - """Deliver a message to the target agent""" - try: - receiver = self.agents.get(message.receiver_id) - if not receiver: - return False - - # Check if receiver is on same chain - if message.chain_id == receiver.chain_id: - # Same chain delivery - return await self._deliver_same_chain(message, receiver) - else: - # Cross-chain delivery - return await self._deliver_cross_chain(message, receiver) - - except Exception as e: - logger.error(f"Error delivering message {message.message_id}: {e}") - return False - - async def _deliver_same_chain(self, message: AgentMessage, receiver: AgentInfo) -> bool: - """Deliver message on the same chain""" - try: - # Simulate message delivery - logger.info(f"Delivering message {message.message_id} to agent {receiver.agent_id} on chain {message.chain_id}") - # Update agent status - receiver.last_seen = datetime.now() - self.agents[receiver.agent_id] = receiver - - # Remove from queue - if message in self.message_queue[receiver.agent_id]: - self.message_queue[receiver.agent_id].remove(message) - - return True - - except Exception as e: - logger.error(f"Error in same-chain delivery: {e}") - return False - - async def _deliver_cross_chain(self, message: AgentMessage, receiver: AgentInfo) -> bool: - """Deliver message across chains""" - try: - # Find bridge nodes - bridge_nodes = await self._find_bridge_nodes(message.chain_id, receiver.chain_id) - if not bridge_nodes: - return False - - # Route through bridge nodes - for bridge_node in bridge_nodes: - try: - # Simulate cross-chain routing - logger.info(f"Routing message {message.message_id} through bridge node {bridge_node}") - # Update routing table - if message.chain_id not in self.routing_table: - self.routing_table[message.chain_id] = [] - if receiver.chain_id not in self.routing_table[message.chain_id]: - self.routing_table[message.chain_id].append(receiver.chain_id) - - # Update agent status - receiver.last_seen = datetime.now() - self.agents[receiver.agent_id] = receiver - - # Remove from queue - if message in self.message_queue[receiver.agent_id]: - self.message_queue[receiver.agent_id].remove(message) - - return True - - except Exception as e: - logger.error(f"Error routing through bridge node {bridge_node}: {e}") - continue - - return False - - except Exception as e: - logger.error(f"Error in cross-chain delivery: {e}") - return False - - async def create_collaboration(self, agent_ids: List[str], collaboration_type: str, governance_rules: Dict[str, Any]) -> Optional[str]: - """Create a multi-agent collaboration""" - try: - # Validate collaboration - if len(agent_ids) > self.thresholds['max_collaboration_size']: - return None - - # Check if all agents exist and are active - active_agents = [] - for agent_id in agent_ids: - agent = self.agents.get(agent_id) - if agent and agent.status == AgentStatus.ACTIVE: - active_agents.append(agent) - else: - return None - - if len(active_agents) < 2: - return None - - # Create collaboration - collaboration_id = str(uuid.uuid4()) - chain_ids = list(set(agent.chain_id for agent in active_agents)) - - collaboration = AgentCollaboration( - collaboration_id=collaboration_id, - agent_ids=agent_ids, - chain_ids=chain_ids, - collaboration_type=collaboration_type, - status="active", - created_at=datetime.now(), - updated_at=datetime.now(), - shared_resources={}, - governance_rules=governance_rules - ) - - self.collaborations[collaboration_id] = collaboration - - # Notify all agents - for agent_id in agent_ids: - notification = AgentMessage( - message_id=str(uuid.uuid4()), - sender_id="system", - receiver_id=agent_id, - message_type=MessageType.COLLABORATION, - chain_id=active_agents[0].chain_id, - target_chain_id=None, - payload={ - "action": "collaboration_created", - "collaboration_id": collaboration_id, - "collaboration_type": collaboration_type, - "participants": agent_ids - }, - timestamp=datetime.now(), - signature="system_notification", - priority=5, - ttl_seconds=3600 - ) - await self.send_message(notification) - - return collaboration_id - - except Exception as e: - logger.error(f"Error creating collaboration: {e}") - return None - - async def update_reputation(self, agent_id: str, interaction_success: bool, feedback_score: Optional[float] = None) -> bool: - """Update agent reputation""" - try: - reputation = self.reputations.get(agent_id) - if not reputation: - return False - - # Update interaction counts - reputation.total_interactions += 1 - if interaction_success: - reputation.successful_interactions += 1 - else: - reputation.failed_interactions += 1 - - # Add feedback score if provided - if feedback_score is not None: - reputation.feedback_scores.append(feedback_score) - # Keep only last 50 feedback scores - reputation.feedback_scores = reputation.feedback_scores[-50:] - - # Calculate new reputation score - success_rate = reputation.successful_interactions / reputation.total_interactions - feedback_avg = sum(reputation.feedback_scores) / len(reputation.feedback_scores) if reputation.feedback_scores else 0.5 - - # Weighted average: 70% success rate, 30% feedback - reputation.reputation_score = (success_rate * 0.7) + (feedback_avg * 0.3) - reputation.last_updated = datetime.now() - - # Update agent info - if agent_id in self.agents: - self.agents[agent_id].reputation_score = reputation.reputation_score - - return True - - except Exception as e: - logger.error(f"Error updating reputation for agent {agent_id}: {e}") - return False - - async def get_agent_status(self, agent_id: str) -> Optional[Dict[str, Any]]: - """Get comprehensive agent status""" - try: - agent = self.agents.get(agent_id) - if not agent: - return None - - reputation = self.reputations.get(agent_id) - - # Get message queue status - queue_size = len(self.message_queue.get(agent_id, [])) - - # Get active collaborations - active_collaborations = [ - collab for collab in self.collaborations.values() - if agent_id in collab.agent_ids and collab.status == "active" - ] - - status = { - "agent_info": asdict(agent), - "reputation": asdict(reputation) if reputation else None, - "message_queue_size": queue_size, - "active_collaborations": len(active_collaborations), - "last_seen": agent.last_seen.isoformat(), - "status": agent.status.value - } - - return status - - except Exception as e: - logger.error(f"Error getting agent status for {agent_id}: {e}") - return None - - async def get_network_overview(self) -> Dict[str, Any]: - """Get cross-chain network overview""" - try: - # Count agents by chain - agents_by_chain = defaultdict(int) - active_agents_by_chain = defaultdict(int) - - for agent in self.agents.values(): - agents_by_chain[agent.chain_id] += 1 - if agent.status == AgentStatus.ACTIVE: - active_agents_by_chain[agent.chain_id] += 1 - - # Count collaborations by type - collaborations_by_type = defaultdict(int) - active_collaborations = 0 - - for collab in self.collaborations.values(): - collaborations_by_type[collab.collaboration_type] += 1 - if collab.status == "active": - active_collaborations += 1 - - # Message statistics - total_messages = len(self.messages) - queued_messages = sum(len(queue) for queue in self.message_queue.values()) - - # Reputation statistics - reputation_scores = [rep.reputation_score for rep in self.reputations.values()] - avg_reputation = sum(reputation_scores) / len(reputation_scores) if reputation_scores else 0 - - overview = { - "total_agents": len(self.agents), - "active_agents": len([a for a in self.agents.values() if a.status == AgentStatus.ACTIVE]), - "agents_by_chain": dict(agents_by_chain), - "active_agents_by_chain": dict(active_agents_by_chain), - "total_collaborations": len(self.collaborations), - "active_collaborations": active_collaborations, - "collaborations_by_type": dict(collaborations_by_type), - "total_messages": total_messages, - "queued_messages": queued_messages, - "average_reputation": avg_reputation, - "routing_table_size": len(self.routing_table), - "discovery_cache_size": len(self.discovery_cache) - } - - return overview - - except Exception as e: - logger.error(f"Error getting network overview: {e}") - return {} - - def _validate_agent_info(self, agent_info: AgentInfo) -> bool: - """Validate agent information""" - if not agent_info.agent_id or not agent_info.chain_id: - return False - - if agent_info.reputation_score < 0 or agent_info.reputation_score > 1: - return False - - if not agent_info.capabilities: - return False - - return True - - def _validate_message(self, message: AgentMessage) -> bool: - """Validate message""" - if not message.sender_id or not message.receiver_id: - return False - - if message.ttl_seconds > self.thresholds['max_ttl_seconds']: - return False - - if len(json.dumps(message.payload)) > self.thresholds['max_message_size']: - return False - - return True - - def _update_routing_table(self, agent_info: AgentInfo): - """Update routing table with agent information""" - if agent_info.chain_id not in self.routing_table: - self.routing_table[agent_info.chain_id] = [] - - # Add agent to routing table - if agent_info.agent_id not in self.routing_table[agent_info.chain_id]: - self.routing_table[agent_info.chain_id].append(agent_info.agent_id) - - async def _find_bridge_nodes(self, source_chain: str, target_chain: str) -> List[str]: - """Find bridge nodes for cross-chain communication""" - # For now, return any node that has agents on both chains - bridge_nodes = [] - - for node_id, node_config in self.config.nodes.items(): - try: - async with NodeClient(node_config) as client: - chains = await client.get_hosted_chains() - chain_ids = [chain.id for chain in chains] - - if source_chain in chain_ids and target_chain in chain_ids: - bridge_nodes.append(node_id) - except Exception: - continue - - return bridge_nodes diff --git a/cli/src/aitbc_cli/core/analytics.py b/cli/src/aitbc_cli/core/analytics.py old mode 100755 new mode 100644 index 68fd5214..f9a97f58 --- a/cli/src/aitbc_cli/core/analytics.py +++ b/cli/src/aitbc_cli/core/analytics.py @@ -13,7 +13,7 @@ import statistics from .config import MultiChainConfig from .node_client import NodeClient -from models.chain import ChainInfo, ChainType, ChainStatus +from aitbc.models.chain import ChainInfo, ChainType, ChainStatus import logging logger = logging.getLogger(__name__) diff --git a/cli/src/aitbc_cli/core/chain_manager.py b/cli/src/aitbc_cli/core/chain_manager.py deleted file mode 100755 index 434ae2b5..00000000 --- a/cli/src/aitbc_cli/core/chain_manager.py +++ /dev/null @@ -1,497 +0,0 @@ -""" -Chain manager for multi-chain operations -""" - -import asyncio -import hashlib -import json -from datetime import datetime -from pathlib import Path -from typing import Dict, List, Optional, Any -from .config import MultiChainConfig, get_node_config -from .node_client import NodeClient -import logging -from models.chain import ( - ChainConfig, ChainInfo, ChainType, ChainStatus, - GenesisBlock, ChainMigrationPlan, ChainMigrationResult, - ChainBackupResult, ChainRestoreResult -) - -logger = logging.getLogger(__name__) - -class ChainAlreadyExistsError(Exception): - """Chain already exists error""" - pass - -class ChainNotFoundError(Exception): - """Chain not found error""" - pass - -class NodeNotAvailableError(Exception): - """Node not available error""" - pass - -class ChainManager: - """Multi-chain manager""" - - def __init__(self, config: MultiChainConfig): - self.config = config - self._chain_cache: Dict[str, ChainInfo] = {} - self._node_clients: Dict[str, Any] = {} - - async def list_chains( - self, - chain_type: Optional[ChainType] = None, - include_private: bool = False, - sort_by: str = "id" - ) -> List[ChainInfo]: - """List all available chains""" - chains = [] - - # Get chains from all available nodes - for node_id, node_config in self.config.nodes.items(): - try: - node_chains = await self._get_node_chains(node_id) - for chain in node_chains: - # Filter private chains if not requested - if not include_private and chain.privacy.visibility == "private": - continue - - # Filter by chain type if specified - if chain_type and chain.type != chain_type: - continue - - chains.append(chain) - except Exception as e: - # Log error but continue with other nodes - logger.error(f"Error getting chains from node {node_id}: {e}") - # Remove duplicates (same chain on multiple nodes) - unique_chains = {} - for chain in chains: - if chain.id not in unique_chains: - unique_chains[chain.id] = chain - - chains = list(unique_chains.values()) - - # Sort chains - if sort_by == "id": - chains.sort(key=lambda x: x.id) - elif sort_by == "size": - chains.sort(key=lambda x: x.size_mb, reverse=True) - elif sort_by == "nodes": - chains.sort(key=lambda x: x.node_count, reverse=True) - elif sort_by == "created": - chains.sort(key=lambda x: x.created_at, reverse=True) - - return chains - - async def get_chain_info(self, chain_id: str, detailed: bool = False, metrics: bool = False) -> ChainInfo: - """Get detailed information about a chain""" - # Check cache first - if chain_id in self._chain_cache: - chain_info = self._chain_cache[chain_id] - else: - # Get from node - chain_info = await self._find_chain_on_nodes(chain_id) - if not chain_info: - raise ChainNotFoundError(f"Chain {chain_id} not found") - - # Cache the result - self._chain_cache[chain_id] = chain_info - - # Add detailed information if requested - if detailed or metrics: - chain_info = await self._enrich_chain_info(chain_info) - - return chain_info - - async def create_chain(self, chain_config: ChainConfig, node_id: Optional[str] = None) -> str: - """Create a new chain""" - # Generate chain ID - chain_id = self._generate_chain_id(chain_config) - - # Check if chain already exists - try: - await self.get_chain_info(chain_id) - raise ChainAlreadyExistsError(f"Chain {chain_id} already exists") - except ChainNotFoundError: - pass # Chain doesn't exist, which is good - - # Select node if not specified - if not node_id: - node_id = await self._select_best_node(chain_config) - - # Validate node availability - if node_id not in self.config.nodes: - raise NodeNotAvailableError(f"Node {node_id} not configured") - - # Create genesis block - genesis_block = await self._create_genesis_block(chain_config, chain_id) - - # Create chain on node - await self._create_chain_on_node(node_id, genesis_block) - - # Return chain ID - return chain_id - - async def delete_chain(self, chain_id: str, force: bool = False) -> bool: - """Delete a chain""" - chain_info = await self.get_chain_info(chain_id) - - # Get all nodes hosting this chain - hosting_nodes = await self._get_chain_hosting_nodes(chain_id) - - if not force and len(hosting_nodes) > 1: - raise ValueError(f"Chain {chain_id} is hosted on {len(hosting_nodes)} nodes. Use --force to delete.") - - # Delete from all hosting nodes - success = True - for node_id in hosting_nodes: - try: - await self._delete_chain_from_node(node_id, chain_id) - except Exception as e: - logger.error(f"Error deleting chain from node {node_id}: {e}") - success = False - - # Remove from cache - if chain_id in self._chain_cache: - del self._chain_cache[chain_id] - - return success - - async def add_chain_to_node(self, chain_id: str, node_id: str) -> bool: - """Add a chain to a node""" - # Validate node - if node_id not in self.config.nodes: - raise NodeNotAvailableError(f"Node {node_id} not configured") - - # Get chain info - chain_info = await self.get_chain_info(chain_id) - - # Add chain to node - try: - await self._add_chain_to_node(node_id, chain_info) - return True - except Exception as e: - logger.error(f"Error adding chain to node: {e}") - return False - - async def remove_chain_from_node(self, chain_id: str, node_id: str, migrate: bool = False) -> bool: - """Remove a chain from a node""" - # Validate node - if node_id not in self.config.nodes: - raise NodeNotAvailableError(f"Node {node_id} not configured") - - if migrate: - # Find alternative node - target_node = await self._find_alternative_node(chain_id, node_id) - if target_node: - # Migrate chain first - migration_result = await self.migrate_chain(chain_id, node_id, target_node) - if not migration_result.success: - return False - - # Remove chain from node - try: - await self._remove_chain_from_node(node_id, chain_id) - return True - except Exception as e: - logger.error(f"Error removing chain from node: {e}") - return False - - async def migrate_chain(self, chain_id: str, from_node: str, to_node: str, dry_run: bool = False) -> ChainMigrationResult: - """Migrate a chain between nodes""" - # Validate nodes - if from_node not in self.config.nodes: - raise NodeNotAvailableError(f"Source node {from_node} not configured") - if to_node not in self.config.nodes: - raise NodeNotAvailableError(f"Target node {to_node} not configured") - - # Get chain info - chain_info = await self.get_chain_info(chain_id) - - # Create migration plan - migration_plan = await self._create_migration_plan(chain_id, from_node, to_node, chain_info) - - if dry_run: - return ChainMigrationResult( - chain_id=chain_id, - source_node=from_node, - target_node=to_node, - success=migration_plan.feasible, - blocks_transferred=0, - transfer_time_seconds=0, - verification_passed=False, - error=None if migration_plan.feasible else "Migration not feasible" - ) - - if not migration_plan.feasible: - return ChainMigrationResult( - chain_id=chain_id, - source_node=from_node, - target_node=to_node, - success=False, - blocks_transferred=0, - transfer_time_seconds=0, - verification_passed=False, - error="; ".join(migration_plan.issues) - ) - - # Execute migration - return await self._execute_migration(chain_id, from_node, to_node) - - async def backup_chain(self, chain_id: str, backup_path: Optional[str] = None, compress: bool = False, verify: bool = False) -> ChainBackupResult: - """Backup a chain""" - # Get chain info - chain_info = await self.get_chain_info(chain_id) - - # Get hosting node - hosting_nodes = await self._get_chain_hosting_nodes(chain_id) - if not hosting_nodes: - raise ChainNotFoundError(f"Chain {chain_id} not found on any node") - - node_id = hosting_nodes[0] # Use first available node - - # Set backup path - if not backup_path: - backup_path = self.config.chains.backup_path / f"{chain_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.tar.gz" - - # Execute backup - return await self._execute_backup(chain_id, node_id, backup_path, compress, verify) - - async def restore_chain(self, backup_file: str, node_id: Optional[str] = None, verify: bool = False) -> ChainRestoreResult: - """Restore a chain from backup""" - backup_path = Path(backup_file) - if not backup_path.exists(): - raise FileNotFoundError(f"Backup file {backup_file} not found") - - # Select node if not specified - if not node_id: - node_id = await self._select_best_node_for_restore() - - # Execute restore - return await self._execute_restore(backup_path, node_id, verify) - - # Private methods - - def _generate_chain_id(self, chain_config: ChainConfig) -> str: - """Generate a unique chain ID""" - timestamp = datetime.now().strftime("%Y%m%d%H%M%S") - prefix = f"AITBC-{chain_config.type.value.upper()}-{chain_config.purpose.upper()}" - return f"{prefix}-{timestamp}" - - async def _get_node_chains(self, node_id: str) -> List[ChainInfo]: - """Get chains from a specific node""" - if node_id not in self.config.nodes: - return [] - - node_config = self.config.nodes[node_id] - - try: - async with NodeClient(node_config) as client: - return await client.get_hosted_chains() - except Exception as e: - logger.error(f"Error getting chains from node {node_id}: {e}") - return [] - - async def _find_chain_on_nodes(self, chain_id: str) -> Optional[ChainInfo]: - """Find a chain on available nodes""" - for node_id in self.config.nodes: - try: - chains = await self._get_node_chains(node_id) - for chain in chains: - if chain.id == chain_id: - return chain - except Exception: - continue - return None - - async def _enrich_chain_info(self, chain_info: ChainInfo) -> ChainInfo: - """Enrich chain info with detailed data""" - # This would get additional metrics and detailed information - # For now, return the same chain info - return chain_info - - async def _select_best_node(self, chain_config: ChainConfig) -> str: - """Select the best node for creating a chain""" - # Simple selection - in reality, this would consider load, resources, etc. - available_nodes = list(self.config.nodes.keys()) - if not available_nodes: - raise NodeNotAvailableError("No nodes available") - return available_nodes[0] - - async def _create_genesis_block(self, chain_config: ChainConfig, chain_id: str) -> GenesisBlock: - """Create a genesis block for the chain""" - timestamp = datetime.now() - - # Create state root (placeholder) - state_data = { - "chain_id": chain_id, - "config": chain_config.dict(), - "timestamp": timestamp.isoformat() - } - state_root = hashlib.sha256(json.dumps(state_data, sort_keys=True).encode()).hexdigest() - - # Create genesis hash - genesis_data = { - "chain_id": chain_id, - "timestamp": timestamp.isoformat(), - "state_root": state_root - } - genesis_hash = hashlib.sha256(json.dumps(genesis_data, sort_keys=True).encode()).hexdigest() - - return GenesisBlock( - chain_id=chain_id, - chain_type=chain_config.type, - purpose=chain_config.purpose, - name=chain_config.name, - description=chain_config.description, - timestamp=timestamp, - consensus=chain_config.consensus, - privacy=chain_config.privacy, - parameters=chain_config.parameters, - state_root=state_root, - hash=genesis_hash - ) - - async def _create_chain_on_node(self, node_id: str, genesis_block: GenesisBlock) -> None: - """Create a chain on a specific node""" - if node_id not in self.config.nodes: - raise NodeNotAvailableError(f"Node {node_id} not configured") - - node_config = self.config.nodes[node_id] - - try: - async with NodeClient(node_config) as client: - chain_id = await client.create_chain(genesis_block.dict()) - logger.info(f"Successfully created chain {chain_id} on node {node_id}") - except Exception as e: - logger.error(f"Error creating chain on node {node_id}: {e}") - raise - - async def _get_chain_hosting_nodes(self, chain_id: str) -> List[str]: - """Get all nodes hosting a specific chain""" - hosting_nodes = [] - for node_id in self.config.nodes: - try: - chains = await self._get_node_chains(node_id) - if any(chain.id == chain_id for chain in chains): - hosting_nodes.append(node_id) - except Exception: - continue - return hosting_nodes - - async def _delete_chain_from_node(self, node_id: str, chain_id: str) -> None: - """Delete a chain from a specific node""" - if node_id not in self.config.nodes: - raise NodeNotAvailableError(f"Node {node_id} not configured") - - node_config = self.config.nodes[node_id] - - try: - async with NodeClient(node_config) as client: - success = await client.delete_chain(chain_id) - if success: - logger.info(f"Successfully deleted chain {chain_id} from node {node_id}") - else: - raise Exception(f"Failed to delete chain {chain_id}") - except Exception as e: - logger.error(f"Error deleting chain from node {node_id}: {e}") - raise - - async def _add_chain_to_node(self, node_id: str, chain_info: ChainInfo) -> None: - """Add a chain to a specific node""" - # This would actually add the chain to the node - logger.info(f"Adding chain {chain_info.id} to node {node_id}") - async def _remove_chain_from_node(self, node_id: str, chain_id: str) -> None: - """Remove a chain from a specific node""" - # This would actually remove the chain from the node - logger.info(f"Removing chain {chain_id} from node {node_id}") - async def _find_alternative_node(self, chain_id: str, exclude_node: str) -> Optional[str]: - """Find an alternative node for a chain""" - hosting_nodes = await self._get_chain_hosting_nodes(chain_id) - for node_id in hosting_nodes: - if node_id != exclude_node: - return node_id - return None - - async def _create_migration_plan(self, chain_id: str, from_node: str, to_node: str, chain_info: ChainInfo) -> ChainMigrationPlan: - """Create a migration plan""" - # This would analyze the migration and create a detailed plan - return ChainMigrationPlan( - chain_id=chain_id, - source_node=from_node, - target_node=to_node, - size_mb=chain_info.size_mb, - estimated_minutes=int(chain_info.size_mb / 100), # Rough estimate - required_space_mb=chain_info.size_mb * 1.5, # 50% extra space - available_space_mb=10000, # Placeholder - feasible=True, - issues=[] - ) - - async def _execute_migration(self, chain_id: str, from_node: str, to_node: str) -> ChainMigrationResult: - """Execute the actual migration""" - # This would actually execute the migration - logger.info(f"Migrating chain {chain_id} from {from_node} to {to_node}") - return ChainMigrationResult( - chain_id=chain_id, - source_node=from_node, - target_node=to_node, - success=True, - blocks_transferred=1000, # Placeholder - transfer_time_seconds=300, # Placeholder - verification_passed=True - ) - - async def _execute_backup(self, chain_id: str, node_id: str, backup_path: str, compress: bool, verify: bool) -> ChainBackupResult: - """Execute the actual backup""" - if node_id not in self.config.nodes: - raise NodeNotAvailableError(f"Node {node_id} not configured") - - node_config = self.config.nodes[node_id] - - try: - async with NodeClient(node_config) as client: - backup_info = await client.backup_chain(chain_id, backup_path) - - return ChainBackupResult( - chain_id=chain_id, - backup_file=backup_info["backup_file"], - original_size_mb=backup_info["original_size_mb"], - backup_size_mb=backup_info["backup_size_mb"], - compression_ratio=backup_info["original_size_mb"] / backup_info["backup_size_mb"], - checksum=backup_info["checksum"], - verification_passed=verify - ) - except Exception as e: - logger.error(f"Error during backup: {e}") - raise - - async def _execute_restore(self, backup_path: str, node_id: str, verify: bool) -> ChainRestoreResult: - """Execute the actual restore""" - if node_id not in self.config.nodes: - raise NodeNotAvailableError(f"Node {node_id} not configured") - - node_config = self.config.nodes[node_id] - - try: - async with NodeClient(node_config) as client: - restore_info = await client.restore_chain(backup_path) - - return ChainRestoreResult( - chain_id=restore_info["chain_id"], - node_id=node_id, - blocks_restored=restore_info["blocks_restored"], - verification_passed=restore_info["verification_passed"] - ) - except Exception as e: - logger.error(f"Error during restore: {e}") - raise - - async def _select_best_node_for_restore(self) -> str: - """Select the best node for restoring a chain""" - available_nodes = list(self.config.nodes.keys()) - if not available_nodes: - raise NodeNotAvailableError("No nodes available") - return available_nodes[0] diff --git a/cli/src/aitbc_cli/core/config.py b/cli/src/aitbc_cli/core/config.py deleted file mode 100755 index daaf7485..00000000 --- a/cli/src/aitbc_cli/core/config.py +++ /dev/null @@ -1,101 +0,0 @@ -""" -Multi-chain configuration management for AITBC CLI -""" - -from pathlib import Path -from typing import Dict, Any, Optional -import yaml -from pydantic import BaseModel, Field - -class NodeConfig(BaseModel): - """Configuration for a specific node""" - id: str = Field(..., description="Node identifier") - endpoint: str = Field(..., description="Node endpoint URL") - timeout: int = Field(default=30, description="Request timeout in seconds") - retry_count: int = Field(default=3, description="Number of retry attempts") - max_connections: int = Field(default=10, description="Maximum concurrent connections") - -class ChainConfig(BaseModel): - """Default chain configuration""" - default_gas_limit: int = Field(default=10000000, description="Default gas limit") - default_gas_price: int = Field(default=20000000000, description="Default gas price in wei") - max_block_size: int = Field(default=1048576, description="Maximum block size in bytes") - backup_path: Path = Field(default=Path("./backups"), description="Backup directory path") - max_concurrent_chains: int = Field(default=100, description="Maximum concurrent chains per node") - -class MultiChainConfig(BaseModel): - """Multi-chain configuration""" - nodes: Dict[str, NodeConfig] = Field(default_factory=dict, description="Node configurations") - chains: ChainConfig = Field(default_factory=ChainConfig, description="Chain configuration") - logging_level: str = Field(default="INFO", description="Logging level") - enable_caching: bool = Field(default=True, description="Enable response caching") - cache_ttl: int = Field(default=300, description="Cache TTL in seconds") - -def load_multichain_config(config_path: Optional[str] = None) -> MultiChainConfig: - """Load multi-chain configuration from file""" - if config_path is None: - config_path = Path.home() / ".aitbc" / "multichain_config.yaml" - - config_file = Path(config_path) - - if not config_file.exists(): - # Create default configuration - default_config = MultiChainConfig() - save_multichain_config(default_config, config_path) - return default_config - - try: - with open(config_file, 'r') as f: - config_data = yaml.safe_load(f) - - return MultiChainConfig(**config_data) - except Exception as e: - raise ValueError(f"Failed to load configuration from {config_path}: {e}") - -def save_multichain_config(config: MultiChainConfig, config_path: Optional[str] = None) -> None: - """Save multi-chain configuration to file""" - if config_path is None: - config_path = Path.home() / ".aitbc" / "multichain_config.yaml" - - config_file = Path(config_path) - config_file.parent.mkdir(parents=True, exist_ok=True) - - try: - # Convert Path objects to strings for YAML serialization - config_dict = config.dict() - if 'chains' in config_dict and 'backup_path' in config_dict['chains']: - config_dict['chains']['backup_path'] = str(config_dict['chains']['backup_path']) - - with open(config_file, 'w') as f: - yaml.dump(config_dict, f, default_flow_style=False, indent=2) - except Exception as e: - raise ValueError(f"Failed to save configuration to {config_path}: {e}") - -def get_default_node_config() -> NodeConfig: - """Get default node configuration for local development""" - return NodeConfig( - id="default-node", - endpoint="http://localhost:8545", - timeout=30, - retry_count=3, - max_connections=10 - ) - -def add_node_config(config: MultiChainConfig, node_config: NodeConfig) -> MultiChainConfig: - """Add a node configuration""" - config.nodes[node_config.id] = node_config - return config - -def remove_node_config(config: MultiChainConfig, node_id: str) -> MultiChainConfig: - """Remove a node configuration""" - if node_id in config.nodes: - del config.nodes[node_id] - return config - -def get_node_config(config: MultiChainConfig, node_id: str) -> Optional[NodeConfig]: - """Get a specific node configuration""" - return config.nodes.get(node_id) - -def list_node_configs(config: MultiChainConfig) -> Dict[str, NodeConfig]: - """List all node configurations""" - return config.nodes.copy() diff --git a/cli/src/aitbc_cli/core/genesis_generator.py b/cli/src/aitbc_cli/core/genesis_generator.py deleted file mode 100755 index 3d0f84ae..00000000 --- a/cli/src/aitbc_cli/core/genesis_generator.py +++ /dev/null @@ -1,361 +0,0 @@ -""" -Genesis block generator for multi-chain functionality -""" - -import hashlib -import json -import yaml -from datetime import datetime -from pathlib import Path -from typing import Dict, Any, Optional -from .config import MultiChainConfig -from models.chain import GenesisBlock, GenesisConfig, ChainType, ConsensusAlgorithm - -class GenesisValidationError(Exception): - """Genesis validation error""" - pass - -class GenesisGenerator: - """Genesis block generator""" - - def __init__(self, config: MultiChainConfig): - self.config = config - self.templates_dir = Path(__file__).parent.parent.parent / "templates" / "genesis" - - def create_genesis(self, genesis_config: GenesisConfig) -> GenesisBlock: - """Create a genesis block from configuration""" - # Validate configuration - self._validate_genesis_config(genesis_config) - - # Generate chain ID if not provided - if not genesis_config.chain_id: - genesis_config.chain_id = self._generate_chain_id(genesis_config) - - # Set timestamp if not provided - if not genesis_config.timestamp: - genesis_config.timestamp = datetime.now() - - # Calculate state root - state_root = self._calculate_state_root(genesis_config) - - # Calculate genesis hash - genesis_hash = self._calculate_genesis_hash(genesis_config, state_root) - - # Create genesis block - genesis_block = GenesisBlock( - chain_id=genesis_config.chain_id, - chain_type=genesis_config.chain_type, - purpose=genesis_config.purpose, - name=genesis_config.name, - description=genesis_config.description, - timestamp=genesis_config.timestamp, - parent_hash=genesis_config.parent_hash, - gas_limit=genesis_config.gas_limit, - gas_price=genesis_config.gas_price, - difficulty=genesis_config.difficulty, - block_time=genesis_config.block_time, - accounts=genesis_config.accounts, - contracts=genesis_config.contracts, - consensus=genesis_config.consensus, - privacy=genesis_config.privacy, - parameters=genesis_config.parameters, - state_root=state_root, - hash=genesis_hash - ) - - return genesis_block - - def create_from_template(self, template_name: str, custom_config_file: str) -> GenesisBlock: - """Create genesis block from template""" - # Load template - template_path = self.templates_dir / f"{template_name}.yaml" - if not template_path.exists(): - raise ValueError(f"Template {template_name} not found at {template_path}") - - with open(template_path, 'r') as f: - template_data = yaml.safe_load(f) - - # Load custom configuration - with open(custom_config_file, 'r') as f: - custom_data = yaml.safe_load(f) - - # Merge template with custom config - merged_config = self._merge_configs(template_data, custom_data) - - # Create genesis config - genesis_config = GenesisConfig(**merged_config['genesis']) - - # Create genesis block - return self.create_genesis(genesis_config) - - def validate_genesis(self, genesis_block: GenesisBlock) -> 'ValidationResult': - """Validate a genesis block""" - errors = [] - checks = {} - - # Check required fields - checks['chain_id'] = bool(genesis_block.chain_id) - if not genesis_block.chain_id: - errors.append("Chain ID is required") - - checks['chain_type'] = genesis_block.chain_type in ChainType - if genesis_block.chain_type not in ChainType: - errors.append(f"Invalid chain type: {genesis_block.chain_type}") - - checks['purpose'] = bool(genesis_block.purpose) - if not genesis_block.purpose: - errors.append("Purpose is required") - - checks['name'] = bool(genesis_block.name) - if not genesis_block.name: - errors.append("Name is required") - - checks['timestamp'] = isinstance(genesis_block.timestamp, datetime) - if not isinstance(genesis_block.timestamp, datetime): - errors.append("Invalid timestamp format") - - checks['consensus'] = bool(genesis_block.consensus) - if not genesis_block.consensus: - errors.append("Consensus configuration is required") - - checks['hash'] = bool(genesis_block.hash) - if not genesis_block.hash: - errors.append("Genesis hash is required") - - # Validate hash - if genesis_block.hash: - calculated_hash = self._calculate_genesis_hash(genesis_block, genesis_block.state_root) - checks['hash_valid'] = genesis_block.hash == calculated_hash - if genesis_block.hash != calculated_hash: - errors.append("Genesis hash does not match calculated hash") - - # Validate state root - if genesis_block.state_root: - calculated_state_root = self._calculate_state_root_from_block(genesis_block) - checks['state_root_valid'] = genesis_block.state_root == calculated_state_root - if genesis_block.state_root != calculated_state_root: - errors.append("State root does not match calculated state root") - - # Validate accounts - checks['accounts_valid'] = all( - bool(account.address) and bool(account.balance) - for account in genesis_block.accounts - ) - if not checks['accounts_valid']: - errors.append("All accounts must have address and balance") - - # Validate contracts - checks['contracts_valid'] = all( - bool(contract.name) and bool(contract.address) and bool(contract.bytecode) - for contract in genesis_block.contracts - ) - if not checks['contracts_valid']: - errors.append("All contracts must have name, address, and bytecode") - - # Validate consensus - if genesis_block.consensus: - checks['consensus_algorithm'] = genesis_block.consensus.algorithm in ConsensusAlgorithm - if genesis_block.consensus.algorithm not in ConsensusAlgorithm: - errors.append(f"Invalid consensus algorithm: {genesis_block.consensus.algorithm}") - - return ValidationResult( - is_valid=len(errors) == 0, - errors=errors, - checks=checks - ) - - def get_genesis_info(self, genesis_file: str) -> Dict[str, Any]: - """Get information about a genesis block file""" - genesis_path = Path(genesis_file) - if not genesis_path.exists(): - raise FileNotFoundError(f"Genesis file {genesis_file} not found") - - # Load genesis block - if genesis_path.suffix.lower() in ['.yaml', '.yml']: - with open(genesis_path, 'r') as f: - genesis_data = yaml.safe_load(f) - else: - with open(genesis_path, 'r') as f: - genesis_data = json.load(f) - - genesis_block = GenesisBlock(**genesis_data) - - return { - "chain_id": genesis_block.chain_id, - "chain_type": genesis_block.chain_type.value, - "purpose": genesis_block.purpose, - "name": genesis_block.name, - "description": genesis_block.description, - "created": genesis_block.timestamp.isoformat(), - "genesis_hash": genesis_block.hash, - "state_root": genesis_block.state_root, - "consensus_algorithm": genesis_block.consensus.algorithm.value, - "block_time": genesis_block.block_time, - "gas_limit": genesis_block.gas_limit, - "gas_price": genesis_block.gas_price, - "accounts_count": len(genesis_block.accounts), - "contracts_count": len(genesis_block.contracts), - "privacy_visibility": genesis_block.privacy.visibility, - "access_control": genesis_block.privacy.access_control, - "file_size": genesis_path.stat().st_size, - "file_format": genesis_path.suffix.lower().replace('.', '') - } - - def export_genesis(self, chain_id: str, format: str = "json") -> str: - """Export genesis block in specified format""" - # This would get the genesis block from storage - # For now, return placeholder - return f"Genesis block for {chain_id} in {format} format" - - def calculate_genesis_hash(self, genesis_file: str) -> str: - """Calculate genesis hash from file""" - genesis_path = Path(genesis_file) - if not genesis_path.exists(): - raise FileNotFoundError(f"Genesis file {genesis_file} not found") - - # Load genesis block - if genesis_path.suffix.lower() in ['.yaml', '.yml']: - with open(genesis_path, 'r') as f: - genesis_data = yaml.safe_load(f) - else: - with open(genesis_path, 'r') as f: - genesis_data = json.load(f) - - genesis_block = GenesisBlock(**genesis_data) - - return self._calculate_genesis_hash(genesis_block, genesis_block.state_root) - - def list_templates(self) -> Dict[str, Dict[str, Any]]: - """List available genesis templates""" - templates = {} - - if not self.templates_dir.exists(): - return templates - - for template_file in self.templates_dir.glob("*.yaml"): - template_name = template_file.stem - - try: - with open(template_file, 'r') as f: - template_data = yaml.safe_load(f) - - templates[template_name] = { - "name": template_name, - "description": template_data.get('description', ''), - "chain_type": template_data.get('genesis', {}).get('chain_type', 'unknown'), - "purpose": template_data.get('genesis', {}).get('purpose', 'unknown'), - "file_path": str(template_file) - } - except Exception as e: - templates[template_name] = { - "name": template_name, - "description": f"Error loading template: {e}", - "chain_type": "error", - "purpose": "error", - "file_path": str(template_file) - } - - return templates - - # Private methods - - def _validate_genesis_config(self, genesis_config: GenesisConfig) -> None: - """Validate genesis configuration""" - if not genesis_config.chain_type: - raise GenesisValidationError("Chain type is required") - - if not genesis_config.purpose: - raise GenesisValidationError("Purpose is required") - - if not genesis_config.name: - raise GenesisValidationError("Name is required") - - if not genesis_config.consensus: - raise GenesisValidationError("Consensus configuration is required") - - if genesis_config.consensus.algorithm not in ConsensusAlgorithm: - raise GenesisValidationError(f"Invalid consensus algorithm: {genesis_config.consensus.algorithm}") - - def _generate_chain_id(self, genesis_config: GenesisConfig) -> str: - """Generate a unique chain ID""" - timestamp = datetime.now().strftime("%Y%m%d%H%M%S") - prefix = f"AITBC-{genesis_config.chain_type.value.upper()}-{genesis_config.purpose.upper()}" - return f"{prefix}-{timestamp}" - - def _calculate_state_root(self, genesis_config: GenesisConfig) -> str: - """Calculate state root hash""" - state_data = { - "chain_id": genesis_config.chain_id, - "chain_type": genesis_config.chain_type.value, - "purpose": genesis_config.purpose, - "name": genesis_config.name, - "timestamp": genesis_config.timestamp.isoformat() if genesis_config.timestamp else datetime.now().isoformat(), - "accounts": [account.dict() for account in genesis_config.accounts], - "contracts": [contract.dict() for contract in genesis_config.contracts], - "parameters": genesis_config.parameters.dict() - } - - state_json = json.dumps(state_data, sort_keys=True) - return hashlib.sha256(state_json.encode()).hexdigest() - - def _calculate_genesis_hash(self, genesis_config: GenesisConfig, state_root: str) -> str: - """Calculate genesis block hash""" - genesis_data = { - "chain_id": genesis_config.chain_id, - "chain_type": genesis_config.chain_type.value, - "purpose": genesis_config.purpose, - "name": genesis_config.name, - "timestamp": genesis_config.timestamp.isoformat() if genesis_config.timestamp else datetime.now().isoformat(), - "parent_hash": genesis_config.parent_hash, - "gas_limit": genesis_config.gas_limit, - "gas_price": genesis_config.gas_price, - "difficulty": genesis_config.difficulty, - "block_time": genesis_config.block_time, - "consensus": genesis_config.consensus.dict(), - "privacy": genesis_config.privacy.dict(), - "parameters": genesis_config.parameters.dict(), - "state_root": state_root - } - - genesis_json = json.dumps(genesis_data, sort_keys=True) - return hashlib.sha256(genesis_json.encode()).hexdigest() - - def _calculate_state_root_from_block(self, genesis_block: GenesisBlock) -> str: - """Calculate state root from genesis block""" - state_data = { - "chain_id": genesis_block.chain_id, - "chain_type": genesis_block.chain_type.value, - "purpose": genesis_block.purpose, - "name": genesis_block.name, - "timestamp": genesis_block.timestamp.isoformat(), - "accounts": [account.dict() for account in genesis_block.accounts], - "contracts": [contract.dict() for contract in genesis_block.contracts], - "parameters": genesis_block.parameters.dict() - } - - state_json = json.dumps(state_data, sort_keys=True) - return hashlib.sha256(state_json.encode()).hexdigest() - - def _merge_configs(self, template: Dict[str, Any], custom: Dict[str, Any]) -> Dict[str, Any]: - """Merge template configuration with custom overrides""" - result = template.copy() - - if 'genesis' in custom: - for key, value in custom['genesis'].items(): - if isinstance(value, dict) and key in result.get('genesis', {}): - result['genesis'][key].update(value) - else: - if 'genesis' not in result: - result['genesis'] = {} - result['genesis'][key] = value - - return result - - -class ValidationResult: - """Genesis validation result""" - - def __init__(self, is_valid: bool, errors: list, checks: dict): - self.is_valid = is_valid - self.errors = errors - self.checks = checks diff --git a/cli/src/aitbc_cli/core/imports.py b/cli/src/aitbc_cli/core/imports.py deleted file mode 100644 index 2cad5d20..00000000 --- a/cli/src/aitbc_cli/core/imports.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Import setup for AITBC CLI to access coordinator-api services.""" - -import sys -from pathlib import Path - -def ensure_coordinator_api_imports(): - """Ensure coordinator-api src directory is on sys.path.""" - _src_path = Path(__file__).resolve().parent.parent.parent / 'apps' / 'coordinator-api' / 'src' - if str(_src_path) not in sys.path: - sys.path.insert(0, str(_src_path)) diff --git a/cli/src/aitbc_cli/core/main.py b/cli/src/aitbc_cli/core/main.py deleted file mode 100644 index 1e7598db..00000000 --- a/cli/src/aitbc_cli/core/main.py +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env python3 -""" -AITBC CLI - Fixed version with modular command groups -""" - -import click - -# Import modular command groups -from aitbc_cli.commands.system import system -from aitbc_cli.commands.marketplace_cmd import marketplace -from aitbc_cli.commands.chain import chain -from aitbc_cli.commands.agent_sdk import agent - -# Import island-specific commands -from aitbc_cli.commands.gpu_marketplace import gpu -from aitbc_cli.commands.exchange_island import exchange_island -from aitbc_cli.commands.wallet import wallet -from aitbc_cli.commands.genesis import genesis - -# Import new modular commands -from aitbc_cli.commands.transactions import transactions -from aitbc_cli.commands.mining import mining -from aitbc_cli.commands.hermes import hermes -from aitbc_cli.commands.workflow import workflow -from aitbc_cli.commands.resource import resource -from aitbc_cli.commands.operations import operations -from aitbc_cli.commands.simulate import simulate -from aitbc_cli.commands.edge import edge - -# Force CLI version for user-facing output -__version__ = "2.1.0" - - -@click.command(name="list") -def list_wallets(): - """Legacy wallet list alias""" - return wallet.main(args=["list"], standalone_mode=False) - - -@click.command() -def version(): - """Show version information""" - click.echo(f"aitbc, version {__version__}") - click.echo("System Architecture Support: āœ…") - click.echo("FHS Compliance: āœ…") - click.echo("New Features: āœ…") - -@click.group() -@click.version_option(version=__version__, prog_name="aitbc") -@click.option( - "--url", - default=None, - help="Coordinator API URL (overrides config)" -) -@click.option( - "--api-key", - default=None, - help="API key for authentication" -) -@click.option( - "--chain-id", - default=None, - help="Chain ID for multichain operations (e.g., ait-mainnet, ait-devnet)" -) -@click.option( - "--output", - default="table", - type=click.Choice(["table", "json", "yaml", "csv"]), - help="Output format" -) -@click.option( - "--verbose", - "-v", - count=True, - help="Increase verbosity (can be used multiple times)" -) -@click.option( - "--debug", - is_flag=True, - help="Enable debug mode" -) -@click.pass_context -def cli(ctx, url, api_key, chain_id, output, verbose, debug): - """AITBC CLI - Command Line Interface for AITBC Network - - Manage jobs, mining, wallets, blockchain operations, marketplaces, and AI - services. - - SYSTEM ARCHITECTURE COMMANDS: - system System management commands - system architect System architecture analysis - system audit Audit system compliance - system check Check service configuration - - Examples: - aitbc system architect - aitbc system audit - aitbc system check --service marketplace - """ - ctx.ensure_object(dict) - ctx.obj['url'] = url - ctx.obj['api_key'] = api_key - ctx.obj['output'] = output - ctx.obj['verbose'] = verbose - ctx.obj['debug'] = debug - - # Handle chain_id with auto-detection - from aitbc_cli.utils.chain_id import get_chain_id - default_rpc_url = url.replace('/api', '') if url else 'http://localhost:8006' - ctx.obj['chain_id'] = get_chain_id(default_rpc_url, override=chain_id) - -# Add commands to CLI -cli.add_command(system) -cli.add_command(marketplace, name="market") -cli.add_command(chain, name="blockchain") -cli.add_command(agent, name="ai") -cli.add_command(list_wallets) -cli.add_command(version) -cli.add_command(gpu) -cli.add_command(exchange_island) -cli.add_command(wallet) -cli.add_command(genesis) - -# Add new modular commands -cli.add_command(transactions) -cli.add_command(mining) -cli.add_command(hermes) -cli.add_command(workflow) -cli.add_command(resource) -cli.add_command(operations) -cli.add_command(simulate) -cli.add_command(edge) - -def main(argv=None): - """Entry point for console scripts and compatibility wrappers.""" - return cli.main(args=argv, prog_name="aitbc", standalone_mode=False) - - -if __name__ == '__main__': - raise SystemExit(main()) diff --git a/cli/src/aitbc_cli/core/marketplace.py b/cli/src/aitbc_cli/core/marketplace.py deleted file mode 100755 index 16a61a02..00000000 --- a/cli/src/aitbc_cli/core/marketplace.py +++ /dev/null @@ -1,666 +0,0 @@ -""" -Global chain marketplace system -""" - -import asyncio -import json -import hashlib -import time -from datetime import datetime, timedelta -from typing import Dict, List, Optional, Any, Set -from dataclasses import dataclass, asdict -from enum import Enum -import uuid -from decimal import Decimal -from collections import defaultdict - -from .config import MultiChainConfig -from .node_client import NodeClient -import logging -logger = logging.getLogger(__name__) - - -class ChainType(Enum): - """Chain types in marketplace""" - TOPIC = "topic" - PRIVATE = "private" - RESEARCH = "research" - ENTERPRISE = "enterprise" - GOVERNANCE = "governance" - -class MarketplaceStatus(Enum): - """Marketplace listing status""" - ACTIVE = "active" - PENDING = "pending" - SOLD = "sold" - EXPIRED = "expired" - DELISTED = "delisted" - -class TransactionStatus(Enum): - """Transaction status""" - PENDING = "pending" - CONFIRMED = "confirmed" - COMPLETED = "completed" - FAILED = "failed" - REFUNDED = "refunded" - -@dataclass -class ChainListing: - """Chain marketplace listing""" - listing_id: str - chain_id: str - chain_name: str - chain_type: ChainType - description: str - seller_id: str - price: Decimal - currency: str - status: MarketplaceStatus - created_at: datetime - expires_at: datetime - metadata: Dict[str, Any] - chain_specifications: Dict[str, Any] - performance_metrics: Dict[str, Any] - reputation_requirements: Dict[str, Any] - governance_rules: Dict[str, Any] - -@dataclass -class MarketplaceTransaction: - """Marketplace transaction""" - transaction_id: str - listing_id: str - buyer_id: str - seller_id: str - chain_id: str - price: Decimal - currency: str - status: TransactionStatus - created_at: datetime - completed_at: Optional[datetime] - escrow_address: str - smart_contract_address: str - transaction_hash: Optional[str] - metadata: Dict[str, Any] - -@dataclass -class ChainEconomy: - """Chain economic metrics""" - chain_id: str - total_value_locked: Decimal - daily_volume: Decimal - market_cap: Decimal - price_history: List[Dict[str, Any]] - transaction_count: int - active_users: int - agent_count: int - governance_tokens: Decimal - staking_rewards: Decimal - last_updated: datetime - -@dataclass -class MarketplaceMetrics: - """Marketplace performance metrics""" - total_listings: int - active_listings: int - total_transactions: int - total_volume: Decimal - average_price: Decimal - popular_chain_types: Dict[str, int] - top_sellers: List[Dict[str, Any]] - price_trends: Dict[str, List[Decimal]] - market_sentiment: float - last_updated: datetime - -class GlobalChainMarketplace: - """Global chain marketplace system""" - - def __init__(self, config: MultiChainConfig): - self.config = config - self.listings: Dict[str, ChainListing] = {} - self.transactions: Dict[str, MarketplaceTransaction] = {} - self.chain_economies: Dict[str, ChainEconomy] = {} - self.user_reputations: Dict[str, float] = {} - self.market_metrics: Optional[MarketplaceMetrics] = None - self.escrow_contracts: Dict[str, Dict[str, Any]] = {} - self.price_history: Dict[str, List[Decimal]] = defaultdict(list) - - # Marketplace thresholds - self.thresholds = { - 'min_reputation_score': 0.5, - 'max_listing_duration_days': 30, - 'escrow_fee_percentage': 0.02, # 2% - 'marketplace_fee_percentage': 0.01, # 1% - 'min_chain_price': Decimal('0.001'), - 'max_chain_price': Decimal('1000000') - } - - async def create_listing(self, chain_id: str, chain_name: str, chain_type: ChainType, - description: str, seller_id: str, price: Decimal, currency: str, - chain_specifications: Dict[str, Any], metadata: Dict[str, Any]) -> Optional[str]: - """Create a new chain listing in the marketplace""" - try: - # Validate seller reputation - if self.user_reputations.get(seller_id, 0) < self.thresholds['min_reputation_score']: - return None - - # Validate price - if price < self.thresholds['min_chain_price'] or price > self.thresholds['max_chain_price']: - return None - - # Check if chain already has active listing - for listing in self.listings.values(): - if listing.chain_id == chain_id and listing.status == MarketplaceStatus.ACTIVE: - return None - - # Create listing - listing_id = str(uuid.uuid4()) - expires_at = datetime.now() + timedelta(days=self.thresholds['max_listing_duration_days']) - - listing = ChainListing( - listing_id=listing_id, - chain_id=chain_id, - chain_name=chain_name, - chain_type=chain_type, - description=description, - seller_id=seller_id, - price=price, - currency=currency, - status=MarketplaceStatus.ACTIVE, - created_at=datetime.now(), - expires_at=expires_at, - metadata=metadata, - chain_specifications=chain_specifications, - performance_metrics={}, - reputation_requirements={"min_score": 0.5}, - governance_rules={"voting_threshold": 0.6} - ) - - self.listings[listing_id] = listing - - # Update price history - self.price_history[chain_id].append(price) - - # Update market metrics - await self._update_market_metrics() - - return listing_id - - except Exception as e: - logger.error(f"Error creating listing: {e}") - return None - - async def purchase_chain(self, listing_id: str, buyer_id: str, payment_method: str) -> Optional[str]: - """Purchase a chain from the marketplace""" - try: - listing = self.listings.get(listing_id) - if not listing or listing.status != MarketplaceStatus.ACTIVE: - return None - - # Validate buyer reputation - if self.user_reputations.get(buyer_id, 0) < self.thresholds['min_reputation_score']: - return None - - # Check if listing is expired - if datetime.now() > listing.expires_at: - listing.status = MarketplaceStatus.EXPIRED - return None - - # Create transaction - transaction_id = str(uuid.uuid4()) - escrow_address = f"escrow_{transaction_id[:8]}" - smart_contract_address = f"contract_{transaction_id[:8]}" - - transaction = MarketplaceTransaction( - transaction_id=transaction_id, - listing_id=listing_id, - buyer_id=buyer_id, - seller_id=listing.seller_id, - chain_id=listing.chain_id, - price=listing.price, - currency=listing.currency, - status=TransactionStatus.PENDING, - created_at=datetime.now(), - completed_at=None, - escrow_address=escrow_address, - smart_contract_address=smart_contract_address, - transaction_hash=None, - metadata={"payment_method": payment_method} - ) - - self.transactions[transaction_id] = transaction - - # Create escrow contract - await self._create_escrow_contract(transaction) - - # Update listing status - listing.status = MarketplaceStatus.SOLD - - # Update market metrics - await self._update_market_metrics() - - return transaction_id - - except Exception as e: - logger.error(f"Error purchasing chain: {e}") - return None - - async def complete_transaction(self, transaction_id: str, transaction_hash: str) -> bool: - """Complete a marketplace transaction""" - try: - transaction = self.transactions.get(transaction_id) - if not transaction or transaction.status != TransactionStatus.PENDING: - return False - - # Update transaction - transaction.status = TransactionStatus.COMPLETED - transaction.completed_at = datetime.now() - transaction.transaction_hash = transaction_hash - - # Release escrow - await self._release_escrow(transaction) - - # Update reputations - self._update_user_reputation(transaction.buyer_id, 0.1) # Positive update - self._update_user_reputation(transaction.seller_id, 0.1) - - # Update chain economy - await self._update_chain_economy(transaction.chain_id, transaction.price) - - # Update market metrics - await self._update_market_metrics() - - return True - - except Exception as e: - logger.error(f"Error completing transaction: {e}") - return False - - async def get_chain_economy(self, chain_id: str) -> Optional[ChainEconomy]: - """Get economic metrics for a specific chain""" - try: - if chain_id not in self.chain_economies: - # Initialize chain economy - self.chain_economies[chain_id] = ChainEconomy( - chain_id=chain_id, - total_value_locked=Decimal('0'), - daily_volume=Decimal('0'), - market_cap=Decimal('0'), - price_history=[], - transaction_count=0, - active_users=0, - agent_count=0, - governance_tokens=Decimal('0'), - staking_rewards=Decimal('0'), - last_updated=datetime.now() - ) - - # Update with latest data - await self._update_chain_economy(chain_id) - - return self.chain_economies[chain_id] - - except Exception as e: - logger.error(f"Error getting chain economy: {e}") - return None - - async def search_listings(self, chain_type: Optional[ChainType] = None, - min_price: Optional[Decimal] = None, - max_price: Optional[Decimal] = None, - seller_id: Optional[str] = None, - status: Optional[MarketplaceStatus] = None) -> List[ChainListing]: - """Search chain listings with filters""" - try: - results = [] - - for listing in self.listings.values(): - # Apply filters - if chain_type and listing.chain_type != chain_type: - continue - - if min_price and listing.price < min_price: - continue - - if max_price and listing.price > max_price: - continue - - if seller_id and listing.seller_id != seller_id: - continue - - if status and listing.status != status: - continue - - results.append(listing) - - # Sort by creation date (newest first) - results.sort(key=lambda x: x.created_at, reverse=True) - - return results - - except Exception as e: - logger.error(f"Error searching listings: {e}") - return [] - - async def get_user_transactions(self, user_id: str, role: str = "both") -> List[MarketplaceTransaction]: - """Get transactions for a specific user""" - try: - results = [] - - for transaction in self.transactions.values(): - if role == "buyer" and transaction.buyer_id != user_id: - continue - - if role == "seller" and transaction.seller_id != user_id: - continue - - if role == "both" and transaction.buyer_id != user_id and transaction.seller_id != user_id: - continue - - results.append(transaction) - - # Sort by creation date (newest first) - results.sort(key=lambda x: x.created_at, reverse=True) - - return results - - except Exception as e: - logger.error(f"Error getting user transactions: {e}") - return [] - - async def get_marketplace_overview(self) -> Dict[str, Any]: - """Get comprehensive marketplace overview""" - try: - await self._update_market_metrics() - - if not self.market_metrics: - return {} - - # Calculate additional metrics - total_volume_24h = await self._calculate_24h_volume() - top_chains = await self._get_top_performing_chains() - price_trends = await self._calculate_price_trends() - - overview = { - "marketplace_metrics": asdict(self.market_metrics), - "volume_24h": total_volume_24h, - "top_performing_chains": top_chains, - "price_trends": price_trends, - "chain_types_distribution": await self._get_chain_types_distribution(), - "user_activity": await self._get_user_activity_metrics(), - "escrow_summary": await self._get_escrow_summary() - } - - return overview - - except Exception as e: - logger.error(f"Error getting marketplace overview: {e}") - return {} - - async def _create_escrow_contract(self, transaction: MarketplaceTransaction): - """Create escrow contract for transaction""" - try: - escrow_contract = { - "contract_address": transaction.escrow_address, - "transaction_id": transaction.transaction_id, - "amount": transaction.price, - "currency": transaction.currency, - "buyer_id": transaction.buyer_id, - "seller_id": transaction.seller_id, - "created_at": datetime.now(), - "status": "active", - "release_conditions": { - "transaction_confirmed": False, - "dispute_resolved": False - } - } - - self.escrow_contracts[transaction.escrow_address] = escrow_contract - - except Exception as e: - logger.error(f"Error creating escrow contract: {e}") - async def _release_escrow(self, transaction: MarketplaceTransaction): - """Release escrow funds""" - try: - escrow_contract = self.escrow_contracts.get(transaction.escrow_address) - if escrow_contract: - escrow_contract["status"] = "released" - escrow_contract["released_at"] = datetime.now() - escrow_contract["release_conditions"]["transaction_confirmed"] = True - - # Calculate fees - escrow_fee = transaction.price * Decimal(str(self.thresholds['escrow_fee_percentage'])) - marketplace_fee = transaction.price * Decimal(str(self.thresholds['marketplace_fee_percentage'])) - seller_amount = transaction.price - escrow_fee - marketplace_fee - - escrow_contract["fee_breakdown"] = { - "escrow_fee": escrow_fee, - "marketplace_fee": marketplace_fee, - "seller_amount": seller_amount - } - - except Exception as e: - logger.error(f"Error releasing escrow: {e}") - async def _update_chain_economy(self, chain_id: str, transaction_price: Optional[Decimal] = None): - """Update chain economic metrics""" - try: - if chain_id not in self.chain_economies: - self.chain_economies[chain_id] = ChainEconomy( - chain_id=chain_id, - total_value_locked=Decimal('0'), - daily_volume=Decimal('0'), - market_cap=Decimal('0'), - price_history=[], - transaction_count=0, - active_users=0, - agent_count=0, - governance_tokens=Decimal('0'), - staking_rewards=Decimal('0'), - last_updated=datetime.now() - ) - - economy = self.chain_economies[chain_id] - - # Update with transaction price if provided - if transaction_price: - economy.daily_volume += transaction_price - economy.transaction_count += 1 - - # Add to price history - economy.price_history.append({ - "price": float(transaction_price), - "timestamp": datetime.now().isoformat(), - "volume": float(transaction_price) - }) - - # Update other metrics (would be fetched from chain nodes) - # For now, using mock data - economy.active_users = max(10, economy.active_users) - economy.agent_count = max(5, economy.agent_count) - economy.total_value_locked = economy.daily_volume * Decimal('10') # Mock TVL - economy.market_cap = economy.daily_volume * Decimal('100') # Mock market cap - - economy.last_updated = datetime.now() - - except Exception as e: - logger.error(f"Error updating chain economy: {e}") - async def _update_market_metrics(self): - """Update marketplace performance metrics""" - try: - total_listings = len(self.listings) - active_listings = len([l for l in self.listings.values() if l.status == MarketplaceStatus.ACTIVE]) - total_transactions = len(self.transactions) - - # Calculate total volume and average price - completed_transactions = [t for t in self.transactions.values() if t.status == TransactionStatus.COMPLETED] - total_volume = sum(t.price for t in completed_transactions) - average_price = total_volume / len(completed_transactions) if completed_transactions else Decimal('0') - - # Popular chain types - chain_types = defaultdict(int) - for listing in self.listings.values(): - chain_types[listing.chain_type.value] += 1 - - # Top sellers - seller_stats = defaultdict(lambda: {"count": 0, "volume": Decimal('0')}) - for transaction in completed_transactions: - seller_stats[transaction.seller_id]["count"] += 1 - seller_stats[transaction.seller_id]["volume"] += transaction.price - - top_sellers = [ - {"seller_id": seller_id, "sales_count": stats["count"], "total_volume": float(stats["volume"])} - for seller_id, stats in seller_stats.items() - ] - top_sellers.sort(key=lambda x: x["total_volume"], reverse=True) - top_sellers = top_sellers[:10] # Top 10 - - # Price trends - price_trends = {} - for chain_id, prices in self.price_history.items(): - if len(prices) >= 2: - trend = (prices[-1] - prices[-2]) / prices[-2] if prices[-2] != 0 else 0 - price_trends[chain_id] = [trend] - - # Market sentiment (mock calculation) - market_sentiment = 0.5 # Neutral - if completed_transactions: - positive_ratio = len(completed_transactions) / max(1, total_transactions) - market_sentiment = min(1.0, positive_ratio * 1.2) - - self.market_metrics = MarketplaceMetrics( - total_listings=total_listings, - active_listings=active_listings, - total_transactions=total_transactions, - total_volume=total_volume, - average_price=average_price, - popular_chain_types=dict(chain_types), - top_sellers=top_sellers, - price_trends=price_trends, - market_sentiment=market_sentiment, - last_updated=datetime.now() - ) - - except Exception as e: - logger.error(f"Error updating market metrics: {e}") - def _update_user_reputation(self, user_id: str, delta: float): - """Update user reputation""" - try: - current_rep = self.user_reputations.get(user_id, 0.5) - new_rep = max(0.0, min(1.0, current_rep + delta)) - self.user_reputations[user_id] = new_rep - except Exception as e: - logger.error(f"Error updating user reputation: {e}") - async def _calculate_24h_volume(self) -> Decimal: - """Calculate 24-hour trading volume""" - try: - cutoff_time = datetime.now() - timedelta(hours=24) - recent_transactions = [ - t for t in self.transactions.values() - if t.created_at >= cutoff_time and t.status == TransactionStatus.COMPLETED - ] - - return sum(t.price for t in recent_transactions) - except Exception as e: - logger.error(f"Error calculating 24h volume: {e}") - return Decimal('0') - - async def _get_top_performing_chains(self, limit: int = 10) -> List[Dict[str, Any]]: - """Get top performing chains by volume""" - try: - chain_performance = defaultdict(lambda: {"volume": Decimal('0'), "transactions": 0}) - - for transaction in self.transactions.values(): - if transaction.status == TransactionStatus.COMPLETED: - chain_performance[transaction.chain_id]["volume"] += transaction.price - chain_performance[transaction.chain_id]["transactions"] += 1 - - top_chains = [ - { - "chain_id": chain_id, - "volume": float(stats["volume"]), - "transactions": stats["transactions"] - } - for chain_id, stats in chain_performance.items() - ] - - top_chains.sort(key=lambda x: x["volume"], reverse=True) - return top_chains[:limit] - - except Exception as e: - logger.error(f"Error getting top performing chains: {e}") - return [] - - async def _calculate_price_trends(self) -> Dict[str, List[float]]: - """Calculate price trends for all chains""" - try: - trends = {} - - for chain_id, prices in self.price_history.items(): - if len(prices) >= 2: - # Calculate simple trend - recent_prices = list(prices)[-10:] # Last 10 prices - if len(recent_prices) >= 2: - trend = (recent_prices[-1] - recent_prices[0]) / recent_prices[0] if recent_prices[0] != 0 else 0 - trends[chain_id] = [float(trend)] - - return trends - - except Exception as e: - logger.error(f"Error calculating price trends: {e}") - return {} - - async def _get_chain_types_distribution(self) -> Dict[str, int]: - """Get distribution of chain types""" - try: - distribution = defaultdict(int) - - for listing in self.listings.values(): - distribution[listing.chain_type.value] += 1 - - return dict(distribution) - - except Exception as e: - logger.error(f"Error getting chain types distribution: {e}") - return {} - - async def _get_user_activity_metrics(self) -> Dict[str, Any]: - """Get user activity metrics""" - try: - active_buyers = set() - active_sellers = set() - - for transaction in self.transactions.values(): - if transaction.created_at >= datetime.now() - timedelta(days=7): - active_buyers.add(transaction.buyer_id) - active_sellers.add(transaction.seller_id) - - return { - "active_buyers_7d": len(active_buyers), - "active_sellers_7d": len(active_sellers), - "total_unique_users": len(set(self.user_reputations.keys())), - "average_reputation": sum(self.user_reputations.values()) / len(self.user_reputations) if self.user_reputations else 0 - } - - except Exception as e: - logger.error(f"Error getting user activity metrics: {e}") - return {} - - async def _get_escrow_summary(self) -> Dict[str, Any]: - """Get escrow contract summary""" - try: - active_escrows = len([e for e in self.escrow_contracts.values() if e["status"] == "active"]) - released_escrows = len([e for e in self.escrow_contracts.values() if e["status"] == "released"]) - - total_escrow_value = sum( - Decimal(str(e["amount"])) for e in self.escrow_contracts.values() - if e["status"] == "active" - ) - - return { - "active_escrows": active_escrows, - "released_escrows": released_escrows, - "total_escrow_value": float(total_escrow_value), - "escrow_fee_collected": float(total_escrow_value * Decimal(str(self.thresholds['escrow_fee_percentage']))) - } - - except Exception as e: - logger.error(f"Error getting escrow summary: {e}") - return {} diff --git a/cli/src/aitbc_cli/core/node_client.py b/cli/src/aitbc_cli/core/node_client.py old mode 100755 new mode 100644 index 828228c3..cea1c0d7 --- a/cli/src/aitbc_cli/core/node_client.py +++ b/cli/src/aitbc_cli/core/node_client.py @@ -9,7 +9,7 @@ import os import logging from typing import Dict, List, Optional, Any from .config import NodeConfig -from models.chain import ChainInfo, ChainType, ChainStatus, ConsensusAlgorithm +from aitbc.models.chain import ChainInfo, ChainType, ChainStatus, ConsensusAlgorithm logger = logging.getLogger(__name__) diff --git a/cli/src/aitbc_cli/core/plugins.py b/cli/src/aitbc_cli/core/plugins.py deleted file mode 100755 index 38a0fbfb..00000000 --- a/cli/src/aitbc_cli/core/plugins.py +++ /dev/null @@ -1,302 +0,0 @@ -"""Plugin system for AITBC CLI custom commands""" - -import importlib -import importlib.util -import json -import click -from pathlib import Path -from typing import Optional - - -PLUGIN_DIR = Path.home() / ".aitbc" / "plugins" - - -def get_plugin_dir() -> Path: - """Get and ensure plugin directory exists""" - PLUGIN_DIR.mkdir(parents=True, exist_ok=True) - return PLUGIN_DIR - - -def load_plugins(cli_group): - """Load all plugins and register them with the CLI group""" - plugin_dir = get_plugin_dir() - manifest_file = plugin_dir / "plugins.json" - - if not manifest_file.exists(): - return - - with open(manifest_file) as f: - manifest = json.load(f) - - for plugin_info in manifest.get("plugins", []): - if not plugin_info.get("enabled", True): - continue - - plugin_path = plugin_dir / plugin_info["file"] - if not plugin_path.exists(): - continue - - try: - spec = importlib.util.spec_from_file_location( - plugin_info["name"], str(plugin_path) - ) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - - # Look for a click group or command named 'plugin_command' - if hasattr(module, "plugin_command"): - cli_group.add_command(module.plugin_command) - except Exception: - pass # Skip broken plugins silently - - -@click.group() -def plugin(): - """Manage CLI plugins""" - pass - - -@plugin.command(name="list") -@click.pass_context -def list_plugins(ctx): - """List installed plugins""" - from .utils import output - - plugin_dir = get_plugin_dir() - manifest_file = plugin_dir / "plugins.json" - - if not manifest_file.exists(): - output({"message": "No plugins installed"}, ctx.obj.get('output_format', 'table')) - return - - with open(manifest_file) as f: - manifest = json.load(f) - - plugins = manifest.get("plugins", []) - if not plugins: - output({"message": "No plugins installed"}, ctx.obj.get('output_format', 'table')) - else: - output(plugins, ctx.obj.get('output_format', 'table')) - - -@plugin.command() -@click.argument("name") -@click.argument("file_path", type=click.Path(exists=True)) -@click.option("--description", default="", help="Plugin description") -@click.pass_context -def install(ctx, name: str, file_path: str, description: str): - """Install a plugin from a Python file""" - import shutil - from .utils import output, error, success - - plugin_dir = get_plugin_dir() - manifest_file = plugin_dir / "plugins.json" - - # Copy plugin file - dest = plugin_dir / f"{name}.py" - shutil.copy2(file_path, dest) - - # Update manifest - manifest = {"plugins": []} - if manifest_file.exists(): - with open(manifest_file) as f: - manifest = json.load(f) - - # Remove existing entry with same name - manifest["plugins"] = [p for p in manifest["plugins"] if p["name"] != name] - manifest["plugins"].append({ - "name": name, - "file": f"{name}.py", - "description": description, - "enabled": True - }) - - with open(manifest_file, "w") as f: - json.dump(manifest, f, indent=2) - - success(f"Plugin '{name}' installed") - output({"name": name, "file": str(dest), "status": "installed"}, ctx.obj.get('output_format', 'table')) - - -@plugin.command() -@click.argument("name") -@click.pass_context -def uninstall(ctx, name: str): - """Uninstall a plugin""" - from .utils import output, error, success - - plugin_dir = get_plugin_dir() - manifest_file = plugin_dir / "plugins.json" - - if not manifest_file.exists(): - error(f"Plugin '{name}' not found") - return - - with open(manifest_file) as f: - manifest = json.load(f) - - plugin_entry = next((p for p in manifest["plugins"] if p["name"] == name), None) - if not plugin_entry: - error(f"Plugin '{name}' not found") - return - - # Remove file - plugin_file = plugin_dir / plugin_entry["file"] - if plugin_file.exists(): - plugin_file.unlink() - - # Update manifest - manifest["plugins"] = [p for p in manifest["plugins"] if p["name"] != name] - with open(manifest_file, "w") as f: - json.dump(manifest, f, indent=2) - - success(f"Plugin '{name}' uninstalled") - output({"name": name, "status": "uninstalled"}, ctx.obj.get('output_format', 'table')) - - -@plugin.command() -@click.argument("name") -@click.option("--type", default="cli", help="Plugin type (cli, web, blockchain, ai)") -@click.option("--description", default="", help="Plugin description") -@click.option("--author", default="", help="Plugin author") -@click.pass_context -def create(ctx, name: str, type: str, description: str, author: str): - """Create a new plugin skeleton""" - from .utils import output, success - - plugin_dir = get_plugin_dir() - plugin_file = plugin_dir / f"{name}.py" - - if plugin_file.exists(): - from .utils import error - error(f"Plugin '{name}' already exists") - return - - # Create plugin skeleton - template = f'''"""{name} - {description}""" - -import click - -@click.group() -def plugin_command(): - """{name} plugin commands""" - pass - -@plugin_command.command() -def hello(): - """Hello from {name} plugin""" - click.echo("Hello from {name} plugin!") -''' - - with open(plugin_file, "w") as f: - f.write(template) - - # Update manifest - manifest_file = plugin_dir / "plugins.json" - manifest = {"plugins": []} - if manifest_file.exists(): - with open(manifest_file) as f: - manifest = json.load(f) - - manifest["plugins"].append({ - "name": name, - "file": f"{name}.py", - "description": description, - "author": author, - "type": type, - "enabled": True - }) - - with open(manifest_file, "w") as f: - json.dump(manifest, f, indent=2) - - success(f"Plugin '{name}' created") - output({"name": name, "file": str(plugin_file), "type": type}, ctx.obj.get('output_format', 'table')) - - -@plugin.command() -@click.argument("name") -@click.option("--output", default=".", help="Output directory") -@click.pass_context -def package(ctx, name: str, output: str): - """Package a plugin for distribution""" - from .utils import output, success, error - import shutil - from pathlib import Path - import tarfile - - plugin_dir = get_plugin_dir() - manifest_file = plugin_dir / "plugins.json" - - if not manifest_file.exists(): - error(f"Plugin '{name}' not found") - return - - with open(manifest_file) as f: - manifest = json.load(f) - - plugin_entry = next((p for p in manifest["plugins"] if p["name"] == name), None) - if not plugin_entry: - error(f"Plugin '{name}' not found") - return - - plugin_file = plugin_dir / plugin_entry["file"] - if not plugin_file.exists(): - error(f"Plugin file '{plugin_entry['file']}' not found") - return - - # Create package - output_dir = Path(output) - output_dir.mkdir(parents=True, exist_ok=True) - package_file = output_dir / f"{name}.tar.gz" - - with tarfile.open(package_file, "w:gz") as tar: - tar.add(plugin_file, arcname=plugin_file.name) - # Add metadata - metadata = json.dumps({ - "name": name, - "description": plugin_entry.get("description", ""), - "author": plugin_entry.get("author", ""), - "type": plugin_entry.get("type", "cli"), - "version": "1.0.0" - }) - metadata_file = output_dir / "metadata.json" - with open(metadata_file, "w") as f: - f.write(metadata) - tar.add(metadata_file, arcname="metadata.json") - metadata_file.unlink() - - success(f"Plugin '{name}' packaged to {package_file}") - output({"name": name, "package": str(package_file)}, ctx.obj.get('output_format', 'table')) - - -@plugin.command() -@click.argument("name") -@click.argument("state", type=click.Choice(["enable", "disable"])) -@click.pass_context -def toggle(ctx, name: str, state: str): - """Enable or disable a plugin""" - from .utils import output, error, success - - plugin_dir = get_plugin_dir() - manifest_file = plugin_dir / "plugins.json" - - if not manifest_file.exists(): - error(f"Plugin '{name}' not found") - return - - with open(manifest_file) as f: - manifest = json.load(f) - - plugin_entry = next((p for p in manifest["plugins"] if p["name"] == name), None) - if not plugin_entry: - error(f"Plugin '{name}' not found") - return - - plugin_entry["enabled"] = (state == "enable") - - with open(manifest_file, "w") as f: - json.dump(manifest, f, indent=2) - - success(f"Plugin '{name}' {'enabled' if state == 'enable' else 'disabled'}") - output({"name": name, "enabled": plugin_entry["enabled"]}, ctx.obj.get('output_format', 'table')) diff --git a/cli/src/aitbc_cli/handlers/__init__.py b/cli/src/aitbc_cli/handlers/__init__.py deleted file mode 100644 index 87b797cc..00000000 --- a/cli/src/aitbc_cli/handlers/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""CLI command handlers organized by command group.""" diff --git a/cli/src/aitbc_cli/handlers/account.py b/cli/src/aitbc_cli/handlers/account.py deleted file mode 100644 index 9f11c9d1..00000000 --- a/cli/src/aitbc_cli/handlers/account.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Account handlers.""" - -import json -import sys - -from aitbc import AITBCHTTPClient, NetworkError -import logging -logger = logging.getLogger(__name__) - - - -def handle_account_get(args, default_rpc_url, output_format): - """Handle account get command.""" - rpc_url = args.rpc_url or default_rpc_url - chain_id = getattr(args, "chain_id", None) - - if not args.address: - logger.error("Error: --address is required") - sys.exit(1) - - logger.info(f"Getting account {args.address} from {rpc_url}...") - try: - params = {} - if chain_id: - params["chain_id"] = chain_id - - http_client = AITBCHTTPClient(base_url=rpc_url, timeout=10) - account = http_client.get(f"/rpc/account/{args.address}", params=params) - if output_format(args) == "json": - logger.info(json.dumps(account, indent=2)) - else: - render_mapping(f"Account {args.address}:", account) - except NetworkError as e: - logger.error(f"Error getting account: {e}") - sys.exit(1) - except Exception as e: - logger.error(f"Error getting account: {e}") - sys.exit(1) diff --git a/cli/src/aitbc_cli/handlers/ai.py b/cli/src/aitbc_cli/handlers/ai.py deleted file mode 100644 index 0f4f1bfe..00000000 --- a/cli/src/aitbc_cli/handlers/ai.py +++ /dev/null @@ -1,323 +0,0 @@ -"""AI job submission and management handlers.""" - -import json -import sys -import click - -import requests - - -def handle_ai_submit(args, default_rpc_url, default_coordinator_url, first, read_password, render_mapping): - """Handle AI job submission.""" - rpc_url = args.rpc_url or default_rpc_url - chain_id = getattr(args, "chain_id", None) - - wallet = first(getattr(args, "wallet_name", None), getattr(args, "wallet", None)) - model = first(getattr(args, "job_type_arg", None), getattr(args, "job_type", None)) - prompt = first(getattr(args, "prompt_arg", None), getattr(args, "prompt", None)) - payment = first(getattr(args, "payment_arg", None), getattr(args, "payment", None)) - - if not wallet or not model or not prompt: - click.echo("Error: --wallet, --type, and --prompt are required") - sys.exit(1) - - # Get sender address (no password needed for Agent Coordinator) - from pathlib import Path - import json - - # Get sender address - keystore_dir = Path("/var/lib/aitbc/keystore") - sender_keystore = keystore_dir / f"{wallet}.json" - - coordinator_url = getattr(args, 'coordinator_url', default_coordinator_url) or default_coordinator_url - - # Build AI job request - job_data = { - "task_data": { - "model": model or getattr(args, 'model', 'llama2'), - "prompt": prompt or getattr(args, 'prompt', ''), - "parameters": getattr(args, 'parameters', {}) - } - } - - click.echo(f"Submitting AI job to {coordinator_url}...") - try: - response = requests.post(f"{coordinator_url}/tasks/submit", json=job_data, timeout=30) - if response.status_code in (200, 201): - result = response.json() - click.echo("AI job submitted successfully") - render_mapping("Job:", result) - else: - click.echo(f"Job submission failed: {response.status_code}") - click.echo(f"Error: {response.text}") - sys.exit(1) - except Exception as e: - click.echo(f"Error submitting AI job: {e}") - sys.exit(1) - - -def handle_ai_jobs(args, default_rpc_url, default_coordinator_url, output_format, render_mapping): - """Handle AI jobs list query.""" - coordinator_url = args.coordinator_url or default_coordinator_url - chain_id = getattr(args, "chain_id", None) - - click.echo(f"Getting AI jobs from {coordinator_url}...") - try: - params = {} - if chain_id: - params["chain_id"] = chain_id - if args.limit: - params["limit"] = args.limit - - response = requests.get(f"{coordinator_url}/tasks", params=params, timeout=30) - if response.status_code == 200: - jobs = response.json() - if output_format(args) == "json": - click.echo(json.dumps(jobs, indent=2)) - else: - click.echo("AI jobs:") - if isinstance(jobs, list): - for job in jobs: - click.echo(f" Job ID: {job.get('job_id', 'N/A')}, Model: {job.get('model', 'N/A')}, Status: {job.get('status', 'N/A')}") - else: - click.echo(f" {jobs}") - else: - click.echo(f"Query failed: {response.status_code}") - click.echo(f"Error: {response.text}") - # Return stub data instead of failing - stub_jobs = { - "jobs": [ - {"job_id": "job_1", "model": "llama2", "status": "completed"}, - {"job_id": "job_2", "model": "llama2", "status": "running"} - ] - } - render_mapping("AI Jobs (stub):", stub_jobs) - except Exception as e: - click.echo(f"Error querying AI jobs: {e}") - # Return stub data instead of failing - stub_jobs = { - "jobs": [ - {"job_id": "job_1", "model": "llama2", "status": "completed"}, - {"job_id": "job_2", "model": "llama2", "status": "running"} - ] - } - render_mapping("AI Jobs (stub):", stub_jobs) - - -def handle_ai_job(args, default_rpc_url, output_format, render_mapping, first): - """Handle AI job details query.""" - rpc_url = args.rpc_url or default_rpc_url - chain_id = getattr(args, "chain_id", None) - - job_id = first(getattr(args, "job_id_arg", None), getattr(args, "job_id", None)) - - if not job_id: - click.echo("Error: --job-id is required") - sys.exit(1) - - click.echo(f"Getting AI job {job_id} from {rpc_url}...") - try: - params = {} - if chain_id: - params["chain_id"] = chain_id - - response = requests.get(f"{rpc_url}/rpc/ai/job/{job_id}", params=params, timeout=10) - if response.status_code == 200: - job = response.json() - if output_format(args) == "json": - click.echo(json.dumps(job, indent=2)) - else: - render_mapping(f"Job {job_id}:", job) - else: - click.echo(f"Query failed: {response.status_code}") - click.echo(f"Error: {response.text}") - sys.exit(1) - except Exception as e: - click.echo(f"Error getting AI job: {e}") - sys.exit(1) - - -def handle_ai_cancel(args, default_rpc_url, read_password, render_mapping, first): - """Handle AI job cancellation.""" - rpc_url = args.rpc_url or default_rpc_url - chain_id = getattr(args, "chain_id", None) - - job_id = first(getattr(args, "job_id_arg", None), getattr(args, "job_id", None)) - wallet = getattr(args, "wallet", None) - - if not job_id or not wallet: - click.echo("Error: --job-id and --wallet are required") - sys.exit(1) - - # Get auth headers - password = read_password(args) - from keystore_auth import get_auth_headers - headers = get_auth_headers(wallet, password, args.password_file) - - cancel_data = { - "job_id": job_id, - "wallet": wallet, - } - if chain_id: - cancel_data["chain_id"] = chain_id - - click.echo(f"Cancelling AI job {job_id} on {rpc_url}...") - try: - response = requests.post(f"{rpc_url}/rpc/ai/job/{job_id}/cancel", json=cancel_data, headers=headers, timeout=30) - if response.status_code == 200: - result = response.json() - click.echo("AI job cancelled successfully") - render_mapping("Cancel result:", result) - else: - click.echo(f"Cancellation failed: {response.status_code}") - click.echo(f"Error: {response.text}") - sys.exit(1) - except Exception as e: - click.echo(f"Error cancelling AI job: {e}") - sys.exit(1) - - -def handle_ai_stats(args, default_rpc_url, output_format, render_mapping): - """Handle AI service statistics query.""" - rpc_url = args.rpc_url or default_rpc_url - chain_id = getattr(args, "chain_id", None) - - click.echo(f"Getting AI service statistics from {rpc_url}...") - try: - params = {} - if chain_id: - params["chain_id"] = chain_id - - response = requests.get(f"{rpc_url}/rpc/ai/stats", params=params, timeout=10) - if response.status_code == 200: - stats = response.json() - if output_format(args) == "json": - click.echo(json.dumps(stats, indent=2)) - else: - render_mapping("AI service statistics:", stats) - else: - click.echo(f"Query failed: {response.status_code}") - click.echo(f"Error: {response.text}") - sys.exit(1) - except Exception as e: - click.echo(f"Error getting AI stats: {e}") - sys.exit(1) - - -def handle_ai_distribution_stats(args, default_coordinator_url, output_format, render_mapping): - """Handle task distribution statistics query from agent coordinator.""" - coordinator_url = getattr(args, 'coordinator_url', None) or default_coordinator_url - - click.echo(f"Getting task distribution statistics from {coordinator_url}...") - try: - response = requests.get(f"{coordinator_url}/tasks/status", timeout=10) - if response.status_code == 200: - stats = response.json() - if output_format(args) == "json": - click.echo(json.dumps(stats, indent=2)) - else: - render_mapping("Task distribution statistics:", stats) - else: - click.echo(f"Query failed: {response.status_code}") - click.echo(f"Error: {response.text}") - sys.exit(1) - except Exception as e: - click.echo(f"Error getting distribution stats: {e}") - sys.exit(1) - - -def handle_ai_service_list(args, ai_operations, render_mapping): - """Handle AI service list command.""" - result = ai_operations("service_list") - if result: - render_mapping("AI Services:", result) - else: - sys.exit(1) - - -def handle_ai_service_status(args, ai_operations, render_mapping): - """Handle AI service status command.""" - kwargs = {} - if hasattr(args, "name") and args.name: - kwargs["name"] = args.name - result = ai_operations("service_status", **kwargs) - if result: - render_mapping("Service Status:", result) - else: - sys.exit(1) - - -def handle_ai_service_test(args, ai_operations, render_mapping): - """Handle AI service test command.""" - kwargs = {} - if hasattr(args, "name") and args.name: - kwargs["name"] = args.name - result = ai_operations("service_test", **kwargs) - if result: - render_mapping("Service Test:", result) - else: - sys.exit(1) - - -def handle_ai_status(args, default_coordinator_url, default_rpc_url, output_format, render_mapping): - """Handle AI service status check (combined Agent Coordinator and Blockchain AI).""" - coordinator_url = getattr(args, 'coordinator_url', None) or default_coordinator_url - rpc_url = args.rpc_url or default_rpc_url - - combined_status = { - "agent_coordinator": {"status": "unavailable"}, - "blockchain_ai": {"status": "unavailable"}, - "overall": "unavailable" - } - - # Check Agent Coordinator health - click.echo(f"Checking Agent Coordinator at {coordinator_url}...") - try: - response = requests.get(f"{coordinator_url}/health", timeout=10) - if response.status_code == 200: - coordinator_data = response.json() - combined_status["agent_coordinator"] = coordinator_data - click.echo(f" Agent Coordinator: {coordinator_data.get('status', 'unknown')} (v{coordinator_data.get('version', 'N/A')})") - else: - click.echo(f" Agent Coordinator: Failed (HTTP {response.status_code})") - except Exception as e: - click.echo(f" Agent Coordinator: Error - {e}") - - # Check Blockchain AI stats - click.echo(f"Checking Blockchain AI stats at {rpc_url}...") - try: - params = {} - if hasattr(args, "chain_id") and args.chain_id: - params["chain_id"] = args.chain_id - response = requests.get(f"{rpc_url}/rpc/ai/stats", params=params, timeout=10) - if response.status_code == 200: - stats_data = response.json() - combined_status["blockchain_ai"] = stats_data - click.echo(f" Blockchain AI Stats: Available") - else: - click.echo(f" Blockchain AI Stats: Not available (HTTP {response.status_code})") - except Exception as e: - click.echo(f" Blockchain AI Stats: Error - {e}") - - # Calculate overall status - if combined_status["agent_coordinator"].get("status") == "healthy" and combined_status["blockchain_ai"].get("status") != "unavailable": - combined_status["overall"] = "operational" - elif combined_status["agent_coordinator"].get("status") == "healthy" or combined_status["blockchain_ai"].get("status") != "unavailable": - combined_status["overall"] = "partially_operational" - - # Render output - if output_format(args) == "json": - click.echo(json.dumps(combined_status, indent=2)) - else: - click.echo(f"\nOverall Status: {combined_status['overall']}") - if combined_status["agent_coordinator"].get("status") == "healthy": - click.echo(" Agent Coordinator: Operational") - elif combined_status["agent_coordinator"].get("status") != "unavailable": - click.echo(f" Agent Coordinator: {combined_status['agent_coordinator'].get('status')}") - else: - click.echo(" Agent Coordinator: Unavailable") - - if combined_status["blockchain_ai"].get("status") != "unavailable": - click.echo(" Blockchain AI: Operational") - else: - click.echo(" Blockchain AI: Unavailable") diff --git a/cli/src/aitbc_cli/handlers/analytics.py b/cli/src/aitbc_cli/handlers/analytics.py deleted file mode 100644 index 492baf1e..00000000 --- a/cli/src/aitbc_cli/handlers/analytics.py +++ /dev/null @@ -1,91 +0,0 @@ -"""Analytics command handlers for AITBC CLI.""" - -import json -import logging -logger = logging.getLogger(__name__) - - - -def handle_analytics_metrics(args, default_rpc_url, output_format, render_mapping): - """Handle analytics metrics command.""" - period = getattr(args, "period", "24h") - - metrics_data = { - "period": period, - "transactions": 1520, - "tps": 1250, - "avg_latency_ms": 45, - "timestamp": __import__('datetime').datetime.now().isoformat() - } - - if output_format(args) == "json": - logger.info(json.dumps(metrics_data, indent=2)) - else: - render_mapping("Analytics Metrics:", metrics_data) - - -def handle_analytics_report(args, default_rpc_url, output_format, render_mapping): - """Handle analytics report command.""" - report_type = getattr(args, "report_type", "all") - - report_data = { - "type": report_type, - "generated_at": __import__('datetime').datetime.now().isoformat(), - "summary": { - "total_transactions": 1520, - "total_blocks": 45, - "active_nodes": 2 - } - } - - if output_format(args) == "json": - logger.info(json.dumps(report_data, indent=2)) - else: - render_mapping("Analytics Report:", report_data) - - -def handle_analytics_export(args, default_rpc_url, render_mapping): - """Handle analytics export command.""" - format_type = getattr(args, "format", "csv") - - export_data = { - "format": format_type, - "status": "exported", - "file": f"analytics_export_{int(__import__('time').time())}.{format_type}", - "timestamp": __import__('datetime').datetime.now().isoformat() - } - - logger.info(f"Analytics exported as {format_type}") - render_mapping("Export:", export_data) - - -def handle_analytics_predict(args, default_rpc_url, render_mapping): - """Handle analytics predict command.""" - model = getattr(args, "model", "lstm") - target = getattr(args, "target", "job-completion") - - prediction_data = { - "model": model, - "target": target, - "prediction": "85% confidence", - "timestamp": __import__('datetime').datetime.now().isoformat() - } - - logger.info(f"Prediction using {model} model for {target}") - render_mapping("Prediction:", prediction_data) - - -def handle_analytics_optimize(args, default_rpc_url, render_mapping): - """Handle analytics optimize command.""" - parameters = getattr(args, "parameters", False) - target = getattr(args, "target", "efficiency") - - optimization_data = { - "target": target, - "parameters_optimized": parameters, - "improvement": "18%", - "timestamp": __import__('datetime').datetime.now().isoformat() - } - - logger.info(f"Analytics optimization applied for {target}") - render_mapping("Optimization:", optimization_data) diff --git a/cli/src/aitbc_cli/handlers/blockchain.py b/cli/src/aitbc_cli/handlers/blockchain.py deleted file mode 100644 index 954c5295..00000000 --- a/cli/src/aitbc_cli/handlers/blockchain.py +++ /dev/null @@ -1,315 +0,0 @@ -"""Blockchain command handlers.""" - -import json -import os -import sys - -import requests -import logging -logger = logging.getLogger(__name__) - - - -def handle_blockchain_info(args, get_chain_info, render_mapping): - """Handle blockchain info command.""" - chain_info = get_chain_info(rpc_url=args.rpc_url) - if not chain_info: - sys.exit(1) - render_mapping("Blockchain information:", chain_info) - - -def handle_blockchain_height(args, get_chain_info): - """Handle blockchain height command.""" - chain_info = get_chain_info(rpc_url=args.rpc_url) - logger.info(chain_info.get("height", 0) if chain_info else 0) -def handle_blockchain_block(args, default_rpc_url): - """Handle blockchain block command.""" - if args.number is None: - logger.error("Error: block number is required") - sys.exit(1) - - rpc_url = args.rpc_url or os.getenv("NODE_URL", default_rpc_url) - chain_id = getattr(args, 'chain_id', None) or os.getenv('CHAIN_ID', 'ait-mainnet') - logger.info(f"Querying block #{args.number} from {rpc_url} (chain: {chain_id})...") - try: - params = {} - if chain_id: - params['chain_id'] = chain_id - response = requests.get(f"{rpc_url}/rpc/blocks/{args.number}", params=params, timeout=10) - if response.status_code == 200: - data = response.json() - logger.info(f"Block #{args.number}:") - logger.info(f" Hash: {data.get('hash', 'N/A')}") - logger.info(f" Timestamp: {data.get('timestamp', 'N/A')}") - logger.info(f" Transactions: {data.get('tx_count', len(data.get('transactions', [])))}") - logger.info(f" Miner: {data.get('proposer', 'N/A')}") - else: - logger.error(f"Failed to get block: {response.status_code}") - sys.exit(1) - except Exception as e: - logger.error(f"Error getting block: {e}") - sys.exit(1) - - -def handle_blockchain_init(args, default_rpc_url): - """Handle blockchain init command.""" - rpc_url = args.rpc_url or os.getenv("NODE_URL", default_rpc_url) - logger.info(f"Checking blockchain status on {rpc_url}...") - try: - # Check if blockchain is already initialized by checking for genesis block (block 0) - response = requests.get(f"{rpc_url}/rpc/blocks/0", timeout=10) - if response.status_code == 200: - data = response.json() - logger.info("Blockchain already initialized") - logger.info(f"Genesis block hash: {data.get('hash', 'N/A')}") - logger.info(f"Block number: {data.get('number', 0)}") - if args.force: - logger.info("Force flag ignored - blockchain already initialized") - else: - logger.info(f"Blockchain not initialized or endpoint unavailable: {response.status_code}") - sys.exit(1) - except Exception as e: - logger.error(f"Error checking blockchain status: {e}") - logger.info("Note: Blockchain may not be initialized or RPC endpoint unavailable") - sys.exit(1) - - -def handle_blockchain_genesis(args, default_rpc_url): - """Handle blockchain genesis command.""" - rpc_url = args.rpc_url or os.getenv("NODE_URL", default_rpc_url) - - if args.create: - logger.info(f"Creating genesis block on {rpc_url}...") - try: - # Check if genesis block already exists - response = requests.get(f"{rpc_url}/rpc/blocks/0", timeout=10) - if response.status_code == 200: - data = response.json() - logger.info("Genesis block already exists") - logger.info(f"Block hash: {data.get('hash', 'N/A')}") - logger.info(f"Block number: {data.get('number', 0)}") - logger.info(f"Timestamp: {data.get('timestamp', 'N/A')}") - logger.info("Skipping genesis block creation") - return - else: - logger.info(f"Cannot create genesis block - endpoint not available: {response.status_code}") - logger.info("Note: Genesis block creation may not be supported in current RPC implementation") - sys.exit(1) - except Exception as e: - logger.error(f"Error checking genesis block: {e}") - logger.info("Note: Genesis block creation may not be supported in current RPC implementation") - sys.exit(1) - else: - logger.info(f"Inspecting genesis block on {rpc_url}...") - try: - response = requests.get(f"{rpc_url}/rpc/blocks/0", timeout=10) - if response.status_code == 200: - data = response.json() - logger.info("Genesis block information:") - logger.info(f" Hash: {data.get('hash', 'N/A')}") - logger.info(f" Number: {data.get('number', 0)}") - logger.info(f" Timestamp: {data.get('timestamp', 'N/A')}") - logger.info(f" Miner: {data.get('miner', 'N/A')}") - logger.info(f" Reward: {data.get('reward', 'N/A')} AIT") - else: - logger.error(f"Failed to get genesis block: {response.status_code}") - sys.exit(1) - except Exception as e: - logger.error(f"Error inspecting genesis block: {e}") - sys.exit(1) - - -def handle_blockchain_import(args, default_rpc_url, render_mapping): - """Handle blockchain import command.""" - rpc_url = args.rpc_url or default_rpc_url - chain_id = getattr(args, "chain_id", None) - - # Load block data from file or stdin - if args.file: - with open(args.file) as f: - block_data = json.load(f) - elif args.json: - block_data = json.loads(args.json) - else: - logger.error("Error: --file or --json is required") - sys.exit(1) - - # Add chain_id if provided - if chain_id: - block_data["chain_id"] = chain_id - - logger.info(f"Importing block to {rpc_url}...") - try: - response = requests.post(f"{rpc_url}/rpc/importBlock", json=block_data, timeout=30) - if response.status_code == 200: - result = response.json() - logger.info("Block imported successfully") - render_mapping("Import result:", result) - else: - logger.error(f"Import failed: {response.status_code}") - logger.error(f"Error: {response.text}") - sys.exit(1) - except Exception as e: - logger.error(f"Error importing block: {e}") - sys.exit(1) - - -def handle_blockchain_export(args, default_rpc_url): - """Handle blockchain export command.""" - rpc_url = args.rpc_url or default_rpc_url - chain_id = getattr(args, "chain_id", None) - - logger.info(f"Exporting chain from {rpc_url}...") - try: - params = {} - if chain_id: - params["chain_id"] = chain_id - - response = requests.get(f"{rpc_url}/rpc/export-chain", params=params, timeout=60) - if response.status_code == 200: - chain_data = response.json() - if args.output: - with open(args.output, "w") as f: - json.dump(chain_data, f, indent=2) - logger.info(f"Chain exported to {args.output}") - else: - logger.info(json.dumps(chain_data, indent=2)) - else: - logger.error(f"Export failed: {response.status_code}") - logger.error(f"Error: {response.text}") - sys.exit(1) - except Exception as e: - logger.error(f"Error exporting chain: {e}") - sys.exit(1) - - -def handle_blockchain_import_chain(args, default_rpc_url, render_mapping): - """Handle blockchain import chain command.""" - rpc_url = args.rpc_url or default_rpc_url - - if not args.file: - logger.error("Error: --file is required") - sys.exit(1) - - with open(args.file) as f: - chain_data = json.load(f) - - logger.info(f"Importing chain state to {rpc_url}...") - try: - response = requests.post(f"{rpc_url}/rpc/import-chain", json=chain_data, timeout=120) - if response.status_code == 200: - result = response.json() - logger.info("Chain state imported successfully") - render_mapping("Import result:", result) - else: - logger.error(f"Import failed: {response.status_code}") - logger.error(f"Error: {response.text}") - sys.exit(1) - except Exception as e: - logger.error(f"Error importing chain state: {e}") - sys.exit(1) - - -def handle_blockchain_blocks_range(args, default_rpc_url, output_format): - """Handle blockchain blocks range command.""" - rpc_url = args.rpc_url or default_rpc_url - chain_id = getattr(args, "chain_id", None) - - params = {"limit": args.limit} - if args.start: - params["from_height"] = args.start - if args.end: - params["to_height"] = args.end - if chain_id: - params["chain_id"] = chain_id - - logger.info(f"Querying blocks range from {rpc_url}...") - try: - response = requests.get(f"{rpc_url}/rpc/blocks-range", params=params, timeout=30) - if response.status_code == 200: - blocks_data = response.json() - if output_format(args) == "json": - logger.info(json.dumps(blocks_data, indent=2)) - else: - logger.info(f"Blocks range: {args.start or 'head'} to {args.end or 'limit ' + str(args.limit)}") - if isinstance(blocks_data, list): - for block in blocks_data: - logger.info(f" - Block #{block.get('height', 'N/A')}: {block.get('hash', 'N/A')}") - else: - logger.info(json.dumps(blocks_data, indent=2)) - else: - logger.error(f"Query failed: {response.status_code}") - logger.error(f"Error: {response.text}") - sys.exit(1) - except Exception as e: - logger.error(f"Error querying blocks range: {e}") - sys.exit(1) - - -def handle_blockchain_transactions(args, default_rpc_url): - """Handle blockchain transactions command.""" - rpc_url = args.rpc_url or default_rpc_url - chain_id = getattr(args, "chain_id", None) - - logger.info(f"Querying transactions from {rpc_url}...") - try: - params = {} - if args.address: - params["address"] = args.address - if chain_id: - params["chain_id"] = chain_id - if args.limit: - params["limit"] = args.limit - if args.offset: - params["offset"] = args.offset - - response = requests.get(f"{rpc_url}/rpc/transactions", params=params, timeout=10) - if response.status_code == 200: - transactions = response.json() - if isinstance(transactions, list): - logger.info(f"Transactions: {len(transactions)} found") - for tx in transactions[:args.limit]: - logger.info(f" - Hash: {tx.get('hash', 'N/A')}") - logger.info(f" From: {tx.get('from', 'N/A')}") - logger.info(f" To: {tx.get('to', 'N/A')}") - logger.info(f" Amount: {tx.get('value', 0)} AIT") - else: - logger.info(json.dumps(transactions, indent=2)) - else: - logger.error(f"Query failed: {response.status_code}") - logger.error(f"Error: {response.text}") - sys.exit(1) - except Exception as e: - logger.error(f"Error querying transactions: {e}") - sys.exit(1) - - -def handle_blockchain_mempool(args, default_rpc_url): - """Handle blockchain mempool command.""" - rpc_url = args.rpc_url or default_rpc_url - chain_id = getattr(args, "chain_id", None) - - logger.info(f"Getting pending transactions from {rpc_url}...") - try: - params = {} - if chain_id: - params["chain_id"] = chain_id - - response = requests.get(f"{rpc_url}/rpc/mempool", params=params, timeout=10) - if response.status_code == 200: - mempool = response.json() - if isinstance(mempool, list): - logger.info(f"Pending transactions: {len(mempool)}") - for tx in mempool: - logger.info(f" - Hash: {tx.get('hash', 'N/A')}") - logger.info(f" From: {tx.get('from', 'N/A')}") - logger.info(f" Amount: {tx.get('value', 0)} AIT") - else: - logger.info(json.dumps(mempool, indent=2)) - else: - logger.error(f"Query failed: {response.status_code}") - logger.error(f"Error: {response.text}") - sys.exit(1) - except Exception as e: - logger.error(f"Error getting mempool: {e}") - sys.exit(1) diff --git a/cli/src/aitbc_cli/handlers/bridge.py b/cli/src/aitbc_cli/handlers/bridge.py deleted file mode 100644 index c64e9970..00000000 --- a/cli/src/aitbc_cli/handlers/bridge.py +++ /dev/null @@ -1,127 +0,0 @@ -"""Blockchain event bridge handlers.""" - -import subprocess - -from aitbc import AITBCHTTPClient, NetworkError -import logging -logger = logging.getLogger(__name__) - - - -def handle_bridge_health(args): - """Health check for blockchain event bridge service.""" - try: - from commands.legacy.blockchain_event_bridge import get_config as get_bridge_config - config = get_bridge_config() - - if args.test_mode: - logger.info("šŸ„ Blockchain Event Bridge Health (test mode):") - logger.info("āœ… Status: healthy") - logger.info("šŸ“¦ Service: blockchain-event-bridge") - return - - bridge_url = getattr(config, "bridge_url", "http://localhost:8204") - http_client = AITBCHTTPClient(base_url=bridge_url, timeout=10) - health = http_client.get("/health") - - logger.info("šŸ„ Blockchain Event Bridge Health:") - for key, value in health.items(): - logger.info(f" {key}: {value}") - except NetworkError as e: - logger.error(f"āŒ Health check failed: {e}") - except Exception as e: - logger.error(f"āŒ Error checking health: {e}") -def handle_bridge_metrics(args): - """Get Prometheus metrics from blockchain event bridge service.""" - try: - from commands.legacy.blockchain_event_bridge import get_config as get_bridge_config - config = get_bridge_config() - - if args.test_mode: - logger.info("šŸ“Š Prometheus Metrics (test mode):") - logger.info(" bridge_events_total: 103691") - logger.info(" bridge_events_processed_total: 103691") - return - - bridge_url = getattr(config, "bridge_url", "http://localhost:8204") - http_client = AITBCHTTPClient(base_url=bridge_url, timeout=10) - metrics = http_client.get("/metrics", return_response=True) - - logger.info("šŸ“Š Prometheus Metrics:") - logger.info(metrics.text) - except NetworkError as e: - logger.error(f"āŒ Failed to get metrics: {e}") - except Exception as e: - logger.error(f"āŒ Error getting metrics: {e}") -def handle_bridge_status(args): - """Get detailed status of blockchain event bridge service.""" - try: - from commands.legacy.blockchain_event_bridge import get_config as get_bridge_config - config = get_bridge_config() - - if args.test_mode: - logger.info("šŸ“Š Blockchain Event Bridge Status (test mode):") - logger.info("āœ… Status: running") - logger.info("šŸ”” Subscriptions: blocks, transactions, contract_events") - return - - bridge_url = getattr(config, "bridge_url", "http://localhost:8204") - http_client = AITBCHTTPClient(base_url=bridge_url, timeout=10) - status = http_client.get("/") - - logger.info("šŸ“Š Blockchain Event Bridge Status:") - for key, value in status.items(): - logger.info(f" {key}: {value}") - except NetworkError as e: - logger.error(f"āŒ Failed to get status: {e}") - except Exception as e: - logger.error(f"āŒ Error getting status: {e}") -def handle_bridge_config(args): - """Show current configuration of blockchain event bridge service.""" - try: - from commands.legacy.blockchain_event_bridge import get_config as get_bridge_config - config = get_bridge_config() - - if args.test_mode: - logger.info("āš™ļø Blockchain Event Bridge Configuration (test mode):") - logger.info("šŸ”— Blockchain RPC URL: http://localhost:8006") - logger.info("šŸ’¬ Gossip Backend: redis") - return - - bridge_url = getattr(config, "bridge_url", "http://localhost:8204") - http_client = AITBCHTTPClient(base_url=bridge_url, timeout=10) - service_config = http_client.get("/config") - - logger.info("āš™ļø Blockchain Event Bridge Configuration:") - for key, value in service_config.items(): - logger.info(f" {key}: {value}") - except NetworkError as e: - logger.error(f"āŒ Failed to get config: {e}") - except Exception as e: - logger.error(f"āŒ Error getting config: {e}") -def handle_bridge_restart(args): - """Restart blockchain event bridge service (via systemd).""" - try: - if args.test_mode: - logger.info("šŸ”„ Blockchain event bridge restart triggered (test mode)") - logger.info("āœ… Restart completed successfully") - return - - result = subprocess.run( - ["sudo", "systemctl", "restart", "aitbc-blockchain-event-bridge"], - capture_output=True, - text=True, - timeout=30 - ) - - if result.returncode == 0: - logger.info("šŸ”„ Blockchain event bridge restart triggered") - logger.info("āœ… Restart completed successfully") - else: - logger.error(f"āŒ Restart failed: {result.stderr}") - except subprocess.TimeoutExpired: - logger.info("āŒ Restart timeout - service may be starting") - except FileNotFoundError: - logger.info("āŒ systemctl not found - cannot restart service") - except Exception as e: - logger.error(f"āŒ Error restarting service: {e}") \ No newline at end of file diff --git a/cli/src/aitbc_cli/handlers/contract.py b/cli/src/aitbc_cli/handlers/contract.py deleted file mode 100644 index a3595e0f..00000000 --- a/cli/src/aitbc_cli/handlers/contract.py +++ /dev/null @@ -1,172 +0,0 @@ -"""Contract command handlers for AITBC CLI""" - -import requests -from typing import Optional, Dict, Any -import logging -logger = logging.getLogger(__name__) - - - -def handle_contract_list(args, default_rpc_url: str): - """Handle contract list command""" - rpc_url = args.rpc_url if hasattr(args, 'rpc_url') and args.rpc_url else default_rpc_url - - try: - response = requests.get(f"{rpc_url}/rpc/contracts", timeout=30) - if response.status_code == 200: - data = response.json() - # Handle both response formats: with or without "success" field - if data.get("success") is not False: - contracts = data.get("contracts", []) - if contracts: - logger.info(f"Deployed contracts ({len(contracts)}):") - for contract in contracts: - logger.info(f" - Address: {contract.get('address', 'N/A')}") - logger.info(f" Type: {contract.get('type', 'N/A')}") - logger.info(f" Deployed: {contract.get('deployed_at', 'N/A')}") - else: - logger.info("No contracts deployed") - else: - logger.error(f"Error: {data.get('error', 'Unknown error')}") - else: - logger.error(f"Error: RPC returned {response.status_code}") - except Exception as e: - logger.error(f"Error listing contracts: {e}") -def handle_contract_deploy(args, default_rpc_url: str, read_password, render_mapping): - """Handle contract deploy command""" - rpc_url = args.rpc_url if hasattr(args, 'rpc_url') and args.rpc_url else default_rpc_url - contract_name = getattr(args, 'name', None) - contract_type = getattr(args, 'type', 'zk-verifier') - - if not contract_name: - logger.error("Error: Contract name is required (--name)") - return - - password = read_password(args) - if not password: - logger.error("Error: Wallet password is required (--password or --password-file)") - return - - try: - payload = { - "name": contract_name, - "type": contract_type - } - - response = requests.post( - f"{rpc_url}/rpc/contracts/deploy", - json=payload, - headers={"X-Wallet-Password": password}, - timeout=60 - ) - - if response.status_code == 200: - data = response.json() - if data.get("success"): - render_mapping("Contract deployed successfully", data) - else: - logger.error(f"Error: {data.get('error', 'Unknown error')}") - else: - logger.error(f"Error: RPC returned {response.status_code}") - except Exception as e: - logger.error(f"Error deploying contract: {e}") -def handle_contract_call(args, default_rpc_url: str, read_password): - """Handle contract call command""" - rpc_url = args.rpc_url if hasattr(args, 'rpc_url') and args.rpc_url else default_rpc_url - contract_address = getattr(args, 'address', None) - method = getattr(args, 'method', None) - - if not contract_address: - logger.error("Error: Contract address is required (--address)") - return - - if not method: - logger.error("Error: Method name is required (--method)") - return - - password = read_password(args) - - try: - payload = { - "address": contract_address, - "method": method - } - - # Add optional parameters - if hasattr(args, 'params') and args.params: - payload["params"] = args.params - - headers = {} - if password: - headers["X-Wallet-Password"] = password - - response = requests.post( - f"{rpc_url}/rpc/contracts/call", - json=payload, - headers=headers, - timeout=60 - ) - - if response.status_code == 200: - data = response.json() - if data.get("success"): - result = data.get("result") - logger.info(f"Contract call result:") - logger.info(f" Address: {contract_address}") - logger.info(f" Method: {method}") - logger.info(f" Result: {result}") - else: - logger.error(f"Error: {data.get('error', 'Unknown error')}") - else: - logger.error(f"Error: RPC returned {response.status_code}") - except Exception as e: - logger.error(f"Error calling contract: {e}") -def handle_contract_verify(args, default_rpc_url: str, read_password): - """Handle contract verify command (for ZK proofs)""" - rpc_url = args.rpc_url if hasattr(args, 'rpc_url') and args.rpc_url else default_rpc_url - contract_address = getattr(args, 'address', None) - - if not contract_address: - logger.error("Error: Contract address is required (--address)") - return - - password = read_password(args) - - try: - payload = { - "address": contract_address - } - - # Add proof data if available - if hasattr(args, 'proof_file') and args.proof_file: - import json - with open(args.proof_file) as f: - proof_data = json.load(f) - payload["proof"] = proof_data - - headers = {} - if password: - headers["X-Wallet-Password"] = password - - response = requests.post( - f"{rpc_url}/rpc/contracts/verify", - json=payload, - headers=headers, - timeout=60 - ) - - if response.status_code == 200: - data = response.json() - if data.get("success"): - result = data.get("result") - logger.info(f"Verification result:") - logger.info(f" Address: {contract_address}") - logger.info(f" Valid: {result.get('valid', False)}") - if result.get('receipt_hash'): - logger.info(f" Receipt Hash: {result.get('receipt_hash')}") - else: - logger.error(f"Error: {data.get('error', 'Unknown error')}") - else: - logger.error(f"Error: RPC returned {response.status_code}") - except Exception as e: - logger.error(f"Error verifying contract: {e}") \ No newline at end of file diff --git a/cli/src/aitbc_cli/handlers/market.py b/cli/src/aitbc_cli/handlers/market.py deleted file mode 100644 index f4fcc04d..00000000 --- a/cli/src/aitbc_cli/handlers/market.py +++ /dev/null @@ -1,419 +0,0 @@ -"""Marketplace command handlers.""" - -import json -import os -import sys -import requests -import logging -logger = logging.getLogger(__name__) - - - -def _marketplace_url(args, fallback=None): - explicit_url = getattr(args, "marketplace_url", None) - if explicit_url: - return explicit_url - env_url = os.getenv("AITBC_MARKETPLACE_URL") or os.getenv("EXCHANGE_API_URL") - if env_url: - return env_url - if fallback and not fallback.endswith(":8011") and not fallback.endswith(":8102"): - return fallback - return "http://localhost:8001" - - -def _auth_headers(args, read_password): - wallet = getattr(args, "wallet", None) - password = read_password(args) - password_file = getattr(args, "password_file", None) - if wallet and (password or password_file): - try: - from keystore_auth import get_auth_headers - return get_auth_headers(wallet, password, password_file) - except Exception: - return {"X-Wallet": wallet} - if wallet: - return {"X-Wallet": wallet} - return {} - - -def handle_market_listings(args, default_coordinator_url, output_format, render_mapping): - """Handle marketplace listings command.""" - marketplace_url = _marketplace_url(args, default_coordinator_url) - chain_id = getattr(args, "chain_id", None) - - logger.info(f"Getting marketplace listings from {marketplace_url}...") - try: - params = {} - if chain_id: - params["chain_id"] = chain_id - - response = requests.get(f"{marketplace_url}/v1/marketplace/offers", params=params, timeout=10) - if response.status_code == 200: - listings = response.json() - if output_format(args) == "json": - logger.info(json.dumps(listings, indent=2)) - else: - logger.info("Marketplace listings:") - if isinstance(listings, list): - if listings: - for listing in listings: - logger.info(f" - ID: {listing.get('id', 'N/A')}") - logger.info(f" Model: {listing.get('model', 'N/A')}") - logger.info(f" Price: {listing.get('price_per_hour', 0)} AIT/hour") - logger.info(f" Status: {listing.get('status', 'N/A')}") - else: - logger.info(" No GPU listings found") - else: - render_mapping("Listings:", listings) - else: - logger.error(f"Query failed: {response.status_code}") - logger.error(f"Error: {response.text}") - return - except Exception as e: - logger.error(f"Error getting listings: {e}") - return - - -def handle_market_create(args, default_coordinator_url, read_password, render_mapping): - """Handle marketplace create command.""" - marketplace_url = _marketplace_url(args, default_coordinator_url) - chain_id = getattr(args, "chain_id", None) - wallet = getattr(args, "wallet", None) - item = getattr(args, "item", None) - item_type = getattr(args, "item_type", None) or item or "service" - price = getattr(args, "price", None) - - if not wallet or price is None: - logger.error("Error: --wallet and --price are required") - return - - headers = _auth_headers(args, read_password) - - listing_data = { - "wallet": wallet, - "item": item or item_type, - "item_type": item_type, - "price": price, - "description": getattr(args, "description", ""), - } - if chain_id: - listing_data["chain_id"] = chain_id - - logger.info(f"Creating marketplace listing on {marketplace_url}...") - try: - response = requests.post(f"{marketplace_url}/v1/marketplace/offers", json=listing_data, headers=headers, timeout=30) - if response.status_code in (200, 201): - result = response.json() - logger.info("Listing created successfully") - render_mapping("Listing:", result) - else: - logger.error(f"Creation failed: {response.status_code}") - logger.error(f"Error: {response.text}") - return - except Exception as e: - logger.error(f"Error creating listing: {e}") - return - - -def handle_market_get(args, default_rpc_url): - """Handle marketplace get command.""" - marketplace_url = _marketplace_url(args, default_rpc_url) - chain_id = getattr(args, "chain_id", None) - - if not args.listing_id: - logger.error("Error: --listing-id is required") - return - - logger.info(f"Getting listing {args.listing_id} from {marketplace_url}...") - try: - import requests - response = requests.get(f"{marketplace_url}/v1/marketplace/offers/{args.listing_id}", timeout=10) - if response.status_code == 200: - listing = response.json() - logger.info(json.dumps(listing, indent=2)) - else: - logger.error(f"Query failed: {response.status_code}") - logger.error(f"Error: {response.text}") - return - except Exception as e: - logger.error(f"Error getting listing: {e}") - return - - -def handle_market_delete(args, default_coordinator_url, read_password, render_mapping): - """Handle marketplace delete command.""" - marketplace_url = _marketplace_url(args, default_coordinator_url) - listing_id = getattr(args, "listing_id", None) or getattr(args, "order", None) - - if not listing_id: - logger.error("Error: --listing-id or --order is required") - return - - headers = _auth_headers(args, read_password) - endpoint_type = "orders" if str(listing_id).startswith("order_") else "offers" - - logger.info(f"Deleting {endpoint_type[:-1]} {listing_id} on {marketplace_url}...") - try: - response = requests.delete(f"{marketplace_url}/v1/marketplace/{endpoint_type}/{listing_id}", headers=headers, timeout=30) - if response.status_code == 200: - result = response.json() - logger.info("Marketplace item deleted successfully") - render_mapping("Delete result:", result) - else: - logger.error(f"Deletion failed: {response.status_code}") - logger.error(f"Error: {response.text}") - return - except Exception as e: - logger.error(f"Error deleting listing: {e}") - return - - -def handle_market_gpu_register(args, default_coordinator_url): - """Handle GPU registration command with nvidia-smi auto-detection.""" - # Use GPU service URL instead of coordinator URL - gpu_url = getattr(args, 'gpu_url', 'http://localhost:8101') - - # Auto-detect GPU specs from nvidia-smi - gpu_name = args.name - memory_gb = args.memory - compute_capability = getattr(args, "compute_capability", None) - - if not gpu_name or memory_gb is None: - logger.info("Auto-detecting GPU specifications from nvidia-smi...") - try: - import subprocess - result = subprocess.run( - ["nvidia-smi", "--query-gpu=name,memory.total,compute_cap", "--format=csv,noheader"], - capture_output=True, - text=True, - timeout=10 - ) - if result.returncode == 0: - # Parse output: "NVIDIA GeForce RTX 4060 Ti, 16380 MiB, 8.9" - parts = result.stdout.strip().split(", ") - if len(parts) >= 3: - detected_name = parts[0] - detected_memory = parts[1].strip() # "16380 MiB" - detected_compute = parts[2].strip() # "8.9" - - # Convert memory to GB - memory_value = int(detected_memory.split()[0]) # 16380 - memory_gb_detected = round(memory_value / 1024, 1) # 16.0 - - if not gpu_name: - gpu_name = detected_name - logger.info(f" Detected GPU: {gpu_name}") - if memory_gb is None: - memory_gb = memory_gb_detected - logger.info(f" Detected Memory: {memory_gb} GB") - if not compute_capability: - compute_capability = detected_compute - logger.info(f" Detected Compute Capability: {compute_capability}") - else: - logger.error(" Warning: nvidia-smi failed, using manual input or defaults") - except (subprocess.TimeoutExpired, FileNotFoundError, Exception) as e: - logger.warning(f" Warning: Could not run nvidia-smi: {e}") - # Fallback to manual input if auto-detection failed - if not gpu_name or memory_gb is None: - logger.error("Error: Could not auto-detect GPU specs. Please provide --name and --memory manually.") - logger.info(" Example: aitbc-cli market gpu register --name 'NVIDIA GeForce RTX 4060 Ti' --memory 16 --price-per-hour 0.05") - return - - if not args.price_per_hour: - logger.error("Error: --price-per-hour is required (in AIT coins)") - return - - # Build GPU specs - gpu_specs = { - "name": gpu_name, - "memory_gb": memory_gb, - "cuda_cores": getattr(args, "cuda_cores", None), - "compute_capability": compute_capability, - "price_per_hour": args.price_per_hour, - "description": getattr(args, "description", ""), - "miner_id": getattr(args, "miner_id", "default_miner"), - "registered_at": __import__("datetime").datetime.now().isoformat() - } - - logger.info(f"Registering GPU on {gpu_url}...") - try: - response = requests.post( - f"{gpu_url}/v1/marketplace/gpu/register", - headers={ - "Content-Type": "application/json", - "X-Miner-ID": gpu_specs["miner_id"] - }, - json={"gpu": gpu_specs}, - timeout=30 - ) - if response.status_code in (200, 201): - result = response.json() - logger.info(f"GPU registered successfully: {result.get('gpu_id', 'N/A')}") - from ..utils import render_mapping - render_mapping("Registration result:", result) - else: - logger.error(f"Registration failed: {response.status_code}") - logger.error(f"Error: {response.text}") - return - except Exception as e: - logger.error(f"Error registering GPU: {e}") - return - - -def handle_market_gpu_list(args, default_coordinator_url, output_format): - """Handle GPU list command.""" - # Use GPU service URL instead of coordinator URL - gpu_url = getattr(args, 'gpu_url', 'http://localhost:8101') - - logger.info(f"Listing GPUs from {gpu_url}...") - try: - params = { - "action": "offer", - "status": "active" - } - if getattr(args, "available", None): - params["status"] = "active" - if getattr(args, "price_max", None): - params["price_max"] = args.price_max - if getattr(args, "region", None): - params["region"] = args.region - if getattr(args, "model", None): - params["model"] = args.model - if getattr(args, "limit", None): - params["limit"] = args.limit - - response = requests.get(f"{gpu_url}/v1/transactions", params=params, timeout=10) - if response.status_code == 200: - gpus = response.json() - if output_format(args) == "json": - logger.info(json.dumps(gpus, indent=2)) - else: - logger.info("GPU Listings:") - if isinstance(gpus, list): - if gpus: - for gpu in gpus: - if isinstance(gpu, dict): - logger.info(f" - ID: {gpu.get('id', 'N/A')}") - logger.info(f" Model: {gpu.get('model', 'N/A')}") - logger.info(f" Memory: {gpu.get('memory_gb', 'N/A')} GB") - logger.info(f" Price: {gpu.get('price_per_hour', 0)} AIT/hour") - logger.info(f" Status: {gpu.get('status', 'N/A')}") - logger.info(f" Region: {gpu.get('region', 'N/A')}") - else: - logger.info(" No GPUs found") - else: - from ..utils import render_mapping - render_mapping("GPUs:", gpus) - else: - logger.error(f"Query failed: {response.status_code}") - logger.error(f"Error: {response.text}") - return - except Exception as e: - logger.error(f"Error listing GPUs: {e}") - return - - -def handle_market_buy(args, default_coordinator_url, read_password, render_mapping): - """Handle marketplace buy command via marketplace service.""" - marketplace_url = _marketplace_url(args, default_coordinator_url) - - if not args.item or not args.wallet: - logger.error("Error: --item and --wallet are required") - return - - purchase_data = { - "duration_hours": 1.0, - "wallet": args.wallet, - "price": getattr(args, "price", None) - } - - logger.info(f"Submitting purchase to {marketplace_url}...") - try: - response = requests.post(f"{marketplace_url}/v1/marketplace/offers/{args.item}/book", json=purchase_data, headers=_auth_headers(args, read_password), timeout=30) - if response.status_code in (200, 201): - result = response.json() - logger.info("Purchase submitted successfully") - render_mapping("Purchase:", result) - else: - logger.error(f"Purchase failed: {response.status_code}") - logger.error(f"Error: {response.text}") - return - except Exception as e: - logger.error(f"Error submitting purchase: {e}") - return - - -def handle_market_sell(args, default_coordinator_url, read_password, render_mapping): - """Handle marketplace sell command.""" - handle_market_create(args, default_coordinator_url, read_password, render_mapping) - - -def handle_market_orders(args, default_coordinator_url, output_format, render_mapping): - """Handle marketplace orders command.""" - marketplace_url = _marketplace_url(args, default_coordinator_url) - params = {} - wallet = getattr(args, "wallet", None) - if wallet: - params["wallet"] = wallet - - logger.info(f"Getting marketplace orders from {marketplace_url}...") - try: - response = requests.get(f"{marketplace_url}/v1/marketplace/orders", params=params, timeout=10) - if response.status_code == 200: - orders = response.json() - if output_format(args) == "json": - logger.info(json.dumps(orders, indent=2)) - return - if isinstance(orders, dict): - orders = orders.get("orders", []) - logger.info("Active marketplace orders:") - if not orders: - logger.info(" No active orders found") - return - for order in orders: - logger.info(f" - ID: {order.get('id', 'N/A')}") - logger.info(f" Type: {order.get('order_type', 'N/A')}") - logger.info(f" Item: {order.get('item', 'N/A')}") - logger.info(f" Price: {order.get('price', 0)} AIT") - logger.info(f" Status: {order.get('status', 'N/A')}") - else: - logger.error(f"Query failed: {response.status_code}") - logger.error(f"Error: {response.text}") - return - except Exception as e: - logger.error(f"Error getting orders: {e}") - return - - -def handle_market_list_plugins(args, default_coordinator_url, output_format, render_mapping): - """Handle marketplace plugin listing command.""" - marketplace_url = _marketplace_url(args, default_coordinator_url) - - logger.info(f"Getting marketplace plugins from {marketplace_url}...") - try: - response = requests.get(f"{marketplace_url}/v1/marketplace/plugins", timeout=10) - if response.status_code == 200: - plugins = response.json() - if output_format(args) == "json": - logger.info(json.dumps(plugins, indent=2)) - return - if isinstance(plugins, dict): - plugins = plugins.get("plugins", []) - logger.info("Available marketplace plugins:") - if not plugins: - logger.info(" No plugins found") - return - for plugin in plugins: - logger.info(f" - ID: {plugin.get('id', 'N/A')}") - logger.info(f" Name: {plugin.get('name', 'N/A')}") - logger.info(f" Type: {plugin.get('type', 'N/A')}") - logger.info(f" Author: {plugin.get('author', 'N/A')}") - logger.info(f" Description: {plugin.get('description', 'N/A')}") - logger.info(f" Version: {plugin.get('version', 'N/A')}") - else: - logger.error(f"Query failed: {response.status_code}") - logger.error(f"Error: {response.text}") - return - except Exception as e: - logger.error(f"Error getting plugins: {e}") - return diff --git a/cli/src/aitbc_cli/handlers/messaging.py b/cli/src/aitbc_cli/handlers/messaging.py deleted file mode 100644 index 574eccca..00000000 --- a/cli/src/aitbc_cli/handlers/messaging.py +++ /dev/null @@ -1,352 +0,0 @@ -"""Messaging contract handlers.""" - -import json -import sys - -import requests -import logging -logger = logging.getLogger(__name__) - - - -def handle_messaging_deploy(args, default_rpc_url, render_mapping): - """Handle messaging contract deployment.""" - rpc_url = args.rpc_url or default_rpc_url - chain_id = getattr(args, "chain_id", None) - - logger.info(f"Deploying messaging contract to {rpc_url}...") - try: - params = {} - if chain_id: - params["chain_id"] = chain_id - - response = requests.post(f"{rpc_url}/rpc/contracts/deploy/messaging", json={}, params=params, timeout=30) - if response.status_code == 200: - result = response.json() - logger.info("Messaging contract deployed successfully") - render_mapping("Deployment result:", result) - else: - logger.error(f"Deployment failed: {response.status_code}") - logger.error(f"Error: {response.text}") - sys.exit(1) - except Exception as e: - logger.error(f"Error deploying messaging contract: {e}") - sys.exit(1) - - -def handle_messaging_state(args, default_rpc_url, output_format, render_mapping): - """Handle messaging contract state query.""" - rpc_url = args.rpc_url or default_rpc_url - chain_id = getattr(args, "chain_id", None) - - logger.info(f"Getting messaging contract state from {rpc_url}...") - try: - params = {} - if chain_id: - params["chain_id"] = chain_id - - response = requests.get(f"{rpc_url}/rpc/contracts/messaging/state", params=params, timeout=10) - if response.status_code == 200: - state = response.json() - if output_format(args) == "json": - logger.info(json.dumps(state, indent=2)) - else: - render_mapping("Messaging contract state:", state) - else: - logger.error(f"Query failed: {response.status_code}") - logger.error(f"Error: {response.text}") - sys.exit(1) - except Exception as e: - logger.error(f"Error getting contract state: {e}") - sys.exit(1) - - -def handle_messaging_topics(args, default_rpc_url, output_format, render_mapping): - """Handle forum topics query.""" - rpc_url = args.rpc_url or default_rpc_url - chain_id = getattr(args, "chain_id", None) - - logger.info(f"Getting forum topics from {rpc_url}...") - try: - params = {} - if chain_id: - params["chain_id"] = chain_id - - response = requests.get(f"{rpc_url}/rpc/messaging/topics", params=params, timeout=10) - if response.status_code == 200: - topics = response.json() - if output_format(args) == "json": - logger.info(json.dumps(topics, indent=2)) - else: - logger.info("Forum topics:") - if isinstance(topics, list): - for topic in topics: - logger.info(f" ID: {topic.get('topic_id', 'N/A')}, Title: {topic.get('title', 'N/A')}") - else: - render_mapping("Topics:", topics) - else: - logger.error(f"Query failed: {response.status_code}") - logger.error(f"Error: {response.text}") - sys.exit(1) - except Exception as e: - logger.error(f"Error getting topics: {e}") - sys.exit(1) - - -def handle_messaging_create_topic(args, default_rpc_url, read_password, render_mapping): - """Handle forum topic creation.""" - rpc_url = args.rpc_url or default_rpc_url - chain_id = getattr(args, "chain_id", None) - - if not args.title or not args.content: - logger.error("Error: --title and --content are required") - sys.exit(1) - - # Get auth headers if wallet provided - headers = {} - if args.wallet: - password = read_password(args) - from keystore_auth import get_auth_headers - headers = get_auth_headers(args.wallet, password, args.password_file) - - topic_data = { - "title": args.title, - "content": args.content, - } - if chain_id: - topic_data["chain_id"] = chain_id - - logger.info(f"Creating forum topic on {rpc_url}...") - try: - response = requests.post(f"{rpc_url}/rpc/messaging/topics/create", json=topic_data, headers=headers, timeout=30) - if response.status_code == 200: - result = response.json() - logger.info("Topic created successfully") - render_mapping("Topic:", result) - else: - logger.error(f"Creation failed: {response.status_code}") - logger.error(f"Error: {response.text}") - sys.exit(1) - except Exception as e: - logger.error(f"Error creating topic: {e}") - sys.exit(1) - - -def handle_messaging_messages(args, default_rpc_url, output_format, render_mapping): - """Handle messages query for a topic.""" - rpc_url = args.rpc_url or default_rpc_url - chain_id = getattr(args, "chain_id", None) - - if not args.topic_id: - logger.error("Error: --topic-id is required") - sys.exit(1) - - logger.info(f"Getting messages for topic {args.topic_id} from {rpc_url}...") - try: - params = {"topic_id": args.topic_id} - if chain_id: - params["chain_id"] = chain_id - - response = requests.get(f"{rpc_url}/rpc/messaging/topics/{args.topic_id}/messages", params=params, timeout=10) - if response.status_code == 200: - messages = response.json() - if output_format(args) == "json": - logger.info(json.dumps(messages, indent=2)) - else: - logger.info(f"Messages for topic {args.topic_id}:") - if isinstance(messages, list): - for msg in messages: - logger.info(f" Message ID: {msg.get('message_id', 'N/A')}, Author: {msg.get('author', 'N/A')}") - else: - render_mapping("Messages:", messages) - else: - logger.error(f"Query failed: {response.status_code}") - logger.error(f"Error: {response.text}") - sys.exit(1) - except Exception as e: - logger.error(f"Error getting messages: {e}") - sys.exit(1) - - -def handle_messaging_post(args, default_rpc_url, read_password, render_mapping): - """Handle message posting to a topic.""" - rpc_url = args.rpc_url or default_rpc_url - chain_id = getattr(args, "chain_id", None) - - if not args.topic_id or not args.content: - logger.error("Error: --topic-id and --content are required") - sys.exit(1) - - # Get auth headers if wallet provided - headers = {} - if args.wallet: - password = read_password(args) - from keystore_auth import get_auth_headers - headers = get_auth_headers(args.wallet, password, args.password_file) - - message_data = { - "topic_id": args.topic_id, - "content": args.content, - } - if chain_id: - message_data["chain_id"] = chain_id - - logger.info(f"Posting message to topic {args.topic_id} on {rpc_url}...") - try: - response = requests.post(f"{rpc_url}/rpc/messaging/messages/post", json=message_data, headers=headers, timeout=30) - if response.status_code == 200: - result = response.json() - logger.info("Message posted successfully") - render_mapping("Message:", result) - else: - logger.error(f"Post failed: {response.status_code}") - logger.error(f"Error: {response.text}") - sys.exit(1) - except Exception as e: - logger.error(f"Error posting message: {e}") - sys.exit(1) - - -def handle_messaging_vote(args, default_rpc_url, read_password, render_mapping): - """Handle voting on a message.""" - rpc_url = args.rpc_url or default_rpc_url - chain_id = getattr(args, "chain_id", None) - - if not args.message_id or not args.vote: - logger.error("Error: --message-id and --vote are required") - sys.exit(1) - - # Get auth headers if wallet provided - headers = {} - if args.wallet: - password = read_password(args) - from keystore_auth import get_auth_headers - headers = get_auth_headers(args.wallet, password, args.password_file) - - vote_data = { - "message_id": args.message_id, - "vote": args.vote, - } - if chain_id: - vote_data["chain_id"] = chain_id - - logger.info(f"Voting on message {args.message_id} on {rpc_url}...") - try: - response = requests.post(f"{rpc_url}/rpc/messaging/messages/{args.message_id}/vote", json=vote_data, headers=headers, timeout=30) - if response.status_code == 200: - result = response.json() - logger.info("Vote recorded successfully") - render_mapping("Vote result:", result) - else: - logger.error(f"Vote failed: {response.status_code}") - logger.error(f"Error: {response.text}") - sys.exit(1) - except Exception as e: - logger.error(f"Error voting on message: {e}") - sys.exit(1) - - -def handle_messaging_search(args, default_rpc_url, output_format, render_mapping): - """Handle message search.""" - rpc_url = args.rpc_url or default_rpc_url - chain_id = getattr(args, "chain_id", None) - - if not args.query: - logger.error("Error: --query is required") - sys.exit(1) - - logger.info(f"Searching messages for '{args.query}' on {rpc_url}...") - try: - params = {"query": args.query} - if chain_id: - params["chain_id"] = chain_id - - response = requests.get(f"{rpc_url}/rpc/messaging/messages/search", params=params, timeout=30) - if response.status_code == 200: - results = response.json() - if output_format(args) == "json": - logger.info(json.dumps(results, indent=2)) - else: - logger.info(f"Search results for '{args.query}':") - if isinstance(results, list): - for msg in results: - logger.info(f" Message ID: {msg.get('message_id', 'N/A')}, Topic: {msg.get('topic_id', 'N/A')}") - else: - render_mapping("Search results:", results) - else: - logger.error(f"Search failed: {response.status_code}") - logger.error(f"Error: {response.text}") - sys.exit(1) - except Exception as e: - logger.error(f"Error searching messages: {e}") - sys.exit(1) - - -def handle_messaging_reputation(args, default_rpc_url, output_format, render_mapping): - """Handle agent reputation query.""" - rpc_url = args.rpc_url or default_rpc_url - chain_id = getattr(args, "chain_id", None) - - if not args.agent_id: - logger.error("Error: --agent-id is required") - sys.exit(1) - - logger.info(f"Getting reputation for agent {args.agent_id} from {rpc_url}...") - try: - params = {} - if chain_id: - params["chain_id"] = chain_id - - response = requests.get(f"{rpc_url}/rpc/messaging/agents/{args.agent_id}/reputation", params=params, timeout=10) - if response.status_code == 200: - reputation = response.json() - if output_format(args) == "json": - logger.info(json.dumps(reputation, indent=2)) - else: - render_mapping(f"Agent {args.agent_id} reputation:", reputation) - else: - logger.error(f"Query failed: {response.status_code}") - logger.error(f"Error: {response.text}") - sys.exit(1) - except Exception as e: - logger.error(f"Error getting reputation: {e}") - sys.exit(1) - - -def handle_messaging_moderate(args, default_rpc_url, read_password, render_mapping): - """Handle message moderation.""" - rpc_url = args.rpc_url or default_rpc_url - chain_id = getattr(args, "chain_id", None) - - if not args.message_id or not args.action: - logger.error("Error: --message-id and --action are required") - sys.exit(1) - - # Get auth headers if wallet provided - headers = {} - if args.wallet: - password = read_password(args) - from keystore_auth import get_auth_headers - headers = get_auth_headers(args.wallet, password, args.password_file) - - moderation_data = { - "message_id": args.message_id, - "action": args.action, - } - if chain_id: - moderation_data["chain_id"] = chain_id - - logger.info(f"Moderating message {args.message_id} on {rpc_url}...") - try: - response = requests.post(f"{rpc_url}/rpc/messaging/messages/{args.message_id}/moderate", json=moderation_data, headers=headers, timeout=30) - if response.status_code == 200: - result = response.json() - logger.info("Moderation action completed successfully") - render_mapping("Moderation result:", result) - else: - logger.error(f"Moderation failed: {response.status_code}") - logger.error(f"Error: {response.text}") - sys.exit(1) - except Exception as e: - logger.error(f"Error moderating message: {e}") - sys.exit(1) diff --git a/cli/src/aitbc_cli/handlers/network.py b/cli/src/aitbc_cli/handlers/network.py deleted file mode 100644 index 975248e9..00000000 --- a/cli/src/aitbc_cli/handlers/network.py +++ /dev/null @@ -1,95 +0,0 @@ -"""Network status and peer management handlers.""" - -import json -import sys -from urllib.parse import urlparse - -import requests -import logging -logger = logging.getLogger(__name__) - - - -def handle_network_status(args, default_rpc_url, get_network_snapshot): - """Handle network status query.""" - snapshot = get_network_snapshot(getattr(args, "rpc_url", default_rpc_url)) - logger.info("Network status:") - logger.info(f" Connected nodes: {snapshot['connected_count']}") - for index, node in enumerate(snapshot["nodes"]): - label = "Local" if index == 0 else f"Peer {node['name']}" - health = "healthy" if node["healthy"] else "unreachable" - logger.info(f" {label}: {health}") - logger.info(f" Sync status: {snapshot['sync_status']}") -def handle_network_peers(args, default_rpc_url, get_network_snapshot): - """Handle network peers query.""" - snapshot = get_network_snapshot(getattr(args, "rpc_url", default_rpc_url)) - logger.info("Network peers:") - for node in snapshot["nodes"]: - endpoint = urlparse(node["rpc_url"]).netloc - status = "Connected" if node["healthy"] else f"Unreachable ({node['error'] or 'unknown error'})" - logger.info(f" - {node['name']} ({endpoint}) - {status}") -def handle_network_sync(args, default_rpc_url, get_network_snapshot): - """Handle network sync status query.""" - snapshot = get_network_snapshot(getattr(args, "rpc_url", default_rpc_url)) - logger.info("Network sync status:") - logger.info(f" Status: {snapshot['sync_status']}") - for node in snapshot["nodes"]: - height = node["height"] if node["height"] is not None else "unknown" - logger.info(f" {node['name']} height: {height}") - local_timestamp = snapshot["nodes"][0].get("timestamp") if snapshot["nodes"] else None - logger.info(f" Last local block: {local_timestamp or 'unknown'}") -def handle_network_ping(args, default_rpc_url, read_blockchain_env, normalize_rpc_url, first, probe_rpc_node): - """Handle network ping command.""" - env_config = read_blockchain_env() - _, _, local_port = normalize_rpc_url(getattr(args, "rpc_url", default_rpc_url)) - peer_rpc_port_value = env_config.get("rpc_bind_port") - try: - peer_rpc_port = int(peer_rpc_port_value) if peer_rpc_port_value else local_port - except ValueError: - peer_rpc_port = local_port - - node = first(getattr(args, "node_opt", None), getattr(args, "node", None), "aitbc1") - target_url = node if "://" in node else f"http://{node}:{peer_rpc_port}" - target = probe_rpc_node(node, target_url, chain_id=env_config.get("chain_id") or None) - - logger.info(f"Ping: Node {node} {'reachable' if target['healthy'] else 'unreachable'}") - logger.info(f" Endpoint: {urlparse(target['rpc_url']).netloc}") - if target["latency_ms"] is not None: - logger.info(f" Latency: {target['latency_ms']}ms") - logger.error(f" Status: {'connected' if target['healthy'] else 'error'}") -def handle_network_propagate(args, default_rpc_url, get_network_snapshot, first): - """Handle network data propagation.""" - data = first(getattr(args, "data_opt", None), getattr(args, "data", None), "test-data") - snapshot = get_network_snapshot(getattr(args, "rpc_url", default_rpc_url)) - logger.info("Data propagation: Complete") - logger.info(f" Data: {data}") - logger.info(f" Nodes: {snapshot['connected_count']}/{len(snapshot['nodes'])} reachable") -def handle_network_force_sync(args, default_rpc_url, render_mapping): - """Handle network force sync command.""" - rpc_url = args.rpc_url or default_rpc_url - chain_id = getattr(args, "chain_id", None) - - if not args.peer: - logger.error("Error: --peer is required") - sys.exit(1) - - sync_data = { - "peer": args.peer, - } - if chain_id: - sync_data["chain_id"] = chain_id - - logger.info(f"Forcing sync to peer {args.peer} on {rpc_url}...") - try: - response = requests.post(f"{rpc_url}/rpc/force-sync", json=sync_data, timeout=60) - if response.status_code == 200: - result = response.json() - logger.info("Force sync initiated successfully") - render_mapping("Sync result:", result) - else: - logger.error(f"Force sync failed: {response.status_code}") - logger.error(f"Error: {response.text}") - sys.exit(1) - except Exception as e: - logger.error(f"Error forcing sync: {e}") - sys.exit(1) diff --git a/cli/src/aitbc_cli/handlers/performance.py b/cli/src/aitbc_cli/handlers/performance.py deleted file mode 100644 index 6254ea5a..00000000 --- a/cli/src/aitbc_cli/handlers/performance.py +++ /dev/null @@ -1,57 +0,0 @@ -"""Performance command handlers for AITBC CLI.""" - -import json -import logging -logger = logging.getLogger(__name__) - - - -def handle_performance_benchmark(args, output_format, render_mapping): - """Handle performance benchmark command.""" - benchmark_data = { - "tps": 1250, - "latency_ms": 45, - "throughput_mbps": 850, - "cpu_usage": 65, - "memory_usage": 72, - "timestamp": __import__('datetime').datetime.now().isoformat() - } - - if output_format(args) == "json": - logger.info(json.dumps(benchmark_data, indent=2)) - else: - logger.info("Performance Benchmark:") - logger.info(f" TPS: {benchmark_data['tps']}") - logger.info(f" Latency: {benchmark_data['latency_ms']}ms") - logger.info(f" Throughput: {benchmark_data['throughput_mbps']}Mbps") - logger.info(f" CPU Usage: {benchmark_data['cpu_usage']}%") - logger.info(f" Memory Usage: {benchmark_data['memory_usage']}%") -def handle_performance_optimize(args, render_mapping): - """Handle performance optimize command.""" - target = getattr(args, "target", "general") - - optimization_data = { - "target": target, - "optimization_applied": True, - "improvement": "15-20%", - "timestamp": __import__('datetime').datetime.now().isoformat() - } - - logger.info(f"Performance optimization applied for {target}") - render_mapping("Optimization:", optimization_data) - - -def handle_performance_tune(args, render_mapping): - """Handle performance tune command.""" - parameters = getattr(args, "parameters", False) - aggressive = getattr(args, "aggressive", False) - - tune_data = { - "parameters_tuned": parameters, - "aggressive_mode": aggressive, - "tuning_applied": True, - "timestamp": __import__('datetime').datetime.now().isoformat() - } - - logger.info("Performance tuning applied") - render_mapping("Tuning:", tune_data) diff --git a/cli/src/aitbc_cli/handlers/pool_hub.py b/cli/src/aitbc_cli/handlers/pool_hub.py deleted file mode 100644 index 2bbf0af0..00000000 --- a/cli/src/aitbc_cli/handlers/pool_hub.py +++ /dev/null @@ -1,193 +0,0 @@ -"""Pool hub SLA and capacity management handlers.""" - -from aitbc import AITBCHTTPClient, NetworkError -import logging -logger = logging.getLogger(__name__) - - - -def handle_pool_hub_sla_metrics(args): - """Get SLA metrics for a miner or all miners.""" - try: - from commands.legacy.pool_hub import get_config as get_pool_hub_config - config = get_pool_hub_config() - - if args.test_mode: - logger.info(" SLA Metrics (test mode):") - logger.info(" Uptime: 97.5%") - logger.info(" Response Time: 850ms") - logger.info(" Job Completion Rate: 92.3%") - return - - pool_hub_url = getattr(config, "pool_hub_url", "http://localhost:8012") - miner_id = getattr(args, "miner_id", None) - - http_client = AITBCHTTPClient(base_url=pool_hub_url, timeout=30) - if miner_id: - metrics = http_client.get(f"/v1/sla/metrics/{miner_id}") - else: - metrics = http_client.get("/v1/sla/metrics") - - logger.info(" SLA Metrics:") - for key, value in metrics.items(): - logger.info(f" {key}: {value}") - except NetworkError as e: - logger.error(f"āŒ Failed to get SLA metrics: {e}") - except Exception as e: - logger.error(f"āŒ Error getting SLA metrics: {e}") -def handle_pool_hub_sla_violations(args): - """Get SLA violations across all miners.""" - try: - from commands.legacy.pool_hub import get_config as get_pool_hub_config - config = get_pool_hub_config() - - if args.test_mode: - logger.info("āš ļø SLA Violations (test mode):") - logger.info(" miner_001: response_time violation") - return - - pool_hub_url = getattr(config, "pool_hub_url", "http://localhost:8012") - http_client = AITBCHTTPClient(base_url=pool_hub_url, timeout=30) - violations = http_client.get("/v1/sla/violations") - - logger.info("āš ļø SLA Violations:") - for v in violations: - logger.info(f" {v}") - except NetworkError as e: - logger.error(f"āŒ Failed to get violations: {e}") - except Exception as e: - logger.error(f"āŒ Error getting violations: {e}") -def handle_pool_hub_capacity_snapshots(args): - """Get capacity planning snapshots.""" - try: - from commands.legacy.pool_hub import get_config as get_pool_hub_config - config = get_pool_hub_config() - - if args.test_mode: - logger.info("šŸ“Š Capacity Snapshots (test mode):") - logger.info(" Total Capacity: 1250 GPU") - logger.info(" Available: 320 GPU") - return - - pool_hub_url = getattr(config, "pool_hub_url", "http://localhost:8012") - http_client = AITBCHTTPClient(base_url=pool_hub_url, timeout=30) - snapshots = http_client.get("/v1/sla/capacity/snapshots") - - logger.info("šŸ“Š Capacity Snapshots:") - for s in snapshots: - logger.info(f" {s}") - except NetworkError as e: - logger.error(f"āŒ Failed to get snapshots: {e}") - except Exception as e: - logger.error(f"āŒ Error getting snapshots: {e}") -def handle_pool_hub_capacity_forecast(args): - """Get capacity forecast.""" - try: - from commands.legacy.pool_hub import get_config as get_pool_hub_config - config = get_pool_hub_config() - - if args.test_mode: - logger.info("šŸ”® Capacity Forecast (test mode):") - logger.info(" Projected Capacity: 1400 GPU") - logger.info(" Growth Rate: 12%") - return - - pool_hub_url = getattr(config, "pool_hub_url", "http://localhost:8012") - http_client = AITBCHTTPClient(base_url=pool_hub_url, timeout=30) - forecast = http_client.get("/v1/sla/capacity/forecast") - - logger.info("šŸ”® Capacity Forecast:") - for key, value in forecast.items(): - logger.info(f" {key}: {value}") - except NetworkError as e: - logger.error(f"āŒ Failed to get forecast: {e}") - except Exception as e: - logger.error(f"āŒ Error getting forecast: {e}") -def handle_pool_hub_capacity_recommendations(args): - """Get scaling recommendations.""" - try: - from commands.legacy.pool_hub import get_config as get_pool_hub_config - config = get_pool_hub_config() - - if args.test_mode: - logger.info("šŸ’” Capacity Recommendations (test mode):") - logger.info(" Type: scale_up") - logger.info(" Action: Add 50 GPU capacity") - return - - pool_hub_url = getattr(config, "pool_hub_url", "http://localhost:8012") - http_client = AITBCHTTPClient(base_url=pool_hub_url, timeout=30) - recommendations = http_client.get("/v1/sla/capacity/recommendations") - - logger.info("šŸ’” Capacity Recommendations:") - for r in recommendations: - logger.info(f" {r}") - except NetworkError as e: - logger.error(f"āŒ Failed to get recommendations: {e}") - except Exception as e: - logger.error(f"āŒ Error getting recommendations: {e}") -def handle_pool_hub_billing_usage(args): - """Get billing usage data.""" - try: - from commands.legacy.pool_hub import get_config as get_pool_hub_config - config = get_pool_hub_config() - - if args.test_mode: - logger.info("šŸ’° Billing Usage (test mode):") - logger.info(" Total GPU Hours: 45678") - logger.info(" Total Cost: $12500.50") - return - - pool_hub_url = getattr(config, "pool_hub_url", "http://localhost:8012") - http_client = AITBCHTTPClient(base_url=pool_hub_url, timeout=30) - usage = http_client.get("/v1/sla/billing/usage") - - logger.info("šŸ’° Billing Usage:") - for key, value in usage.items(): - logger.info(f" {key}: {value}") - except NetworkError as e: - logger.error(f"āŒ Failed to get billing usage: {e}") - except Exception as e: - logger.error(f"āŒ Error getting billing usage: {e}") -def handle_pool_hub_billing_sync(args): - """Trigger billing sync with coordinator-api.""" - try: - from commands.legacy.pool_hub import get_config as get_pool_hub_config - config = get_pool_hub_config() - - if args.test_mode: - logger.info("šŸ”„ Billing sync triggered (test mode)") - logger.info("āœ… Sync completed successfully") - return - - pool_hub_url = getattr(config, "pool_hub_url", "http://localhost:8012") - http_client = AITBCHTTPClient(base_url=pool_hub_url, timeout=60) - result = http_client.post("/v1/sla/billing/sync") - - logger.info("šŸ”„ Billing sync triggered") - logger.info(f"āœ… {result.get('message', 'Success')}") - except NetworkError as e: - logger.error(f"āŒ Billing sync failed: {e}") - except Exception as e: - logger.error(f"āŒ Error triggering billing sync: {e}") -def handle_pool_hub_collect_metrics(args): - """Trigger SLA metrics collection.""" - try: - from commands.legacy.pool_hub import get_config as get_pool_hub_config - config = get_pool_hub_config() - - if args.test_mode: - logger.info("šŸ“Š SLA metrics collection triggered (test mode)") - logger.info("āœ… Collection completed successfully") - return - - pool_hub_url = getattr(config, "pool_hub_url", "http://localhost:8012") - http_client = AITBCHTTPClient(base_url=pool_hub_url, timeout=60) - result = http_client.post("/v1/sla/metrics/collect") - - logger.info("šŸ“Š SLA metrics collection triggered") - logger.info(f"āœ… {result.get('message', 'Success')}") - except NetworkError as e: - logger.error(f"āŒ Metrics collection failed: {e}") - except Exception as e: - logger.error(f"āŒ Error triggering metrics collection: {e}") \ No newline at end of file diff --git a/cli/src/aitbc_cli/handlers/resource.py b/cli/src/aitbc_cli/handlers/resource.py deleted file mode 100644 index c70d9cb4..00000000 --- a/cli/src/aitbc_cli/handlers/resource.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Resource command handlers for AITBC CLI.""" - -import json -import logging -logger = logging.getLogger(__name__) - - - -def handle_resource_status(args, output_format, render_mapping): - """Handle resource status command.""" - status_data = { - "cpu": {"usage": 45, "available": 55}, - "memory": {"usage": 62, "available": 38}, - "disk": {"usage": 30, "available": 70}, - "gpu": {"usage": 0, "available": 100}, - "timestamp": __import__('datetime').datetime.now().isoformat() - } - - if output_format(args) == "json": - logger.info(json.dumps(status_data, indent=2)) - else: - render_mapping("Resource Status:", status_data) - - -def handle_resource_allocate(args, render_mapping): - """Handle resource allocate command.""" - agent_id = getattr(args, "agent_id", None) - cpu = getattr(args, "cpu", 2) - memory = getattr(args, "memory", 4096) - - allocation_data = { - "agent_id": agent_id, - "cpu_allocated": cpu, - "memory_allocated_mb": memory, - "status": "allocated", - "timestamp": __import__('datetime').datetime.now().isoformat() - } - - logger.info(f"Resources allocated to {agent_id}") - render_mapping("Allocation:", allocation_data) - - -def handle_resource_monitor(args, render_mapping): - """Handle resource monitor command.""" - interval = getattr(args, "interval", 5) - duration = getattr(args, "duration", 10) - - monitor_data = { - "monitoring_active": True, - "interval_seconds": interval, - "duration_seconds": duration, - "metrics_collected": 0, - "timestamp": __import__('datetime').datetime.now().isoformat() - } - - logger.info(f"Resource monitoring started (interval: {interval}s, duration: {duration}s)") - render_mapping("Monitor:", monitor_data) - - -def handle_resource_optimize(args, render_mapping): - """Handle resource optimize command.""" - target = getattr(args, "target", "cpu") - - optimization_data = { - "target": target, - "optimization_applied": True, - "efficiency_gain": "12%", - "timestamp": __import__('datetime').datetime.now().isoformat() - } - - logger.info(f"Resource optimization applied for {target}") - render_mapping("Optimization:", optimization_data) - - -def handle_resource_benchmark(args, render_mapping): - """Handle resource benchmark command.""" - benchmark_type = getattr(args, "type", "cpu") - - benchmark_data = { - "type": benchmark_type, - "score": 850, - "units": "operations/sec", - "timestamp": __import__('datetime').datetime.now().isoformat() - } - - logger.info(f"Resource benchmark completed for {benchmark_type}") - render_mapping("Benchmark:", benchmark_data) diff --git a/cli/src/aitbc_cli/handlers/system.py b/cli/src/aitbc_cli/handlers/system.py deleted file mode 100644 index 714e8468..00000000 --- a/cli/src/aitbc_cli/handlers/system.py +++ /dev/null @@ -1,623 +0,0 @@ -"""System and utility handlers.""" - -import sys -import logging -logger = logging.getLogger(__name__) - - - -def handle_system_status(args, cli_version): - """Handle system status command.""" - logger.info("System status: OK") - logger.info(f" Version: aitbc-cli v{cli_version}") - logger.info(" Services: Running") - logger.info(" Nodes: 2 connected") -def handle_analytics(args, default_rpc_url, get_blockchain_analytics): - """Handle analytics command.""" - analytics_type = getattr(args, "analytics_type", None) or getattr(args, "analytics_action", None) or getattr(args, "type", "blocks") - limit = getattr(args, "limit", 10) - rpc_url = getattr(args, "rpc_url", default_rpc_url) - if analytics_type == "blocks": - analytics = get_blockchain_analytics("blocks", limit, rpc_url=rpc_url) - elif analytics_type == "report": - analytics = { - "type": "report", - "report_type": getattr(args, "report_type", "all"), - "status": "Generated", - "throughput": "healthy", - "marketplace": "operational", - "economic_efficiency": "optimized", - } - elif analytics_type == "metrics": - analytics = { - "type": "metrics", - "period": getattr(args, "period", "24h"), - "latency_ms": 45, - "success_rate": "99.5%", - "market_orders": "tracked", - "cost_efficiency": "22% improvement", - } - elif analytics_type == "export": - export_format = getattr(args, "format", "json") - analytics = { - "type": "export", - "format": export_format, - "status": "Exported", - "records": 5, - } - elif analytics_type == "predict": - analytics = { - "type": "predict", - "model": getattr(args, "model", "lstm"), - "target": getattr(args, "target", "job-completion"), - "prediction": "stable growth", - "confidence": "87%", - } - elif analytics_type == "optimize": - analytics = { - "type": "optimize", - "target": getattr(args, "target", "efficiency"), - "parameters": getattr(args, "parameters", False), - "recommendation": "balanced resource allocation", - "expected_gain": "14%", - } - else: - analytics = get_blockchain_analytics(analytics_type, limit, rpc_url=rpc_url) - if analytics: - logger.info(f"Blockchain Analytics ({analytics['type']}):") - for key, value in analytics.items(): - if key != "type": - logger.info(f" {key}: {value}") - else: - sys.exit(1) - - -def handle_agent_action(args, agent_operations, render_mapping): - """Handle agent action command.""" - kwargs = {} - for name in ("name", "description", "verification", "max_execution_time", "max_cost_budget", "input_data", "wallet", "priority", "execution_id", "status", "agent", "message", "to", "content", "password", "password_file", "rpc_url"): - value = getattr(args, name, None) - if value not in (None, "", False): - kwargs[name] = value - - try: - result = agent_operations(args.agent_action, **kwargs) - if not result: - # Return stub data instead of failing - stub_result = { - "action": args.agent_action, - "status": "simulated", - "timestamp": __import__('datetime').datetime.now().isoformat() - } - logger.info(f"Agent {args.agent_action} (simulated)") - render_mapping(f"Agent {args.agent_action}:", stub_result) - return - # Handle case where result doesn't have 'action' field (e.g., message send) - if 'action' in result: - render_mapping(f"Agent {result['action']}:", result) - else: - # Just print success message for message send - logger.info("Agent operation completed successfully") - except Exception as e: - # Return stub data on error - stub_result = { - "action": args.agent_action, - "status": "simulated", - "error": str(e), - "timestamp": __import__('datetime').datetime.now().isoformat() - } - logger.error(f"Agent {args.agent_action} (simulated - error: {e})") - render_mapping(f"Agent {args.agent_action}:", stub_result) - - -def handle_agent_sdk_action(args, render_mapping): - """Handle agent SDK action command.""" - action = getattr(args, "agent_sdk_action", None) - - if action == "create": - name = getattr(args, "name", None) - agent_type = getattr(args, "type", "provider") - - sdk_data = { - "agent_id": f"agent_{int(__import__('time').time())}", - "name": name, - "type": agent_type, - "status": "created", - "timestamp": __import__('datetime').datetime.now().isoformat() - } - - logger.info(f"Agent SDK created: {name}") - render_mapping("Agent SDK:", sdk_data) - - elif action == "update-status": - agent_id = getattr(args, "agent_id", None) - status = getattr(args, "status", None) - load_metrics = getattr(args, "load_metrics", {}) - coordinator_url = getattr(args, "coordinator_url", "http://localhost:9001") - - if not agent_id or not status: - logger.error("Error: --agent-id and --status are required") - sys.exit(1) - - status_update_request = { - "status": status, - "load_metrics": load_metrics if isinstance(load_metrics, dict) else {} - } - - logger.info(f"Updating agent {agent_id} status to {status}...") - try: - import requests - response = requests.put( - f"{coordinator_url}/v1/agents/{agent_id}/status", - json=status_update_request, - timeout=30 - ) - - if response.status_code == 200: - result = response.json() - logger.info(f"Agent status updated successfully") - render_mapping("Status Update:", result) - else: - logger.error(f"Status update failed: {response.status_code}") - logger.error(f"Error: {response.text}") - sys.exit(1) - except Exception as e: - logger.error(f"Error updating agent status: {e}") - sys.exit(1) - - elif action == "register": - agent_id = getattr(args, "agent_id", None) - agent_type = getattr(args, "type", "worker") - capabilities = getattr(args, "capabilities", []) - services = getattr(args, "services", []) - endpoints = getattr(args, "endpoints", {}) - metadata = getattr(args, "metadata", {}) - coordinator_url = getattr(args, "coordinator_url", "http://localhost:9001") - - # Build registration request - registration_request = { - "agent_id": agent_id, - "agent_type": agent_type, - "capabilities": capabilities if isinstance(capabilities, list) else (capabilities.split(",") if capabilities else []), - "services": services if isinstance(services, list) else (services.split(",") if services else []), - "endpoints": endpoints if isinstance(endpoints, dict) else (json.loads(endpoints) if endpoints else {}), - "metadata": metadata if isinstance(metadata, dict) else (json.loads(metadata) if metadata else {}) - } - - logger.info(f"Registering agent {agent_id} with coordinator at {coordinator_url}...") - try: - import requests - response = requests.post( - f"{coordinator_url}/v1/agents/register", - json=registration_request, - timeout=30 - ) - - if response.status_code in (200, 201): - result = response.json() - logger.info(f"Agent registered successfully") - render_mapping("Registration:", result) - else: - logger.error(f"Registration failed: {response.status_code}") - logger.error(f"Error: {response.text}") - sys.exit(1) - except Exception as e: - logger.error(f"Error registering agent: {e}") - sys.exit(1) - - elif action == "list": - # Agent discovery via coordinator - coordinator_url = getattr(args, "coordinator_url", "http://localhost:9001") - status = getattr(args, "status", None) - agent_type = getattr(args, "agent_type", None) - - query = {} - if status: - query["status"] = status - if agent_type: - query["agent_type"] = agent_type - - logger.info(f"Discovering agents from coordinator at {coordinator_url}...") - try: - import requests - response = requests.post( - f"{coordinator_url}/v1/agents/discover", - json=query, - timeout=30 - ) - - if response.status_code == 200: - result = response.json() - logger.info(f"Found {result.get('count', 0)} agents") - render_mapping("Agents:", result) - else: - logger.error(f"Discovery failed: {response.status_code}") - logger.error(f"Error: {response.text}") - sys.exit(1) - except Exception as e: - logger.error(f"Error discovering agents: {e}") - sys.exit(1) - - elif action == "status": - agent_id = getattr(args, "agent_id", None) - coordinator_url = getattr(args, "coordinator_url", "http://localhost:9001") - - logger.info(f"Getting agent info for {agent_id} from coordinator at {coordinator_url}...") - try: - import requests - response = requests.get( - f"{coordinator_url}/v1/agents/{agent_id}", - timeout=30 - ) - - if response.status_code == 200: - result = response.json() - logger.info(f"Agent info retrieved") - render_mapping("Agent:", result) - elif response.status_code == 404: - logger.info(f"Agent not found: {agent_id}") - sys.exit(1) - else: - logger.error(f"Query failed: {response.status_code}") - logger.error(f"Error: {response.text}") - sys.exit(1) - except Exception as e: - logger.error(f"Error getting agent info: {e}") - sys.exit(1) - - elif action == "capabilities": - caps_data = { - "gpu_available": True, - "gpu_memory": "16GB", - "supported_models": ["llama2", "mistral", "gpt-4"], - "max_concurrent_jobs": 2 - } - - logger.info("System capabilities") - render_mapping("Capabilities:", caps_data) - - else: - # Stub for other SDK actions - sdk_result = { - "action": action, - "status": "simulated", - "timestamp": __import__('datetime').datetime.now().isoformat() - } - - logger.info(f"Agent SDK {action} (simulated)") - render_mapping("SDK Operation:", sdk_result) - - -def handle_hermes_training_action(args, hermes_training_operations, first, render_mapping): - """Handle hermes training action command.""" - kwargs = {} - for name in ("agent_file", "wallet", "environment", "agent_id", "metrics", "price"): - value = getattr(args, name, None) - if value not in (None, "", False): - kwargs[name] = value - market_action = first(getattr(args, "market_action", None), getattr(args, "market_action_opt", None)) - if market_action: - kwargs["market_action"] = market_action - - # Handle train actions - if getattr(args, "hermes_training_action", None) == "train": - train_action = getattr(args, "train_action", None) - if train_action == "agent": - for name in ("agent_id", "stage", "training_data", "log_level"): - value = getattr(args, name, None) - if value not in (None, "", False): - kwargs[name] = value - kwargs["train_action"] = "agent" - elif train_action == "validate": - for name in ("agent_id", "stage"): - value = getattr(args, name, None) - if value not in (None, "", False): - kwargs[name] = value - kwargs["train_action"] = "validate" - elif train_action == "certify": - for name in ("agent_id",): - value = getattr(args, name, None) - if value not in (None, "", False): - kwargs[name] = value - kwargs["train_action"] = "certify" - - result = hermes_training_operations(args.hermes_training_action, **kwargs) - if not result: - sys.exit(1) - render_mapping(f"hermes Training {result['action']}:", result) - - -def handle_workflow_action(args, workflow_operations, render_mapping): - """Handle workflow action command.""" - kwargs = {} - for name in ("name", "template", "config_file", "params", "async_exec"): - value = getattr(args, name, None) - if value not in (None, "", False): - kwargs[name] = value - result = workflow_operations(args.workflow_action, **kwargs) - if not result: - sys.exit(1) - render_mapping(f"Workflow {result['action']}:", result) - - -def handle_resource_action(args, resource_operations, render_mapping): - """Handle resource action command.""" - kwargs = {} - for name in ("type", "agent_id", "cpu", "memory", "duration"): - value = getattr(args, name, None) - if value not in (None, "", False): - kwargs[name] = value - result = resource_operations(args.resource_action, **kwargs) - if not result: - sys.exit(1) - render_mapping(f"Resource {result['action']}:", result) - - -def handle_simulate_action(args, simulate_blockchain, simulate_wallets, simulate_price, simulate_network, simulate_ai_jobs): - """Handle simulate command.""" - if args.simulate_command == "blockchain": - simulate_blockchain(args.blocks, args.transactions, args.delay) - elif args.simulate_command == "wallets": - simulate_wallets(args.wallets, args.balance, args.transactions, args.amount_range) - elif args.simulate_command == "price": - simulate_price(args.price, args.volatility, args.timesteps, args.delay) - elif args.simulate_command == "network": - simulate_network(args.nodes, args.network_delay, args.failure_rate) - elif args.simulate_command == "ai-jobs": - simulate_ai_jobs(args.jobs, args.models, args.duration_range) - else: - logger.info(f"Unknown simulate command: {args.simulate_command}") - sys.exit(1) - - -def handle_economics_action(args, render_mapping): - """Handle economics command.""" - action = getattr(args, "economics_action", None) - if action == "distributed": - result = { - "action": "distributed", - "cost_optimization": getattr(args, "cost_optimize", False), - "nodes_optimized": 3, - "cost_reduction": "15.3%", - "last_sync": "2024-01-15T10:30:00Z" - } - render_mapping("Economics:", result) - elif action == "model": - result = { - "action": "model", - "model_type": getattr(args, "type", "cost-optimization"), - "cost_per_inference": "0.008 AIT", - "utilization_target": "90%", - "status": "ready", - } - render_mapping("Economic Model:", result) - elif action == "market": - result = { - "action": "market", - "analysis": getattr(args, "analyze", False), - "demand": "moderate", - "supply": "available", - "pricing_signal": "stable", - } - render_mapping("Market Economics:", result) - elif action == "trends": - result = { - "action": "trends", - "period": getattr(args, "period", "30d"), - "revenue_trend": "up", - "cost_trend": "down", - "efficiency_trend": "improving", - } - render_mapping("Economic Trends:", result) - elif action == "optimize": - result = { - "action": "optimize", - "target": getattr(args, "target", "all"), - "strategy": "balanced", - "projected_improvement": "18%", - "status": "optimized", - } - render_mapping("Economic Optimization:", result) - elif action == "strategy": - result = { - "action": "strategy", - "global_strategy": getattr(args, "global_strategy", False), - "optimize": getattr(args, "optimize", False), - "coordination": "enabled", - "status": "ready", - } - render_mapping("Economic Strategy:", result) - elif action == "balance": - result = { - "action": "balance", - "total_supply": "1000000 AIT", - "circulating_supply": "750000 AIT", - "staked": "250000 AIT", - "burned": "50000 AIT" - } - render_mapping("Token Balance:", result) - else: - logger.info(f"Unknown economics action: {action}") - sys.exit(1) - - -def handle_cluster_action(args, render_mapping): - """Handle cluster command.""" - action = getattr(args, "cluster_action", None) - if action == "sync": - result = { - "action": "sync", - "nodes_synced": 5, - "total_nodes": 5, - "sync_status": "complete", - "last_sync": "2024-01-15T10:30:00Z" - } - render_mapping("Cluster Sync:", result) - elif action == "status": - result = { - "action": "status", - "cluster_health": "healthy", - "active_nodes": 5, - "total_nodes": 5, - "load_balance": "optimal" - } - render_mapping("Cluster Status:", result) - else: - logger.info(f"Unknown cluster action: {action}") - sys.exit(1) - - -def handle_performance_action(args, render_mapping): - """Handle performance command.""" - action = getattr(args, "performance_action", None) - if action == "benchmark": - result = { - "action": "benchmark", - "tps": 1250, - "latency_ms": 45, - "throughput_mbps": 850, - "cpu_usage": "65%", - "memory_usage": "72%" - } - render_mapping("Performance Benchmark:", result) - elif action == "profile": - result = { - "action": "profile", - "hotspots": ["block_validation", "transaction_processing"], - "optimization_suggestions": ["caching", "parallelization"] - } - render_mapping("Performance Profile:", result) - else: - logger.info(f"Unknown performance action: {action}") - sys.exit(1) - - -def handle_security_action(args, render_mapping): - """Handle security command.""" - action = getattr(args, "security_action", None) - if action == "audit": - result = { - "action": "audit", - "vulnerabilities_found": 0, - "security_score": "A+", - "last_audit": "2024-01-15T10:30:00Z" - } - render_mapping("Security Audit:", result) - elif action == "scan": - result = { - "action": "scan", - "scanned_components": ["smart_contracts", "rpc_endpoints", "wallet_keys"], - "threats_detected": 0, - "scan_status": "complete" - } - render_mapping("Security Scan:", result) - elif action == "patch": - result = { - "action": "patch", - "critical_patches": getattr(args, "critical", False), - "patches_applied": 0, - "status": "up to date" - } - render_mapping("Security Patch:", result) - else: - logger.info(f"Unknown security action: {action}") - sys.exit(1) - - -def handle_compliance_check(args, render_mapping): - """Handle compliance check command.""" - standard = getattr(args, "standard", "gdpr") - - compliance_data = { - "standard": standard, - "status": "compliant", - "last_check": __import__('datetime').datetime.now().isoformat(), - "issues_found": 0 - } - - logger.info(f"Compliance check for {standard}") - render_mapping("Compliance:", compliance_data) - - -def handle_compliance_report(args, render_mapping): - """Handle compliance report command.""" - format_type = getattr(args, "format", "detailed") - - report_data = { - "format": format_type, - "generated_at": __import__('datetime').datetime.now().isoformat(), - "standards_checked": ["gdpr", "hipaa", "soc2"], - "overall_status": "compliant" - } - - logger.info(f"Compliance report ({format_type})") - render_mapping("Report:", report_data) - - -def handle_cluster_status(args, render_mapping): - """Handle cluster status command.""" - nodes = getattr(args, "nodes", ["aitbc", "aitbc1"]) - - status_data = { - "connected_nodes": len(nodes), - "nodes": nodes, - "local_status": "healthy", - "sync_status": "standalone", - "timestamp": __import__('datetime').datetime.now().isoformat() - } - - render_mapping("Network Status:", status_data) - - -def handle_cluster_sync(args, render_mapping): - """Handle cluster sync command.""" - sync_all = getattr(args, "all", False) - - sync_data = { - "nodes_synced": 5 if sync_all else 2, - "total_nodes": 5, - "sync_status": "complete", - "last_sync": __import__('datetime').datetime.now().isoformat() - } - - logger.info("Cluster sync completed") - render_mapping("Cluster Sync:", sync_data) - - -def handle_cluster_balance(args, render_mapping): - """Handle cluster balance command.""" - workload = getattr(args, "workload", False) - - balance_data = { - "workload_balanced": workload, - "nodes_active": 5, - "load_distribution": "balanced", - "timestamp": __import__('datetime').datetime.now().isoformat() - } - - logger.info("Workload balanced across cluster") - render_mapping("Cluster Balance:", balance_data) - - -def handle_script_run(args, render_mapping): - """Handle script run command.""" - file_path = getattr(args, "file", None) - script_args = getattr(args, "args", None) - - script_data = { - "file": file_path, - "args": script_args, - "status": "executed", - "timestamp": __import__('datetime').datetime.now().isoformat() - } - - logger.info(f"Script executed: {file_path}") - render_mapping("Script:", script_data) - - -def handle_mining_action(args, default_rpc_url, mining_operations): - """Handle mining command.""" - action = getattr(args, "mining_action", None) - result = mining_operations(action, wallet=getattr(args, "wallet", None), rpc_url=getattr(args, "rpc_url", default_rpc_url)) - if not result: - sys.exit(1) diff --git a/cli/src/aitbc_cli/handlers/wallet.py b/cli/src/aitbc_cli/handlers/wallet.py deleted file mode 100644 index 8953264d..00000000 --- a/cli/src/aitbc_cli/handlers/wallet.py +++ /dev/null @@ -1,261 +0,0 @@ -"""Wallet command handlers.""" - -import json -import requests -import sys -from aitbc.utils.paths import get_data_path -import logging -logger = logging.getLogger(__name__) - - - -def handle_wallet_create(args, create_wallet, read_password, first): - """Handle wallet create command.""" - wallet_name = first(getattr(args, "wallet_name", None), getattr(args, "wallet_name_opt", None)) - password = read_password(args, "wallet_password") - if not wallet_name or not password: - logger.error("Error: Wallet name and password are required") - sys.exit(1) - address = create_wallet(wallet_name, password) - logger.info(f"Wallet address: {address}") -def handle_wallet_list(args, list_wallets, output_format): - """Handle wallet list command.""" - wallets = list_wallets() - if output_format(args) == "json": - logger.info(json.dumps(wallets, indent=2)) - return - logger.info("Wallets:") - for wallet in wallets: - logger.info(f" {wallet['name']}: {wallet['address']}") -def handle_wallet_balance(args, default_rpc_url, list_wallets, get_balance, first): - """Handle wallet balance command.""" - rpc_url = getattr(args, "rpc_url", default_rpc_url) - if getattr(args, "all", False): - logger.info("All wallet balances:") - for wallet in list_wallets(): - balance_info = get_balance(wallet["name"], rpc_url=rpc_url) - if balance_info: - logger.info(f" {wallet['name']}: {balance_info['balance']} AIT") - else: - logger.info(f" {wallet['name']}: unavailable") - return - wallet_name = first(getattr(args, "wallet_name", None), getattr(args, "wallet_name_opt", None)) - if not wallet_name: - logger.error("Error: Wallet name is required") - sys.exit(1) - balance_info = get_balance(wallet_name, rpc_url=rpc_url) - if not balance_info: - sys.exit(1) - logger.info(f"Wallet: {balance_info['wallet_name']}") - logger.info(f"Address: {balance_info['address']}") - logger.info(f"Balance: {balance_info['balance']} AIT") - logger.info(f"Nonce: {balance_info['nonce']}") -def handle_wallet_transactions(args, get_transactions, output_format, first): - """Handle wallet transactions command.""" - wallet_name = first(getattr(args, "wallet_name", None), getattr(args, "wallet_name_opt", None)) - if not wallet_name: - logger.error("Error: Wallet name is required") - sys.exit(1) - transactions = get_transactions(wallet_name, limit=args.limit, rpc_url=args.rpc_url) - if output_format(args) == "json": - logger.info(json.dumps(transactions, indent=2)) - return - logger.info(f"Transactions for {wallet_name}:") - for index, tx in enumerate(transactions, 1): - logger.info(f" {index}. Hash: {tx.get('hash', 'N/A')}") - logger.info(f" Amount: {tx.get('value', 0)} AIT") - logger.info(f" Fee: {tx.get('fee', 0)} AIT") - logger.info(f" Type: {tx.get('type', 'N/A')}") - logger.info("") - - -def handle_wallet_send(args, send_transaction, read_password, first): - """Handle wallet send command.""" - from pathlib import Path - import json - from cryptography.hazmat.primitives.asymmetric import ed25519 - - from_wallet = first(getattr(args, "from_wallet_arg", None), getattr(args, "from_wallet", None)) - to_address = first(getattr(args, "to_address_arg", None), getattr(args, "to_address", None)) - amount_value = first(getattr(args, "amount_arg", None), getattr(args, "amount", None)) - - # Password is now required for signing - password = read_password(args, "wallet_password") - - if not from_wallet or not to_address or amount_value is None: - logger.error("Error: From wallet, destination, and amount are required") - sys.exit(1) - - if not password: - logger.error("Error: Password is required for signing transaction") - sys.exit(1) - - # Use default fee if not specified - fee = getattr(args, "fee", 10) - if fee is None: - fee = 10 - - # Use direct RPC call with decrypted private key - keystore_dir = Path("/var/lib/aitbc/keystore") - sender_keystore = keystore_dir / f"{from_wallet}.json" - - if not sender_keystore.exists(): - logger.error(f"Error: Wallet '{from_wallet}' not found") - sys.exit(1) - - with open(sender_keystore) as f: - sender_data = json.load(f) - - sender_address = sender_data['address'] - - # Decrypt private key for signing - try: - sys.path.insert(0, "/opt/aitbc/cli") - import importlib.util - spec = importlib.util.spec_from_file_location('aitbc_cli_module', '/opt/aitbc/cli/aitbc_cli.py') - aitbc_cli_module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(aitbc_cli_module) - private_key_hex = aitbc_cli_module.decrypt_private_key(sender_keystore, password) - private_key = ed25519.Ed25519PrivateKey.from_private_bytes(bytes.fromhex(private_key_hex)) - except Exception as e: - logger.error(f"Error decrypting wallet: {e}") - sys.exit(1) - - # Get RPC URL - rpc_url = getattr(args, "rpc_url", "http://localhost:8006") - - # Get chain_id - try: - from sys.path import insert - insert(0, "/opt/aitbc") - from aitbc_cli.utils.chain_id import get_chain_id - chain_id = get_chain_id(rpc_url, override=None, timeout=5) - except Exception: - chain_id = "ait-testnet" - - # Get actual nonce from blockchain - actual_nonce = 0 - try: - account_data = requests.get(f"{rpc_url}/rpc/account/{sender_address}", timeout=5).json() - actual_nonce = account_data.get("nonce", 0) - except Exception: - actual_nonce = 0 - - # Build transaction with modern payload format - transaction_payload = { - "type": "TRANSFER", - "from": sender_address, - "to": to_address, - "amount": int(float(amount_value)), - "fee": fee, - "nonce": actual_nonce, - "payload": { - "recipient": to_address, - "amount": int(float(amount_value)) - }, - "chain_id": chain_id - } - - # Sign transaction - message = json.dumps(transaction_payload, sort_keys=True).encode() - signature = private_key.sign(message) - signature_hex = signature.hex() - - transaction_payload["signature"] = signature_hex - - # Submit transaction - try: - response = requests.post(f"{rpc_url}/rpc/transaction", json=transaction_payload, timeout=30) - - if response.status_code == 200: - result = response.json() - if result.get("success"): - logger.info("Transaction sent successfully") - logger.info(f"Transaction hash: {result.get('transaction_hash')}") - else: - logger.error(f"Transaction failed: {result.get('message', 'Unknown error')}") - sys.exit(1) - else: - logger.error(f"Error submitting transaction: {response.status_code}") - logger.error(f"Error: {response.text}") - sys.exit(1) - except Exception as e: - logger.error(f"Error submitting transaction: {e}") - sys.exit(1) - - -def handle_wallet_import(args, import_wallet, read_password, first): - """Handle wallet import command.""" - wallet_name = first(getattr(args, "wallet_name", None), getattr(args, "wallet_name_opt", None)) - private_key = first(getattr(args, "private_key_arg", None), getattr(args, "private_key_opt", None)) - password = read_password(args, "wallet_password") - if not wallet_name or not private_key or not password: - logger.error("Error: Wallet name, private key, and password are required") - sys.exit(1) - address = import_wallet(wallet_name, private_key, password) - if not address: - sys.exit(1) - logger.info(f"Wallet address: {address}") -def handle_wallet_export(args, export_wallet, read_password, first): - """Handle wallet export command.""" - wallet_name = first(getattr(args, "wallet_name", None), getattr(args, "wallet_name_opt", None)) - password = read_password(args, "wallet_password") - if not wallet_name or not password: - logger.error("Error: Wallet name and password are required") - sys.exit(1) - private_key = export_wallet(wallet_name, password) - if not private_key: - sys.exit(1) - logger.info(private_key) -def handle_wallet_delete(args, delete_wallet, first): - """Handle wallet delete command.""" - wallet_name = first(getattr(args, "wallet_name", None), getattr(args, "wallet_name_opt", None)) - if not wallet_name or not args.confirm: - logger.error("Error: Wallet name and --confirm are required") - sys.exit(1) - if not delete_wallet(wallet_name): - sys.exit(1) - - -def handle_wallet_rename(args, rename_wallet, first): - """Handle wallet rename command.""" - old_name = first(getattr(args, "old_name_arg", None), getattr(args, "old_name", None)) - new_name = first(getattr(args, "new_name_arg", None), getattr(args, "new_name", None)) - if not old_name or not new_name: - logger.error("Error: Old and new wallet names are required") - sys.exit(1) - if not rename_wallet(old_name, new_name): - sys.exit(1) - - -def handle_wallet_backup(args, first): - """Handle wallet backup command.""" - wallet_name = first(getattr(args, "wallet_name", None), getattr(args, "wallet_name_opt", None)) - if not wallet_name: - logger.error("Error: Wallet name is required") - sys.exit(1) - logger.info(f"Wallet backup: {wallet_name}") - backup_path = get_data_path("backups") - logger.info(f" Backup created: {backup_path}/{wallet_name}_$(date +%Y%m%d).json") - logger.info(" Status: completed") -def handle_wallet_sync(args, first): - """Handle wallet sync command.""" - wallet_name = first(getattr(args, "wallet_name", None), getattr(args, "wallet_name_opt", None)) - if args.all: - logger.info("Wallet sync: All wallets") - elif wallet_name: - logger.info(f"Wallet sync: {wallet_name}") - else: - logger.error("Error: Wallet name or --all is required") - sys.exit(1) - logger.info(" Sync status: completed") - logger.info(" Last sync: $(date)") -def handle_wallet_batch(args, send_batch_transactions, read_password): - """Handle wallet batch command.""" - password = read_password(args) - if not password: - logger.error("Error: Password is required") - sys.exit(1) - with open(args.file) as handle: - transactions = json.load(handle) - send_batch_transactions(transactions, password, rpc_url=args.rpc_url) diff --git a/cli/src/aitbc_cli/handlers/workflow.py b/cli/src/aitbc_cli/handlers/workflow.py deleted file mode 100644 index d2782575..00000000 --- a/cli/src/aitbc_cli/handlers/workflow.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Workflow command handlers for AITBC CLI.""" - -import json -import logging -logger = logging.getLogger(__name__) - - - -def handle_workflow_create(args, render_mapping): - """Handle workflow create command.""" - name = getattr(args, "name", None) or "unnamed-workflow" - template = getattr(args, "template", "custom") - steps = getattr(args, "steps", 5) - - workflow_data = { - "workflow_id": f"workflow_{int(__import__('time').time())}", - "name": name, - "template": template, - "status": "created", - "steps": steps, - "estimated_duration": f"{steps * 2}-{steps * 3} minutes" - } - - logger.info(f"Workflow created: {workflow_data['workflow_id']}") - render_mapping("Workflow:", workflow_data) - - -def handle_workflow_schedule(args, render_mapping): - """Handle workflow schedule command.""" - name = getattr(args, "name", None) - cron = getattr(args, "cron", None) - command = getattr(args, "command", None) - - schedule_data = { - "schedule_id": f"schedule_{int(__import__('time').time())}", - "workflow_name": name, - "cron_expression": cron, - "command": command, - "status": "scheduled", - "next_run": "pending" - } - - logger.info(f"Workflow scheduled: {schedule_data['schedule_id']}") - render_mapping("Schedule:", schedule_data) - - -def handle_workflow_monitor(args, output_format, render_mapping): - """Handle workflow monitor command.""" - name = getattr(args, "name", None) - - monitor_data = { - "status": "active", - "workflows_running": 2, - "workflows_completed": 15, - "workflows_failed": 0, - "last_check": __import__('datetime').datetime.now().isoformat() - } - - if output_format(args) == "json": - logger.info(json.dumps(monitor_data, indent=2)) - else: - render_mapping("Workflow Monitor:", monitor_data) diff --git a/cli/src/aitbc_cli/main.py b/cli/src/aitbc_cli/main.py deleted file mode 100644 index f0277234..00000000 --- a/cli/src/aitbc_cli/main.py +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env python3 -"""AITBC Command Line Interface - Main Entry Point.""" - -import click -from .commands import ( - wallet, - workflow, - transactions, - agent_comm, - system, - system_architect, - simulate, - resource, - operations, - monitor, - mining, - node, - marketplace_cmd, - hermes, - genesis, - gpu_marketplace, - exchange, - exchange_island, - edge, - deployment, - cross_chain, - config, - chain, - analytics, - agent_sdk, -) - - -@click.group() -@click.version_option(version="0.1.0") -def cli(): - """AITBC Command Line Interface.""" - pass - - -# Add command groups -cli.add_command(wallet.wallet) -cli.add_command(workflow.workflow) -cli.add_command(transactions.transactions) -cli.add_command(agent_comm.agent_comm) -cli.add_command(system.system) -cli.add_command(system_architect.system_architect) -cli.add_command(simulate.simulate) -cli.add_command(resource.resource) -cli.add_command(operations.operations) -cli.add_command(monitor.monitor) -cli.add_command(mining.mining) -cli.add_command(node.node) -cli.add_command(marketplace_cmd.marketplace_cmd) -cli.add_command(hermes.hermes) -cli.add_command(genesis.genesis) -cli.add_command(gpu_marketplace.gpu_marketplace) -cli.add_command(exchange.exchange) -cli.add_command(exchange_island.exchange_island) -cli.add_command(edge.edge) -cli.add_command(deployment.deployment) -cli.add_command(cross_chain.cross_chain) -cli.add_command(config.config) -cli.add_command(chain.chain) -cli.add_command(analytics.analytics) -cli.add_command(agent_sdk.agent_sdk) - - -if __name__ == "__main__": - cli() diff --git a/cli/src/aitbc_cli/parser_context.py b/cli/src/aitbc_cli/parser_context.py deleted file mode 100644 index 6ab4b267..00000000 --- a/cli/src/aitbc_cli/parser_context.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Shared parser context for unified CLI command registration.""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any, Callable, Mapping - - -@dataclass(slots=True) -class ParserContext: - default_rpc_url: str - default_coordinator_url: str - cli_version: str - first: Callable[..., Any] - read_password: Callable[..., Any] - output_format: Callable[..., Any] - render_mapping: Callable[..., Any] - read_blockchain_env: Callable[..., Any] - normalize_rpc_url: Callable[..., Any] - probe_rpc_node: Callable[..., Any] - get_network_snapshot: Callable[..., Any] - handlers: Mapping[str, Callable[..., Any]] - - def __getattr__(self, name: str): - try: - return self.handlers[name] - except KeyError as exc: - raise AttributeError(name) from exc diff --git a/cli/src/aitbc_cli/parsers/__init__.py b/cli/src/aitbc_cli/parsers/__init__.py deleted file mode 100644 index 720e6f01..00000000 --- a/cli/src/aitbc_cli/parsers/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Parser registration modules for the unified CLI.""" - -from . import ai, agent, analytics, blockchain, bridge, contract, genesis, market, messaging, network, hermes, pool_hub, resource, script, system, wallet, workflow - -def register_all(subparsers, ctx): - wallet.register(subparsers, ctx) - blockchain.register(subparsers, ctx) - messaging.register(subparsers, ctx) - network.register(subparsers, ctx) - market.register(subparsers, ctx) - ai.register(subparsers, ctx) - analytics.register(subparsers, ctx) - script.register(subparsers, ctx) - system.register(subparsers, ctx) - agent.register(subparsers, ctx) - hermes.register(subparsers, ctx) - workflow.register(subparsers, ctx) - resource.register(subparsers, ctx) - genesis.register(subparsers, ctx) - pool_hub.register(subparsers, ctx) - bridge.register(subparsers, ctx) - contract.register(subparsers, ctx) diff --git a/cli/src/aitbc_cli/parsers/agent.py b/cli/src/aitbc_cli/parsers/agent.py deleted file mode 100644 index 5b85d1d8..00000000 --- a/cli/src/aitbc_cli/parsers/agent.py +++ /dev/null @@ -1,134 +0,0 @@ -"""Agent command registration for the unified CLI.""" - -import argparse - -from parser_context import ParserContext - - -def register(subparsers: argparse._SubParsersAction, ctx: ParserContext) -> None: - agent_parser = subparsers.add_parser("agent", help="AI agent workflow orchestration") - agent_parser.set_defaults(handler=lambda parsed, parser=agent_parser: parser.print_help()) - agent_subparsers = agent_parser.add_subparsers(dest="agent_action") - - agent_create_parser = agent_subparsers.add_parser("create", help="Create an agent workflow") - agent_create_parser.add_argument("--name", required=True) - agent_create_parser.add_argument("--description") - agent_create_parser.add_argument("--workflow-file") - agent_create_parser.add_argument("--verification", choices=["basic", "full", "zero-knowledge"], default="basic") - agent_create_parser.add_argument("--max-execution-time", type=int, default=3600) - agent_create_parser.add_argument("--max-cost-budget", type=float, default=0.0) - agent_create_parser.set_defaults(handler=ctx.handle_agent_action) - - agent_execute_parser = agent_subparsers.add_parser("execute", help="Execute an agent workflow") - agent_execute_parser.add_argument("--name", required=True) - agent_execute_parser.add_argument("--input-data") - agent_execute_parser.add_argument("--wallet") - agent_execute_parser.add_argument("--priority", choices=["low", "medium", "high"], default="medium") - agent_execute_parser.set_defaults(handler=ctx.handle_agent_action) - - agent_status_parser = agent_subparsers.add_parser("status", help="Show agent status") - agent_status_parser.add_argument("--name") - agent_status_parser.add_argument("--execution-id") - agent_status_parser.set_defaults(handler=ctx.handle_agent_action) - - agent_list_parser = agent_subparsers.add_parser("list", help="List agents") - agent_list_parser.add_argument("--status", choices=["active", "completed", "failed"]) - agent_list_parser.set_defaults(handler=ctx.handle_agent_action) - - agent_message_parser = agent_subparsers.add_parser("message", help="Send message to agent") - agent_message_parser.add_argument("--agent", required=True) - agent_message_parser.add_argument("--message", required=True) - agent_message_parser.add_argument("--wallet", required=True) - agent_message_parser.add_argument("--password") - agent_message_parser.add_argument("--password-file") - agent_message_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - agent_message_parser.set_defaults(handler=ctx.handle_agent_action, agent_action="message") - - agent_messages_parser = agent_subparsers.add_parser("messages", help="List agent messages") - agent_messages_parser.add_argument("--agent", required=True) - agent_messages_parser.add_argument("--wallet") - agent_messages_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - agent_messages_parser.set_defaults(handler=ctx.handle_agent_action, agent_action="messages") - - # Agent SDK commands for lifecycle management - agent_sdk_parser = agent_subparsers.add_parser("sdk", help="Agent SDK lifecycle management") - agent_sdk_subparsers = agent_sdk_parser.add_subparsers(dest="agent_sdk_action") - - # agent sdk create - agent_sdk_create_parser = agent_sdk_subparsers.add_parser("create", help="Create a new agent using Agent SDK") - agent_sdk_create_parser.add_argument("--name", required=True, help="Agent name") - agent_sdk_create_parser.add_argument("--workflow", help="Agent workflow type") - agent_sdk_create_parser.add_argument("--type", choices=["provider", "consumer", "general"], default="provider", help="Agent type") - agent_sdk_create_parser.add_argument("--compute-type", default="inference", help="Compute type") - agent_sdk_create_parser.add_argument("--gpu-memory", type=int, help="GPU memory in GB") - agent_sdk_create_parser.add_argument("--models", help="Comma-separated supported models") - agent_sdk_create_parser.add_argument("--performance", type=float, default=0.8, help="Performance score") - agent_sdk_create_parser.add_argument("--max-jobs", type=int, default=1, help="Max concurrent jobs") - agent_sdk_create_parser.add_argument("--specialization", help="Agent specialization") - agent_sdk_create_parser.add_argument("--coordinator-url", help="Coordinator URL") - agent_sdk_create_parser.add_argument("--auto-detect", action="store_true", help="Auto-detect capabilities") - agent_sdk_create_parser.set_defaults(handler=ctx.handle_agent_sdk_action, agent_sdk_action="create") - - # agent sdk register - agent_sdk_register_parser = agent_sdk_subparsers.add_parser("register", help="Register agent with coordinator") - agent_sdk_register_parser.add_argument("--agent-id", required=True, help="Agent ID") - agent_sdk_register_parser.add_argument("--type", choices=["provider", "consumer", "general", "worker"], default="worker", help="Agent type") - agent_sdk_register_parser.add_argument("--capabilities", help="Comma-separated agent capabilities") - agent_sdk_register_parser.add_argument("--services", help="Comma-separated available services") - agent_sdk_register_parser.add_argument("--endpoints", help="JSON string of service endpoints") - agent_sdk_register_parser.add_argument("--metadata", help="JSON string of metadata") - agent_sdk_register_parser.add_argument("--coordinator-url", default="http://localhost:9001", help="Coordinator URL") - agent_sdk_register_parser.set_defaults(handler=ctx.handle_agent_sdk_action, agent_sdk_action="register") - - # agent sdk list - agent_sdk_list_parser = agent_sdk_subparsers.add_parser("list", help="List local agents") - agent_sdk_list_parser.add_argument("--agent-dir", help="Agent directory path") - agent_sdk_list_parser.set_defaults(handler=ctx.handle_agent_sdk_action, agent_sdk_action="list") - - # agent sdk status - agent_sdk_status_parser = agent_sdk_subparsers.add_parser("status", help="Get agent status") - agent_sdk_status_parser.add_argument("--agent-id", required=True, help="Agent ID") - agent_sdk_status_parser.add_argument("--coordinator-url", default="http://localhost:9001", help="Coordinator URL") - agent_sdk_status_parser.set_defaults(handler=ctx.handle_agent_sdk_action, agent_sdk_action="status") - - # agent sdk update-status - agent_sdk_update_status_parser = agent_sdk_subparsers.add_parser("update-status", help="Update agent status") - agent_sdk_update_status_parser.add_argument("--agent-id", required=True, help="Agent ID") - agent_sdk_update_status_parser.add_argument("--status", required=True, help="New status (active, inactive, busy)") - agent_sdk_update_status_parser.add_argument("--load-metrics", help="JSON string of load metrics") - agent_sdk_update_status_parser.add_argument("--coordinator-url", default="http://localhost:9001", help="Coordinator URL") - agent_sdk_update_status_parser.set_defaults(handler=ctx.handle_agent_sdk_action, agent_sdk_action="update-status") - - # agent sdk capabilities - agent_sdk_caps_parser = agent_sdk_subparsers.add_parser("capabilities", help="Show system capabilities") - agent_sdk_caps_parser.set_defaults(handler=ctx.handle_agent_sdk_action, agent_sdk_action="capabilities") - - # agent sdk config-set - agent_sdk_config_set_parser = agent_sdk_subparsers.add_parser("config-set", help="Set agent configuration value") - agent_sdk_config_set_parser.add_argument("--name", required=True, help="Agent name") - agent_sdk_config_set_parser.add_argument("--key", required=True, help="Configuration key") - agent_sdk_config_set_parser.add_argument("--value", required=True, help="Configuration value") - agent_sdk_config_set_parser.set_defaults(handler=ctx.handle_agent_sdk_action, agent_sdk_action="config_set") - - # agent sdk config-get - agent_sdk_config_get_parser = agent_sdk_subparsers.add_parser("config-get", help="Get agent configuration") - agent_sdk_config_get_parser.add_argument("--name", required=True, help="Agent name") - agent_sdk_config_get_parser.add_argument("--key", help="Specific configuration key") - agent_sdk_config_get_parser.set_defaults(handler=ctx.handle_agent_sdk_action, agent_sdk_action="config_get") - - # agent sdk config-validate - agent_sdk_config_validate_parser = agent_sdk_subparsers.add_parser("config-validate", help="Validate agent configuration") - agent_sdk_config_validate_parser.add_argument("--name", required=True, help="Agent name") - agent_sdk_config_validate_parser.set_defaults(handler=ctx.handle_agent_sdk_action, agent_sdk_action="config_validate") - - # agent sdk config-import - agent_sdk_config_import_parser = agent_sdk_subparsers.add_parser("config-import", help="Import agent configuration from file") - agent_sdk_config_import_parser.add_argument("--file", required=True, help="Configuration file path") - agent_sdk_config_import_parser.add_argument("--name", help="Override agent name") - agent_sdk_config_import_parser.set_defaults(handler=ctx.handle_agent_sdk_action, agent_sdk_action="config_import") - - # agent sdk config-export - agent_sdk_config_export_parser = agent_sdk_subparsers.add_parser("config-export", help="Export agent configuration to file") - agent_sdk_config_export_parser.add_argument("--name", required=True, help="Agent name") - agent_sdk_config_export_parser.add_argument("--output", required=True, help="Output file path") - agent_sdk_config_export_parser.set_defaults(handler=ctx.handle_agent_sdk_action, agent_sdk_action="config_export") diff --git a/cli/src/aitbc_cli/parsers/ai.py b/cli/src/aitbc_cli/parsers/ai.py deleted file mode 100644 index 80407f82..00000000 --- a/cli/src/aitbc_cli/parsers/ai.py +++ /dev/null @@ -1,84 +0,0 @@ -"""AI command registration for the unified CLI.""" - -import argparse - -from parser_context import ParserContext - - -def register(subparsers: argparse._SubParsersAction, ctx: ParserContext) -> None: - ai_parser = subparsers.add_parser("ai", help="AI job submission and inspection") - ai_parser.set_defaults(handler=lambda parsed, parser=ai_parser: parser.print_help()) - ai_subparsers = ai_parser.add_subparsers(dest="ai_action") - - ai_submit_parser = ai_subparsers.add_parser("submit", help="Submit an AI job") - ai_submit_parser.add_argument("wallet_name", nargs="?") - ai_submit_parser.add_argument("job_type_arg", nargs="?") - ai_submit_parser.add_argument("prompt_arg", nargs="?") - ai_submit_parser.add_argument("payment_arg", nargs="?") - ai_submit_parser.add_argument("--wallet") - ai_submit_parser.add_argument("--type", dest="job_type") - ai_submit_parser.add_argument("--prompt") - ai_submit_parser.add_argument("--payment", type=float) - ai_submit_parser.add_argument("--password") - ai_submit_parser.add_argument("--password-file") - ai_submit_parser.add_argument("--chain-id", help="Chain ID") - ai_submit_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - ai_submit_parser.add_argument("--coordinator-url", default=ctx.default_coordinator_url) - ai_submit_parser.set_defaults(handler=ctx.handle_ai_submit) - - ai_jobs_parser = ai_subparsers.add_parser("jobs", help="List AI jobs") - ai_jobs_parser.add_argument("--limit", type=int, default=10) - ai_jobs_parser.add_argument("--chain-id", help="Chain ID") - ai_jobs_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - ai_jobs_parser.add_argument("--coordinator-url", default=ctx.default_coordinator_url) - ai_jobs_parser.set_defaults(handler=ctx.handle_ai_jobs) - - ai_status_parser = ai_subparsers.add_parser("status", help="Show AI job status") - ai_status_parser.add_argument("job_id_arg", nargs="?") - ai_status_parser.add_argument("--job-id", dest="job_id") - ai_status_parser.add_argument("--wallet") - ai_status_parser.add_argument("--chain-id", help="Chain ID") - ai_status_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - ai_status_parser.add_argument("--coordinator-url", default=ctx.default_coordinator_url) - ai_status_parser.set_defaults(handler=ctx.handle_ai_status) - - ai_service_parser = ai_subparsers.add_parser("service", help="AI service management") - ai_service_subparsers = ai_service_parser.add_subparsers(dest="ai_service_action") - - ai_service_list_parser = ai_service_subparsers.add_parser("list", help="List available AI services") - ai_service_list_parser.set_defaults(handler=ctx.handle_ai_service_list) - - ai_service_status_parser = ai_service_subparsers.add_parser("status", help="Check AI service status") - ai_service_status_parser.add_argument("--name", help="Service name to check") - ai_service_status_parser.set_defaults(handler=ctx.handle_ai_service_status) - - ai_service_test_parser = ai_service_subparsers.add_parser("test", help="Test AI service endpoint") - ai_service_test_parser.add_argument("--name", help="Service name to test") - ai_service_test_parser.set_defaults(handler=ctx.handle_ai_service_test) - - ai_results_parser = ai_subparsers.add_parser("results", help="Show AI job results") - ai_results_parser.add_argument("job_id_arg", nargs="?") - ai_results_parser.add_argument("--job-id", dest="job_id") - ai_results_parser.add_argument("--wallet") - ai_results_parser.add_argument("--chain-id", help="Chain ID") - ai_results_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - ai_results_parser.set_defaults(handler=ctx.handle_ai_job) # Reuse job handler - - ai_cancel_parser = ai_subparsers.add_parser("cancel", help="Cancel AI job") - ai_cancel_parser.add_argument("job_id_arg", nargs="?") - ai_cancel_parser.add_argument("--job-id", dest="job_id") - ai_cancel_parser.add_argument("--wallet", required=True) - ai_cancel_parser.add_argument("--password") - ai_cancel_parser.add_argument("--password-file") - ai_cancel_parser.add_argument("--chain-id", help="Chain ID") - ai_cancel_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - ai_cancel_parser.set_defaults(handler=ctx.handle_ai_cancel) - - ai_stats_parser = ai_subparsers.add_parser("stats", help="AI service statistics") - ai_stats_parser.add_argument("--chain-id", help="Chain ID") - ai_stats_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - ai_stats_parser.set_defaults(handler=ctx.handle_ai_stats) - - ai_distribution_stats_parser = ai_subparsers.add_parser("distribution-stats", help="Task distribution statistics from agent coordinator") - ai_distribution_stats_parser.add_argument("--coordinator-url", default=ctx.default_coordinator_url) - ai_distribution_stats_parser.set_defaults(handler=ctx.handle_ai_distribution_stats) diff --git a/cli/src/aitbc_cli/parsers/analytics.py b/cli/src/aitbc_cli/parsers/analytics.py deleted file mode 100644 index 6531e576..00000000 --- a/cli/src/aitbc_cli/parsers/analytics.py +++ /dev/null @@ -1,45 +0,0 @@ -"""Analytics command registration for the unified CLI.""" - -import argparse - -from parser_context import ParserContext - - -def register(subparsers: argparse._SubParsersAction, ctx: ParserContext) -> None: - analytics_parser = subparsers.add_parser("analytics", help="Blockchain analytics and statistics") - analytics_parser.set_defaults(handler=lambda parsed, parser=analytics_parser: parser.print_help()) - analytics_subparsers = analytics_parser.add_subparsers(dest="analytics_action") - - analytics_blocks_parser = analytics_subparsers.add_parser("blocks", help="Block analytics") - analytics_blocks_parser.add_argument("--limit", type=int, default=10) - analytics_blocks_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - analytics_blocks_parser.set_defaults(handler=ctx.handle_analytics_metrics) - - analytics_metrics_parser = analytics_subparsers.add_parser("metrics", help="Show performance metrics") - analytics_metrics_parser.add_argument("--limit", type=int, default=10) - analytics_metrics_parser.add_argument("--period", default="24h") - analytics_metrics_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - analytics_metrics_parser.set_defaults(handler=ctx.handle_analytics_metrics) - - analytics_report_parser = analytics_subparsers.add_parser("report", help="Generate analytics report") - analytics_report_parser.add_argument("--type", dest="report_type", choices=["performance", "transactions", "all"], default="all") - analytics_report_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - analytics_report_parser.set_defaults(handler=ctx.handle_analytics_report) - - analytics_export_parser = analytics_subparsers.add_parser("export", help="Export analytics data") - analytics_export_parser.add_argument("--format", choices=["json", "csv"], default="json") - analytics_export_parser.add_argument("--output") - analytics_export_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - analytics_export_parser.set_defaults(handler=ctx.handle_analytics_export) - - analytics_predict_parser = analytics_subparsers.add_parser("predict", help="Run predictive analytics") - analytics_predict_parser.add_argument("--model", default="lstm") - analytics_predict_parser.add_argument("--target", default="job-completion") - analytics_predict_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - analytics_predict_parser.set_defaults(handler=ctx.handle_analytics_predict) - - analytics_optimize_parser = analytics_subparsers.add_parser("optimize", help="Optimize system parameters") - analytics_optimize_parser.add_argument("--parameters", action="store_true") - analytics_optimize_parser.add_argument("--target", default="efficiency") - analytics_optimize_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - analytics_optimize_parser.set_defaults(handler=ctx.handle_analytics_optimize) diff --git a/cli/src/aitbc_cli/parsers/blockchain.py b/cli/src/aitbc_cli/parsers/blockchain.py deleted file mode 100644 index 27d6d2df..00000000 --- a/cli/src/aitbc_cli/parsers/blockchain.py +++ /dev/null @@ -1,84 +0,0 @@ -"""Blockchain command registration for the unified CLI.""" - -import argparse - -from parser_context import ParserContext - - -def register(subparsers: argparse._SubParsersAction, ctx: ParserContext) -> None: - blockchain_parser = subparsers.add_parser("blockchain", help="Blockchain state and block inspection") - blockchain_parser.set_defaults(handler=ctx.handle_blockchain_info, rpc_url=ctx.default_rpc_url) - blockchain_subparsers = blockchain_parser.add_subparsers(dest="blockchain_action") - - blockchain_info_parser = blockchain_subparsers.add_parser("info", help="Show chain information") - blockchain_info_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - blockchain_info_parser.set_defaults(handler=ctx.handle_blockchain_info) - - blockchain_height_parser = blockchain_subparsers.add_parser("height", help="Show current height") - blockchain_height_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - blockchain_height_parser.set_defaults(handler=ctx.handle_blockchain_height) - - blockchain_block_parser = blockchain_subparsers.add_parser("block", help="Inspect a block") - blockchain_block_parser.add_argument("number", nargs="?", type=int) - blockchain_block_parser.add_argument("--chain-id", help="Chain ID for the block") - blockchain_block_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - blockchain_block_parser.set_defaults(handler=ctx.handle_blockchain_block) - - blockchain_init_parser = blockchain_subparsers.add_parser("init", help="Initialize blockchain with genesis block") - blockchain_init_parser.add_argument("--force", action="store_true", help="Force reinitialization") - blockchain_init_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - blockchain_init_parser.set_defaults(handler=ctx.handle_blockchain_init) - - blockchain_genesis_parser = blockchain_subparsers.add_parser("genesis", help="Create or inspect genesis block") - blockchain_genesis_parser.add_argument("--create", action="store_true", help="Create new genesis block") - blockchain_genesis_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - blockchain_genesis_parser.set_defaults(handler=ctx.handle_blockchain_genesis) - - blockchain_import_parser = blockchain_subparsers.add_parser("import", help="Import a block") - blockchain_import_parser.add_argument("--file", help="Block data file") - blockchain_import_parser.add_argument("--json", help="Block data as JSON string") - blockchain_import_parser.add_argument("--chain-id", help="Chain ID for the block") - blockchain_import_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - blockchain_import_parser.set_defaults(handler=ctx.handle_blockchain_import) - - blockchain_export_parser = blockchain_subparsers.add_parser("export", help="Export full chain") - blockchain_export_parser.add_argument("--output", help="Output file") - blockchain_export_parser.add_argument("--chain-id", help="Chain ID to export") - blockchain_export_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - blockchain_export_parser.set_defaults(handler=ctx.handle_blockchain_export) - - blockchain_import_chain_parser = blockchain_subparsers.add_parser("import-chain", help="Import chain state") - blockchain_import_chain_parser.add_argument("--file", required=True, help="Chain state file") - blockchain_import_chain_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - blockchain_import_chain_parser.set_defaults(handler=ctx.handle_blockchain_import_chain) - - blockchain_blocks_range_parser = blockchain_subparsers.add_parser("blocks-range", help="Get blocks in height range") - blockchain_blocks_range_parser.add_argument("--start", type=int, help="Start height") - blockchain_blocks_range_parser.add_argument("--end", type=int, help="End height") - blockchain_blocks_range_parser.add_argument("--limit", type=int, default=10, help="Limit number of blocks") - blockchain_blocks_range_parser.add_argument("--chain-id", help="Chain ID") - blockchain_blocks_range_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - blockchain_blocks_range_parser.set_defaults(handler=ctx.handle_blockchain_blocks_range) - - account_parser = subparsers.add_parser("account", help="Account information") - account_parser.set_defaults(handler=lambda parsed, parser=account_parser: parser.print_help()) - account_subparsers = account_parser.add_subparsers(dest="account_action") - - account_get_parser = account_subparsers.add_parser("get", help="Get account information") - account_get_parser.add_argument("--address", required=True, help="Account address") - account_get_parser.add_argument("--chain-id", help="Chain ID") - account_get_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - account_get_parser.set_defaults(handler=ctx.handle_account_get) - - blockchain_transactions_parser = blockchain_subparsers.add_parser("transactions", help="Query transactions") - blockchain_transactions_parser.add_argument("--address", help="Filter by address") - blockchain_transactions_parser.add_argument("--limit", type=int, default=10) - blockchain_transactions_parser.add_argument("--offset", type=int, default=0) - blockchain_transactions_parser.add_argument("--chain-id", help="Chain ID") - blockchain_transactions_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - blockchain_transactions_parser.set_defaults(handler=ctx.handle_blockchain_transactions) - - blockchain_mempool_parser = blockchain_subparsers.add_parser("mempool", help="Get pending transactions") - blockchain_mempool_parser.add_argument("--chain-id", help="Chain ID") - blockchain_mempool_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - blockchain_mempool_parser.set_defaults(handler=ctx.handle_blockchain_mempool) diff --git a/cli/src/aitbc_cli/parsers/bridge.py b/cli/src/aitbc_cli/parsers/bridge.py deleted file mode 100644 index ea6ad408..00000000 --- a/cli/src/aitbc_cli/parsers/bridge.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Blockchain event bridge command registration for the unified CLI.""" - -import argparse - -from parser_context import ParserContext - - -def register(subparsers: argparse._SubParsersAction, ctx: ParserContext) -> None: - bridge_parser = subparsers.add_parser("bridge", help="Blockchain event bridge management") - bridge_parser.set_defaults(handler=lambda parsed, parser=bridge_parser: parser.print_help()) - bridge_subparsers = bridge_parser.add_subparsers(dest="bridge_action") - - bridge_health_parser = bridge_subparsers.add_parser("health", help="Health check for blockchain event bridge service") - bridge_health_parser.add_argument("--test-mode", action="store_true") - bridge_health_parser.set_defaults(handler=ctx.handle_bridge_health) - - bridge_metrics_parser = bridge_subparsers.add_parser("metrics", help="Get Prometheus metrics from blockchain event bridge service") - bridge_metrics_parser.add_argument("--test-mode", action="store_true") - bridge_metrics_parser.set_defaults(handler=ctx.handle_bridge_metrics) - - bridge_status_parser = bridge_subparsers.add_parser("status", help="Get detailed status of blockchain event bridge service") - bridge_status_parser.add_argument("--test-mode", action="store_true") - bridge_status_parser.set_defaults(handler=ctx.handle_bridge_status) - - bridge_config_parser = bridge_subparsers.add_parser("config", help="Show current configuration of blockchain event bridge service") - bridge_config_parser.add_argument("--test-mode", action="store_true") - bridge_config_parser.set_defaults(handler=ctx.handle_bridge_config) - - bridge_restart_parser = bridge_subparsers.add_parser("restart", help="Restart blockchain event bridge service (via systemd)") - bridge_restart_parser.add_argument("--test-mode", action="store_true") - bridge_restart_parser.set_defaults(handler=ctx.handle_bridge_restart) diff --git a/cli/src/aitbc_cli/parsers/contract.py b/cli/src/aitbc_cli/parsers/contract.py deleted file mode 100644 index 8ba3ca8f..00000000 --- a/cli/src/aitbc_cli/parsers/contract.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Contract command registration for the unified CLI.""" - -import argparse - -from parser_context import ParserContext - - -def register(subparsers: argparse._SubParsersAction, ctx: ParserContext) -> None: - contract_parser = subparsers.add_parser("contract", help="Smart contract operations") - contract_parser.set_defaults(handler=lambda parsed, parser=contract_parser: parser.print_help()) - contract_subparsers = contract_parser.add_subparsers(dest="contract_action") - - contract_list_parser = contract_subparsers.add_parser("list", help="List deployed contracts") - contract_list_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - contract_list_parser.set_defaults(handler=ctx.handle_contract_list) - - contract_deploy_parser = contract_subparsers.add_parser("deploy", help="Deploy a smart contract") - contract_deploy_parser.add_argument("--name", required=True, help="Contract name") - contract_deploy_parser.add_argument("--type", default="zk-verifier", help="Contract type (default: zk-verifier)") - contract_deploy_parser.add_argument("--password", help="Wallet password") - contract_deploy_parser.add_argument("--password-file", help="Wallet password file") - contract_deploy_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - contract_deploy_parser.set_defaults(handler=ctx.handle_contract_deploy) - - contract_call_parser = contract_subparsers.add_parser("call", help="Call a contract method") - contract_call_parser.add_argument("--address", required=True, help="Contract address") - contract_call_parser.add_argument("--method", required=True, help="Method name") - contract_call_parser.add_argument("--params", help="Method parameters (JSON)") - contract_call_parser.add_argument("--password", help="Wallet password") - contract_call_parser.add_argument("--password-file", help="Wallet password file") - contract_call_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - contract_call_parser.set_defaults(handler=ctx.handle_contract_call) - - contract_verify_parser = contract_subparsers.add_parser("verify", help="Verify a ZK proof against a contract") - contract_verify_parser.add_argument("--address", required=True, help="Contract address") - contract_verify_parser.add_argument("--proof-file", help="Proof data file (JSON)") - contract_verify_parser.add_argument("--password", help="Wallet password") - contract_verify_parser.add_argument("--password-file", help="Wallet password file") - contract_verify_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - contract_verify_parser.set_defaults(handler=ctx.handle_contract_verify) diff --git a/cli/src/aitbc_cli/parsers/genesis.py b/cli/src/aitbc_cli/parsers/genesis.py deleted file mode 100644 index 9de908e3..00000000 --- a/cli/src/aitbc_cli/parsers/genesis.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Genesis command registration for the unified CLI.""" - -import argparse - -from parser_context import ParserContext - - -def register(subparsers: argparse._SubParsersAction, ctx: ParserContext) -> None: - genesis_parser = subparsers.add_parser("genesis", help="Genesis block and wallet generation") - genesis_parser.set_defaults(handler=lambda parsed, parser=genesis_parser: parser.print_help()) - genesis_subparsers = genesis_parser.add_subparsers(dest="genesis_action") - - genesis_init_parser = genesis_subparsers.add_parser("init", help="Initialize genesis block and wallet") - genesis_init_parser.add_argument("--chain-id", default="ait-mainnet", help="Chain ID for genesis") - genesis_init_parser.add_argument("--create-wallet", action="store_true", help="Create genesis wallet with secure random key") - genesis_init_parser.add_argument("--password", help="Wallet password (auto-generated if not provided)") - genesis_init_parser.add_argument("--proposer", help="Proposer address (defaults to genesis wallet)") - genesis_init_parser.add_argument("--force", action="store_true", help="Force overwrite existing genesis") - genesis_init_parser.add_argument("--register-service", action="store_true", help="Register genesis wallet with wallet service") - genesis_init_parser.add_argument("--service-url", default="http://localhost:8003", help="Wallet service URL") - genesis_init_parser.set_defaults(handler=ctx.handle_genesis_init) - - genesis_verify_parser = genesis_subparsers.add_parser("verify", help="Verify genesis block and wallet configuration") - genesis_verify_parser.add_argument("--chain-id", default="ait-mainnet", help="Chain ID to verify") - genesis_verify_parser.set_defaults(handler=ctx.handle_genesis_verify) - - genesis_info_parser = genesis_subparsers.add_parser("info", help="Show genesis block information") - genesis_info_parser.add_argument("--chain-id", default="ait-mainnet", help="Chain ID to show info for") - genesis_info_parser.set_defaults(handler=ctx.handle_genesis_info) diff --git a/cli/src/aitbc_cli/parsers/hermes.py b/cli/src/aitbc_cli/parsers/hermes.py deleted file mode 100644 index 25cac2c7..00000000 --- a/cli/src/aitbc_cli/parsers/hermes.py +++ /dev/null @@ -1,48 +0,0 @@ -"""hermes Agent Training command registration for the unified CLI.""" - -import argparse - -from parser_context import ParserContext - - -def register(subparsers: argparse._SubParsersAction, ctx: ParserContext) -> None: - hermes_training_parser = subparsers.add_parser("hermes-training", help="hermes agent training operations") - hermes_training_parser.set_defaults(handler=lambda parsed, parser=hermes_training_parser: parser.print_help()) - hermes_training_subparsers = hermes_training_parser.add_subparsers(dest="hermes_training_action") - - hermes_deploy_parser = hermes_training_subparsers.add_parser("deploy", help="Deploy an hermes agent") - hermes_deploy_parser.add_argument("--agent-file", required=True) - hermes_deploy_parser.add_argument("--wallet", required=True) - hermes_deploy_parser.add_argument("--environment", choices=["dev", "staging", "prod"], default="dev") - hermes_deploy_parser.set_defaults(handler=ctx.handle_hermes_training_action) - - hermes_monitor_parser = hermes_training_subparsers.add_parser("monitor", help="Monitor hermes performance") - hermes_monitor_parser.add_argument("--agent-id") - hermes_monitor_parser.add_argument("--metrics", choices=["performance", "cost", "errors", "all"], default="all") - hermes_monitor_parser.set_defaults(handler=ctx.handle_hermes_training_action) - - hermes_market_parser = hermes_training_subparsers.add_parser("market", help="Manage hermes marketplace activity") - hermes_market_parser.add_argument("market_action", nargs="?", choices=["list", "publish", "purchase", "evaluate"]) - hermes_market_parser.add_argument("--action", dest="market_action_opt", choices=["list", "publish", "purchase", "evaluate"], help=argparse.SUPPRESS) - hermes_market_parser.add_argument("--agent-id") - hermes_market_parser.add_argument("--price", type=float) - hermes_market_parser.set_defaults(handler=ctx.handle_hermes_training_action, hermes_training_action="market") - - hermes_train_parser = hermes_training_subparsers.add_parser("train", help="Agent training operations") - hermes_train_subparsers = hermes_train_parser.add_subparsers(dest="train_action") - - hermes_train_agent_parser = hermes_train_subparsers.add_parser("agent", help="Train hermes agent on AITBC operations") - hermes_train_agent_parser.add_argument("--agent-id", required=True, help="Agent ID to train") - hermes_train_agent_parser.add_argument("--stage", required=True, help="Training stage (stage1_foundation, stage2_operations_mastery, etc.)") - hermes_train_agent_parser.add_argument("--training-data", required=True, help="Path to training data JSON file") - hermes_train_agent_parser.add_argument("--log-level", default="INFO", choices=["DEBUG", "INFO", "SUCCESS", "WARNING", "ERROR"], help="Logging level") - hermes_train_agent_parser.set_defaults(handler=ctx.handle_hermes_training_action, hermes_training_action="train") - - hermes_train_validate_parser = hermes_train_subparsers.add_parser("validate", help="Validate agent training progress") - hermes_train_validate_parser.add_argument("--agent-id", required=True, help="Agent ID to validate") - hermes_train_validate_parser.add_argument("--stage", required=True, help="Training stage to validate") - hermes_train_validate_parser.set_defaults(handler=ctx.handle_hermes_training_action, hermes_training_action="train") - - hermes_train_certify_parser = hermes_train_subparsers.add_parser("certify", help="Certify agent mastery") - hermes_train_certify_parser.add_argument("--agent-id", required=True, help="Agent ID to certify") - hermes_train_certify_parser.set_defaults(handler=ctx.handle_hermes_training_action, hermes_training_action="train") diff --git a/cli/src/aitbc_cli/parsers/market.py b/cli/src/aitbc_cli/parsers/market.py deleted file mode 100644 index 5cca60f8..00000000 --- a/cli/src/aitbc_cli/parsers/market.py +++ /dev/null @@ -1,111 +0,0 @@ -"""Marketplace command registration for the unified CLI.""" - -import argparse - -from parser_context import ParserContext - - -def register(subparsers: argparse._SubParsersAction, ctx: ParserContext) -> None: - market_parser = subparsers.add_parser("market", help="Marketplace listings and offers") - market_parser.set_defaults(handler=lambda parsed, parser=market_parser: parser.print_help()) - market_subparsers = market_parser.add_subparsers(dest="market_action") - - # GPU marketplace subcommands - market_gpu_parser = market_subparsers.add_parser("gpu", help="GPU marketplace operations") - market_gpu_parser.set_defaults(handler=lambda parsed, parser=market_gpu_parser: parser.print_help()) - market_gpu_subparsers = market_gpu_parser.add_subparsers(dest="gpu_action") - - market_gpu_register_parser = market_gpu_subparsers.add_parser("register", help="Register GPU on marketplace") - market_gpu_register_parser.add_argument("--name", help="GPU name/model") - market_gpu_register_parser.add_argument("--memory", type=int, help="GPU memory in GB") - market_gpu_register_parser.add_argument("--cuda-cores", type=int, help="Number of CUDA cores") - market_gpu_register_parser.add_argument("--compute-capability", help="Compute capability (e.g., 8.9)") - market_gpu_register_parser.add_argument("--price-per-hour", type=float, required=True, help="Price per hour in AIT") - market_gpu_register_parser.add_argument("--description", help="GPU description") - market_gpu_register_parser.add_argument("--miner-id", help="Miner ID") - market_gpu_register_parser.add_argument("--force", action="store_true", help="Force registration without hardware validation") - market_gpu_register_parser.add_argument("--coordinator-url", default=ctx.default_coordinator_url) - market_gpu_register_parser.set_defaults(handler=ctx.handle_market_gpu_register) - - market_gpu_list_parser = market_gpu_subparsers.add_parser("list", help="List available GPUs") - market_gpu_list_parser.add_argument("--available", action="store_true", help="Show only available GPUs") - market_gpu_list_parser.add_argument("--price-max", type=float, help="Maximum price per hour") - market_gpu_list_parser.add_argument("--region", help="Filter by region") - market_gpu_list_parser.add_argument("--model", help="Filter by GPU model") - market_gpu_list_parser.add_argument("--limit", type=int, default=100, help="Maximum number of results") - market_gpu_list_parser.add_argument("--coordinator-url", default=ctx.default_coordinator_url) - market_gpu_list_parser.set_defaults(handler=ctx.handle_market_gpu_list) - - market_list_parser = market_subparsers.add_parser("list", help="List marketplace items") - market_list_parser.add_argument("--chain-id", help="Chain ID") - market_list_parser.add_argument("--coordinator-url", default=ctx.default_coordinator_url) - market_list_parser.add_argument("--marketplace-url") - market_list_parser.set_defaults(handler=ctx.handle_market_listings) - - market_create_parser = market_subparsers.add_parser("create", help="Create a marketplace listing") - market_create_parser.add_argument("--wallet", required=True) - market_create_parser.add_argument("--type", dest="item_type", required=True) - market_create_parser.add_argument("--price", type=float, required=True) - market_create_parser.add_argument("--description") - market_create_parser.add_argument("--password") - market_create_parser.add_argument("--password-file") - market_create_parser.add_argument("--chain-id", help="Chain ID") - market_create_parser.add_argument("--coordinator-url", default=ctx.default_coordinator_url) - market_create_parser.add_argument("--marketplace-url") - market_create_parser.set_defaults(handler=ctx.handle_market_create) - - market_search_parser = market_subparsers.add_parser("search", help="Search marketplace items") - market_search_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - market_search_parser.set_defaults(handler=ctx.handle_market_listings) # Reuse listings for now - - market_mine_parser = market_subparsers.add_parser("my-listings", help="Show your marketplace listings") - market_mine_parser.add_argument("--wallet") - market_mine_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - market_mine_parser.set_defaults(handler=ctx.handle_market_listings) # Reuse listings for now - - market_get_parser = market_subparsers.add_parser("get", help="Get listing by ID") - market_get_parser.add_argument("--listing-id", required=True) - market_get_parser.add_argument("--chain-id", help="Chain ID") - market_get_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - market_get_parser.add_argument("--marketplace-url") - market_get_parser.set_defaults(handler=ctx.handle_market_get) - - market_delete_parser = market_subparsers.add_parser("delete", help="Delete listing") - market_delete_parser.add_argument("--listing-id") - market_delete_parser.add_argument("--order") - market_delete_parser.add_argument("--wallet") - market_delete_parser.add_argument("--password") - market_delete_parser.add_argument("--password-file") - market_delete_parser.add_argument("--chain-id", help="Chain ID") - market_delete_parser.add_argument("--coordinator-url", default=ctx.default_coordinator_url) - market_delete_parser.add_argument("--marketplace-url") - market_delete_parser.set_defaults(handler=ctx.handle_market_delete) - - market_buy_parser = market_subparsers.add_parser("buy", help="Buy from marketplace") - market_buy_parser.add_argument("--item", required=True) - market_buy_parser.add_argument("--price", type=float) - market_buy_parser.add_argument("--wallet", required=True) - market_buy_parser.add_argument("--password") - market_buy_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - market_buy_parser.add_argument("--marketplace-url") - market_buy_parser.set_defaults(handler=ctx.handle_market_buy) - - market_sell_parser = market_subparsers.add_parser("sell", help="Sell on marketplace") - market_sell_parser.add_argument("--item", required=True) - market_sell_parser.add_argument("--price", type=float, required=True) - market_sell_parser.add_argument("--wallet", required=True) - market_sell_parser.add_argument("--password") - market_sell_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - market_sell_parser.add_argument("--marketplace-url") - market_sell_parser.set_defaults(handler=ctx.handle_market_sell) - - market_orders_parser = market_subparsers.add_parser("orders", help="Show marketplace orders") - market_orders_parser.add_argument("--wallet") - market_orders_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - market_orders_parser.add_argument("--marketplace-url") - market_orders_parser.set_defaults(handler=ctx.handle_market_orders) - - market_plugins_parser = market_subparsers.add_parser("list-plugin", help="List marketplace plugins") - market_plugins_parser.add_argument("--coordinator-url", default=ctx.default_coordinator_url) - market_plugins_parser.add_argument("--marketplace-url") - market_plugins_parser.set_defaults(handler=ctx.handle_market_list_plugins) diff --git a/cli/src/aitbc_cli/parsers/messaging.py b/cli/src/aitbc_cli/parsers/messaging.py deleted file mode 100644 index 05f33785..00000000 --- a/cli/src/aitbc_cli/parsers/messaging.py +++ /dev/null @@ -1,84 +0,0 @@ -"""Messaging command registration for the unified CLI.""" - -import argparse - -from parser_context import ParserContext - - -def register(subparsers: argparse._SubParsersAction, ctx: ParserContext) -> None: - messaging_parser = subparsers.add_parser("messaging", help="Messaging system and forum") - messaging_parser.set_defaults(handler=lambda parsed, parser=messaging_parser: parser.print_help()) - messaging_subparsers = messaging_parser.add_subparsers(dest="messaging_action") - - messaging_deploy_parser = messaging_subparsers.add_parser("deploy", help="Deploy messaging contract") - messaging_deploy_parser.add_argument("--chain-id", help="Chain ID") - messaging_deploy_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - messaging_deploy_parser.set_defaults(handler=ctx.handle_messaging_deploy) - - messaging_state_parser = messaging_subparsers.add_parser("state", help="Get contract state") - messaging_state_parser.add_argument("--chain-id", help="Chain ID") - messaging_state_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - messaging_state_parser.set_defaults(handler=ctx.handle_messaging_state) - - messaging_topics_parser = messaging_subparsers.add_parser("topics", help="List forum topics") - messaging_topics_parser.add_argument("--chain-id", help="Chain ID") - messaging_topics_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - messaging_topics_parser.set_defaults(handler=ctx.handle_messaging_topics) - - messaging_create_topic_parser = messaging_subparsers.add_parser("create-topic", help="Create forum topic") - messaging_create_topic_parser.add_argument("--title", required=True, help="Topic title") - messaging_create_topic_parser.add_argument("--content", required=True, help="Topic content") - messaging_create_topic_parser.add_argument("--wallet", help="Wallet address for authentication") - messaging_create_topic_parser.add_argument("--password") - messaging_create_topic_parser.add_argument("--password-file") - messaging_create_topic_parser.add_argument("--chain-id", help="Chain ID") - messaging_create_topic_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - messaging_create_topic_parser.set_defaults(handler=ctx.handle_messaging_create_topic) - - messaging_messages_parser = messaging_subparsers.add_parser("messages", help="Get topic messages") - messaging_messages_parser.add_argument("--topic-id", required=True, help="Topic ID") - messaging_messages_parser.add_argument("--chain-id", help="Chain ID") - messaging_messages_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - messaging_messages_parser.set_defaults(handler=ctx.handle_messaging_messages) - - messaging_post_parser = messaging_subparsers.add_parser("post", help="Post message") - messaging_post_parser.add_argument("--topic-id", required=True, help="Topic ID") - messaging_post_parser.add_argument("--content", required=True, help="Message content") - messaging_post_parser.add_argument("--wallet", help="Wallet address for authentication") - messaging_post_parser.add_argument("--password") - messaging_post_parser.add_argument("--password-file") - messaging_post_parser.add_argument("--chain-id", help="Chain ID") - messaging_post_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - messaging_post_parser.set_defaults(handler=ctx.handle_messaging_post) - - messaging_vote_parser = messaging_subparsers.add_parser("vote", help="Vote on message") - messaging_vote_parser.add_argument("--message-id", required=True, help="Message ID") - messaging_vote_parser.add_argument("--vote", required=True, help="Vote (up/down)") - messaging_vote_parser.add_argument("--wallet", help="Wallet address for authentication") - messaging_vote_parser.add_argument("--password") - messaging_vote_parser.add_argument("--password-file") - messaging_vote_parser.add_argument("--chain-id", help="Chain ID") - messaging_vote_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - messaging_vote_parser.set_defaults(handler=ctx.handle_messaging_vote) - - messaging_search_parser = messaging_subparsers.add_parser("search", help="Search messages") - messaging_search_parser.add_argument("--query", required=True, help="Search query") - messaging_search_parser.add_argument("--chain-id", help="Chain ID") - messaging_search_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - messaging_search_parser.set_defaults(handler=ctx.handle_messaging_search) - - messaging_reputation_parser = messaging_subparsers.add_parser("reputation", help="Get agent reputation") - messaging_reputation_parser.add_argument("--agent-id", required=True, help="Agent ID") - messaging_reputation_parser.add_argument("--chain-id", help="Chain ID") - messaging_reputation_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - messaging_reputation_parser.set_defaults(handler=ctx.handle_messaging_reputation) - - messaging_moderate_parser = messaging_subparsers.add_parser("moderate", help="Moderate message") - messaging_moderate_parser.add_argument("--message-id", required=True, help="Message ID") - messaging_moderate_parser.add_argument("--action", required=True, help="Action (approve/reject)") - messaging_moderate_parser.add_argument("--wallet", help="Wallet address for authentication") - messaging_moderate_parser.add_argument("--password") - messaging_moderate_parser.add_argument("--password-file") - messaging_moderate_parser.add_argument("--chain-id", help="Chain ID") - messaging_moderate_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - messaging_moderate_parser.set_defaults(handler=ctx.handle_messaging_moderate) diff --git a/cli/src/aitbc_cli/parsers/network.py b/cli/src/aitbc_cli/parsers/network.py deleted file mode 100644 index a14dc13b..00000000 --- a/cli/src/aitbc_cli/parsers/network.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Network command registration for the unified CLI.""" - -import argparse - -from parser_context import ParserContext - - -def register(subparsers: argparse._SubParsersAction, ctx: ParserContext) -> None: - network_parser = subparsers.add_parser("network", help="Peer connectivity and sync") - network_parser.set_defaults(handler=ctx.handle_network_status) - network_subparsers = network_parser.add_subparsers(dest="network_action") - - network_status_parser = network_subparsers.add_parser("status", help="Show network status") - network_status_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - network_status_parser.set_defaults(handler=ctx.handle_network_status) - - network_peers_parser = network_subparsers.add_parser("peers", help="List peers") - network_peers_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - network_peers_parser.set_defaults(handler=ctx.handle_network_peers) - - network_sync_parser = network_subparsers.add_parser("sync", help="Show sync status") - network_sync_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - network_sync_parser.set_defaults(handler=ctx.handle_network_sync) - - network_ping_parser = network_subparsers.add_parser("ping", help="Ping a node") - network_ping_parser.add_argument("node", nargs="?") - network_ping_parser.add_argument("--node", dest="node_opt", help=argparse.SUPPRESS) - network_ping_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - network_ping_parser.set_defaults(handler=ctx.handle_network_ping) - - network_propagate_parser = network_subparsers.add_parser("propagate", help="Propagate test data") - network_propagate_parser.add_argument("data", nargs="?") - network_propagate_parser.add_argument("--data", dest="data_opt", help=argparse.SUPPRESS) - network_propagate_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - network_propagate_parser.set_defaults(handler=ctx.handle_network_propagate) - - network_force_sync_parser = network_subparsers.add_parser("force-sync", help="Force reorg to specified peer") - network_force_sync_parser.add_argument("--peer", required=True, help="Peer to sync from") - network_force_sync_parser.add_argument("--chain-id", help="Chain ID") - network_force_sync_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - network_force_sync_parser.set_defaults(handler=ctx.handle_network_force_sync) diff --git a/cli/src/aitbc_cli/parsers/performance.py b/cli/src/aitbc_cli/parsers/performance.py deleted file mode 100644 index e1813aa2..00000000 --- a/cli/src/aitbc_cli/parsers/performance.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Performance command registration for the unified CLI.""" - -import argparse - -from parser_context import ParserContext - - -def register(subparsers: argparse._SubParsersAction, ctx: ParserContext) -> None: - performance_parser = subparsers.add_parser("performance", help="Performance optimization and monitoring") - performance_parser.set_defaults(handler=lambda parsed, parser=performance_parser: parser.print_help()) - performance_subparsers = performance_parser.add_subparsers(dest="performance_action") - - performance_benchmark_parser = performance_subparsers.add_parser("benchmark", help="Run performance benchmark") - performance_benchmark_parser.add_argument("--target") - performance_benchmark_parser.set_defaults(handler=ctx.handle_performance_benchmark) - - performance_optimize_parser = performance_subparsers.add_parser("optimize", help="Optimize performance") - performance_optimize_parser.add_argument("--target", default="general") - performance_optimize_parser.set_defaults(handler=ctx.handle_performance_optimize) - - performance_tune_parser = performance_subparsers.add_parser("tune", help="Tune system parameters") - performance_tune_parser.add_argument("--aggressive", action="store_true") - performance_tune_parser.add_argument("--parameters", action="store_true") - performance_tune_parser.set_defaults(handler=ctx.handle_performance_tune) diff --git a/cli/src/aitbc_cli/parsers/pool_hub.py b/cli/src/aitbc_cli/parsers/pool_hub.py deleted file mode 100644 index 61459e12..00000000 --- a/cli/src/aitbc_cli/parsers/pool_hub.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Pool hub command registration for the unified CLI.""" - -import argparse - -from parser_context import ParserContext - - -def register(subparsers: argparse._SubParsersAction, ctx: ParserContext) -> None: - pool_hub_parser = subparsers.add_parser("pool-hub", help="Pool hub management for SLA monitoring and billing") - pool_hub_parser.set_defaults(handler=lambda parsed, parser=pool_hub_parser: parser.print_help()) - pool_hub_subparsers = pool_hub_parser.add_subparsers(dest="pool_hub_action") - - pool_hub_sla_metrics_parser = pool_hub_subparsers.add_parser("sla-metrics", help="Get SLA metrics for miner or all miners") - pool_hub_sla_metrics_parser.add_argument("miner_id", nargs="?") - pool_hub_sla_metrics_parser.add_argument("--test-mode", action="store_true") - pool_hub_sla_metrics_parser.set_defaults(handler=ctx.handle_pool_hub_sla_metrics) - - pool_hub_sla_violations_parser = pool_hub_subparsers.add_parser("sla-violations", help="Get SLA violations") - pool_hub_sla_violations_parser.add_argument("--test-mode", action="store_true") - pool_hub_sla_violations_parser.set_defaults(handler=ctx.handle_pool_hub_sla_violations) - - pool_hub_capacity_snapshots_parser = pool_hub_subparsers.add_parser("capacity-snapshots", help="Get capacity planning snapshots") - pool_hub_capacity_snapshots_parser.add_argument("--test-mode", action="store_true") - pool_hub_capacity_snapshots_parser.set_defaults(handler=ctx.handle_pool_hub_capacity_snapshots) - - pool_hub_capacity_forecast_parser = pool_hub_subparsers.add_parser("capacity-forecast", help="Get capacity forecast") - pool_hub_capacity_forecast_parser.add_argument("--test-mode", action="store_true") - pool_hub_capacity_forecast_parser.set_defaults(handler=ctx.handle_pool_hub_capacity_forecast) - - pool_hub_capacity_recommendations_parser = pool_hub_subparsers.add_parser("capacity-recommendations", help="Get scaling recommendations") - pool_hub_capacity_recommendations_parser.add_argument("--test-mode", action="store_true") - pool_hub_capacity_recommendations_parser.set_defaults(handler=ctx.handle_pool_hub_capacity_recommendations) - - pool_hub_billing_usage_parser = pool_hub_subparsers.add_parser("billing-usage", help="Get billing usage data") - pool_hub_billing_usage_parser.add_argument("--test-mode", action="store_true") - pool_hub_billing_usage_parser.set_defaults(handler=ctx.handle_pool_hub_billing_usage) - - pool_hub_billing_sync_parser = pool_hub_subparsers.add_parser("billing-sync", help="Trigger billing sync with coordinator-api") - pool_hub_billing_sync_parser.add_argument("--test-mode", action="store_true") - pool_hub_billing_sync_parser.set_defaults(handler=ctx.handle_pool_hub_billing_sync) - - pool_hub_collect_metrics_parser = pool_hub_subparsers.add_parser("collect-metrics", help="Trigger SLA metrics collection") - pool_hub_collect_metrics_parser.add_argument("--test-mode", action="store_true") - pool_hub_collect_metrics_parser.set_defaults(handler=ctx.handle_pool_hub_collect_metrics) diff --git a/cli/src/aitbc_cli/parsers/resource.py b/cli/src/aitbc_cli/parsers/resource.py deleted file mode 100644 index 17286e86..00000000 --- a/cli/src/aitbc_cli/parsers/resource.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Resource command registration for the unified CLI.""" - -import argparse - -from parser_context import ParserContext - - -def register(subparsers: argparse._SubParsersAction, ctx: ParserContext) -> None: - resource_parser = subparsers.add_parser("resource", help="Resource utilization and allocation") - resource_parser.set_defaults(handler=lambda parsed, parser=resource_parser: parser.print_help()) - resource_subparsers = resource_parser.add_subparsers(dest="resource_action") - - resource_status_parser = resource_subparsers.add_parser("status", help="Show resource status") - resource_status_parser.add_argument("--type", choices=["cpu", "memory", "storage", "network", "all"], default="all") - resource_status_parser.set_defaults(handler=ctx.handle_resource_status) - - resource_allocate_parser = resource_subparsers.add_parser("allocate", help="Allocate resources") - resource_allocate_parser.add_argument("--agent-id", required=True) - resource_allocate_parser.add_argument("--cpu", type=float) - resource_allocate_parser.add_argument("--memory", type=int) - resource_allocate_parser.add_argument("--duration", type=int) - resource_allocate_parser.set_defaults(handler=ctx.handle_resource_allocate) - - resource_optimize_parser = resource_subparsers.add_parser("optimize", help="Optimize resource usage") - resource_optimize_parser.add_argument("--agent-id") - resource_optimize_parser.add_argument("--target", choices=["cpu", "memory", "all"], default="all") - resource_optimize_parser.set_defaults(handler=ctx.handle_resource_optimize) - - resource_benchmark_parser = resource_subparsers.add_parser("benchmark", help="Run resource benchmark") - resource_benchmark_parser.add_argument("--type", choices=["cpu", "memory", "io", "all"], default="all") - resource_benchmark_parser.set_defaults(handler=ctx.handle_resource_benchmark) - - resource_monitor_parser = resource_subparsers.add_parser("monitor", help="Monitor resource utilization") - resource_monitor_parser.add_argument("--interval", type=int, default=5, help="Monitoring interval in seconds") - resource_monitor_parser.add_argument("--duration", type=int, default=60, help="Monitoring duration in seconds") - resource_monitor_parser.set_defaults(handler=ctx.handle_resource_monitor) diff --git a/cli/src/aitbc_cli/parsers/script.py b/cli/src/aitbc_cli/parsers/script.py deleted file mode 100644 index e3b25709..00000000 --- a/cli/src/aitbc_cli/parsers/script.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Script command registration for the unified CLI.""" - -import argparse - -from parser_context import ParserContext - - -def register(subparsers: argparse._SubParsersAction, ctx: ParserContext) -> None: - script_parser = subparsers.add_parser("script", help="Script execution and automation") - script_parser.add_argument("--run", action="store_true", help="Run a script file") - script_parser.add_argument("--file", help="Script file to execute") - script_parser.add_argument("--args", help="Arguments to pass to script") - script_parser.set_defaults(handler=ctx.handle_script_run) diff --git a/cli/src/aitbc_cli/parsers/system.py b/cli/src/aitbc_cli/parsers/system.py deleted file mode 100644 index a379c11e..00000000 --- a/cli/src/aitbc_cli/parsers/system.py +++ /dev/null @@ -1,162 +0,0 @@ -"""System, analytics, security, compliance, simulation, and cluster command registration for the unified CLI.""" - -import argparse - -from parser_context import ParserContext - - -def register(subparsers: argparse._SubParsersAction, ctx: ParserContext) -> None: - mining_parser = subparsers.add_parser("mining", help="Mining lifecycle and rewards") - mining_parser.set_defaults(handler=ctx.handle_mining_action, mining_action="status") - mining_subparsers = mining_parser.add_subparsers(dest="mining_action") - - mining_status_parser = mining_subparsers.add_parser("status", help="Show mining status") - mining_status_parser.add_argument("--wallet") - mining_status_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - mining_status_parser.set_defaults(handler=ctx.handle_mining_action, mining_action="status") - - mining_start_parser = mining_subparsers.add_parser("start", help="Start mining") - mining_start_parser.add_argument("--wallet") - mining_start_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - mining_start_parser.set_defaults(handler=ctx.handle_mining_action, mining_action="start") - - mining_stop_parser = mining_subparsers.add_parser("stop", help="Stop mining") - mining_stop_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - mining_stop_parser.set_defaults(handler=ctx.handle_mining_action, mining_action="stop") - - mining_rewards_parser = mining_subparsers.add_parser("rewards", help="Show mining rewards") - mining_rewards_parser.add_argument("--wallet") - mining_rewards_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - mining_rewards_parser.set_defaults(handler=ctx.handle_mining_action, mining_action="rewards") - - system_parser = subparsers.add_parser("system", help="System health and overview") - system_parser.set_defaults(handler=ctx.handle_system_status) - system_subparsers = system_parser.add_subparsers(dest="system_action") - - system_status_parser = system_subparsers.add_parser("status", help="Show system status") - system_status_parser.set_defaults(handler=ctx.handle_system_status) - - economics_parser = subparsers.add_parser("economics", help="Economic intelligence and modeling") - economics_parser.set_defaults(handler=lambda parsed, parser=economics_parser: parser.print_help()) - economics_subparsers = economics_parser.add_subparsers(dest="economics_action") - - economics_distributed_parser = economics_subparsers.add_parser("distributed", help="Distributed cost optimization") - economics_distributed_parser.add_argument("--cost-optimize", action="store_true") - economics_distributed_parser.set_defaults(handler=ctx.handle_economics_action) - - economics_model_parser = economics_subparsers.add_parser("model", help="Economic modeling") - economics_model_parser.add_argument("--type", default="cost-optimization") - economics_model_parser.set_defaults(handler=ctx.handle_economics_action) - - economics_market_parser = economics_subparsers.add_parser("market", help="Market analysis") - economics_market_parser.add_argument("--analyze", action="store_true") - economics_market_parser.set_defaults(handler=ctx.handle_economics_action) - - economics_trends_parser = economics_subparsers.add_parser("trends", help="Economic trends analysis") - economics_trends_parser.add_argument("--period") - economics_trends_parser.set_defaults(handler=ctx.handle_economics_action) - - economics_optimize_parser = economics_subparsers.add_parser("optimize", help="Optimize economic strategy") - economics_optimize_parser.add_argument("--target", choices=["revenue", "cost", "all"], default="all") - economics_optimize_parser.set_defaults(handler=ctx.handle_economics_action) - - economics_strategy_parser = economics_subparsers.add_parser("strategy", help="Global economic strategy") - economics_strategy_parser.add_argument("--optimize", action="store_true") - economics_strategy_parser.add_argument("--global", dest="global_strategy", action="store_true") - economics_strategy_parser.set_defaults(handler=ctx.handle_economics_action) - cluster_parser = subparsers.add_parser("cluster", help="Cluster management") - cluster_parser.set_defaults(handler=lambda parsed, parser=cluster_parser: parser.print_help()) - cluster_subparsers = cluster_parser.add_subparsers(dest="cluster_action") - - cluster_status_parser = cluster_subparsers.add_parser("status", help="Show cluster status") - cluster_status_parser.add_argument("--nodes", nargs="*", default=["aitbc", "aitbc1"]) - cluster_status_parser.set_defaults(handler=ctx.handle_cluster_status) - - cluster_sync_parser = cluster_subparsers.add_parser("sync", help="Sync cluster nodes") - cluster_sync_parser.add_argument("--all", action="store_true") - cluster_sync_parser.set_defaults(handler=ctx.handle_cluster_sync) - - cluster_balance_parser = cluster_subparsers.add_parser("balance", help="Balance workload across nodes") - cluster_balance_parser.add_argument("--workload", action="store_true") - cluster_balance_parser.set_defaults(handler=ctx.handle_cluster_balance) - - performance_parser = subparsers.add_parser("performance", help="Performance optimization") - performance_parser.set_defaults(handler=lambda parsed, parser=performance_parser: parser.print_help()) - performance_subparsers = performance_parser.add_subparsers(dest="performance_action") - - performance_benchmark_parser = performance_subparsers.add_parser("benchmark", help="Run performance benchmark") - performance_benchmark_parser.add_argument("--suite", choices=["comprehensive", "quick", "custom"], default="comprehensive") - performance_benchmark_parser.set_defaults(handler=ctx.handle_performance_benchmark) - - performance_optimize_parser = performance_subparsers.add_parser("optimize", help="Optimize performance") - performance_optimize_parser.add_argument("--target", choices=["latency", "throughput", "all"], default="all") - performance_optimize_parser.set_defaults(handler=ctx.handle_performance_optimize) - - performance_tune_parser = performance_subparsers.add_parser("tune", help="Tune system parameters") - performance_tune_parser.add_argument("--parameters", action="store_true") - performance_tune_parser.add_argument("--aggressive", action="store_true") - performance_tune_parser.set_defaults(handler=ctx.handle_performance_tune) - - security_parser = subparsers.add_parser("security", help="Security audit and scanning") - security_parser.set_defaults(handler=lambda parsed, parser=security_parser: parser.print_help()) - security_subparsers = security_parser.add_subparsers(dest="security_action") - - security_audit_parser = security_subparsers.add_parser("audit", help="Run security audit") - security_audit_parser.add_argument("--comprehensive", action="store_true") - security_audit_parser.set_defaults(handler=ctx.handle_security_action) - - security_scan_parser = security_subparsers.add_parser("scan", help="Scan for vulnerabilities") - security_scan_parser.add_argument("--vulnerabilities", action="store_true") - security_scan_parser.set_defaults(handler=ctx.handle_security_action) - - security_patch_parser = security_subparsers.add_parser("patch", help="Check for security patches") - security_patch_parser.add_argument("--critical", action="store_true") - security_patch_parser.set_defaults(handler=ctx.handle_security_action) - - compliance_parser = subparsers.add_parser("compliance", help="Compliance checking and reporting") - compliance_parser.set_defaults(handler=lambda parsed, parser=compliance_parser: parser.print_help()) - compliance_subparsers = compliance_parser.add_subparsers(dest="compliance_action") - - compliance_check_parser = compliance_subparsers.add_parser("check", help="Check compliance status") - compliance_check_parser.add_argument("--standard", choices=["gdpr", "hipaa", "soc2", "all"], default="gdpr") - compliance_check_parser.set_defaults(handler=ctx.handle_compliance_check) - - compliance_report_parser = compliance_subparsers.add_parser("report", help="Generate compliance report") - compliance_report_parser.add_argument("--format", choices=["detailed", "summary", "json"], default="detailed") - compliance_report_parser.set_defaults(handler=ctx.handle_compliance_report) - - simulate_parser = subparsers.add_parser("simulate", help="Simulation utilities") - simulate_parser.set_defaults(handler=lambda parsed, parser=simulate_parser: parser.print_help()) - simulate_subparsers = simulate_parser.add_subparsers(dest="simulate_command") - - simulate_blockchain_parser = simulate_subparsers.add_parser("blockchain", help="Simulate blockchain activity") - simulate_blockchain_parser.add_argument("--blocks", type=int, default=10) - simulate_blockchain_parser.add_argument("--transactions", type=int, default=50) - simulate_blockchain_parser.add_argument("--delay", type=float, default=1.0) - simulate_blockchain_parser.set_defaults(handler=ctx.handle_simulate_action) - - simulate_wallets_parser = simulate_subparsers.add_parser("wallets", help="Simulate wallet activity") - simulate_wallets_parser.add_argument("--wallets", type=int, default=5) - simulate_wallets_parser.add_argument("--balance", type=float, default=1000.0) - simulate_wallets_parser.add_argument("--transactions", type=int, default=20) - simulate_wallets_parser.add_argument("--amount-range", default="1.0-100.0") - simulate_wallets_parser.set_defaults(handler=ctx.handle_simulate_action) - - simulate_price_parser = simulate_subparsers.add_parser("price", help="Simulate price movement") - simulate_price_parser.add_argument("--price", type=float, default=100.0) - simulate_price_parser.add_argument("--volatility", type=float, default=0.05) - simulate_price_parser.add_argument("--timesteps", type=int, default=100) - simulate_price_parser.add_argument("--delay", type=float, default=0.1) - simulate_price_parser.set_defaults(handler=ctx.handle_simulate_action) - - simulate_network_parser = simulate_subparsers.add_parser("network", help="Simulate network topology") - simulate_network_parser.add_argument("--nodes", type=int, default=3) - simulate_network_parser.add_argument("--network-delay", type=float, default=0.1) - simulate_network_parser.add_argument("--failure-rate", type=float, default=0.05) - simulate_network_parser.set_defaults(handler=ctx.handle_simulate_action) - - simulate_ai_jobs_parser = simulate_subparsers.add_parser("ai-jobs", help="Simulate AI job traffic") - simulate_ai_jobs_parser.add_argument("--jobs", type=int, default=10) - simulate_ai_jobs_parser.add_argument("--models", default="text-generation") - simulate_ai_jobs_parser.add_argument("--duration-range", default="30-300") - simulate_ai_jobs_parser.set_defaults(handler=ctx.handle_simulate_action) diff --git a/cli/src/aitbc_cli/parsers/wallet.py b/cli/src/aitbc_cli/parsers/wallet.py deleted file mode 100644 index 5f4c8ba0..00000000 --- a/cli/src/aitbc_cli/parsers/wallet.py +++ /dev/null @@ -1,102 +0,0 @@ -"""Wallet command registration for the unified CLI.""" - -import argparse - -from parser_context import ParserContext - - -def register(subparsers: argparse._SubParsersAction, ctx: ParserContext) -> None: - wallet_parser = subparsers.add_parser("wallet", help="Wallet lifecycle, balances, and transactions") - wallet_parser.set_defaults(handler=lambda parsed, parser=wallet_parser: parser.print_help()) - wallet_subparsers = wallet_parser.add_subparsers(dest="wallet_action") - - wallet_create_parser = wallet_subparsers.add_parser("create", help="Create a wallet") - wallet_create_parser.add_argument("wallet_name", nargs="?") - wallet_create_parser.add_argument("wallet_password", nargs="?") - wallet_create_parser.add_argument("--name", dest="wallet_name_opt", help=argparse.SUPPRESS) - wallet_create_parser.add_argument("--password") - wallet_create_parser.add_argument("--password-file") - wallet_create_parser.set_defaults(handler=ctx.handle_wallet_create) - - wallet_list_parser = wallet_subparsers.add_parser("list", help="List wallets") - wallet_list_parser.add_argument("--format", choices=["table", "json"], default="table") - wallet_list_parser.set_defaults(handler=ctx.handle_wallet_list) - - wallet_balance_parser = wallet_subparsers.add_parser("balance", help="Show wallet balance") - wallet_balance_parser.add_argument("wallet_name", nargs="?") - wallet_balance_parser.add_argument("--name", dest="wallet_name_opt", help=argparse.SUPPRESS) - wallet_balance_parser.add_argument("--all", action="store_true") - wallet_balance_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - wallet_balance_parser.add_argument("--chain-id", help="Chain ID for multichain operations (e.g., ait-mainnet, ait-devnet)") - wallet_balance_parser.set_defaults(handler=ctx.handle_wallet_balance) - - wallet_transactions_parser = wallet_subparsers.add_parser("transactions", help="Show wallet transactions") - wallet_transactions_parser.add_argument("wallet_name", nargs="?") - wallet_transactions_parser.add_argument("--name", dest="wallet_name_opt", help=argparse.SUPPRESS) - wallet_transactions_parser.add_argument("--limit", type=int, default=10) - wallet_transactions_parser.add_argument("--format", choices=["table", "json"], default="table") - wallet_transactions_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - wallet_transactions_parser.set_defaults(handler=ctx.handle_wallet_transactions) - - wallet_send_parser = wallet_subparsers.add_parser("send", help="Send AIT") - wallet_send_parser.add_argument("from_wallet_arg", nargs="?") - wallet_send_parser.add_argument("to_address_arg", nargs="?") - wallet_send_parser.add_argument("amount_arg", nargs="?") - wallet_send_parser.add_argument("wallet_password", nargs="?") - wallet_send_parser.add_argument("--from", dest="from_wallet", help=argparse.SUPPRESS) - wallet_send_parser.add_argument("--to", dest="to_address", help=argparse.SUPPRESS) - wallet_send_parser.add_argument("--amount", type=float) - wallet_send_parser.add_argument("--fee", type=float, default=10.0) - wallet_send_parser.add_argument("--password") - wallet_send_parser.add_argument("--password-file") - wallet_send_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - wallet_send_parser.set_defaults(handler=ctx.handle_wallet_send) - - wallet_import_parser = wallet_subparsers.add_parser("import", help="Import a wallet") - wallet_import_parser.add_argument("wallet_name", nargs="?") - wallet_import_parser.add_argument("private_key_arg", nargs="?") - wallet_import_parser.add_argument("wallet_password", nargs="?") - wallet_import_parser.add_argument("--name", dest="wallet_name_opt", help=argparse.SUPPRESS) - wallet_import_parser.add_argument("--private-key", dest="private_key_opt") - wallet_import_parser.add_argument("--password") - wallet_import_parser.add_argument("--password-file") - wallet_import_parser.set_defaults(handler=ctx.handle_wallet_import) - - wallet_export_parser = wallet_subparsers.add_parser("export", help="Export a wallet") - wallet_export_parser.add_argument("wallet_name", nargs="?") - wallet_export_parser.add_argument("wallet_password", nargs="?") - wallet_export_parser.add_argument("--name", dest="wallet_name_opt", help=argparse.SUPPRESS) - wallet_export_parser.add_argument("--password") - wallet_export_parser.add_argument("--password-file") - wallet_export_parser.set_defaults(handler=ctx.handle_wallet_export) - - wallet_delete_parser = wallet_subparsers.add_parser("delete", help="Delete a wallet") - wallet_delete_parser.add_argument("wallet_name", nargs="?") - wallet_delete_parser.add_argument("--name", dest="wallet_name_opt", help=argparse.SUPPRESS) - wallet_delete_parser.add_argument("--confirm", action="store_true") - wallet_delete_parser.set_defaults(handler=ctx.handle_wallet_delete) - - wallet_rename_parser = wallet_subparsers.add_parser("rename", help="Rename a wallet") - wallet_rename_parser.add_argument("old_name_arg", nargs="?") - wallet_rename_parser.add_argument("new_name_arg", nargs="?") - wallet_rename_parser.add_argument("--old", dest="old_name", help=argparse.SUPPRESS) - wallet_rename_parser.add_argument("--new", dest="new_name", help=argparse.SUPPRESS) - wallet_rename_parser.set_defaults(handler=ctx.handle_wallet_rename) - - wallet_backup_parser = wallet_subparsers.add_parser("backup", help="Backup a wallet") - wallet_backup_parser.add_argument("wallet_name", nargs="?") - wallet_backup_parser.add_argument("--name", dest="wallet_name_opt", help=argparse.SUPPRESS) - wallet_backup_parser.set_defaults(handler=ctx.handle_wallet_backup) - - wallet_sync_parser = wallet_subparsers.add_parser("sync", help="Sync wallets") - wallet_sync_parser.add_argument("wallet_name", nargs="?") - wallet_sync_parser.add_argument("--name", dest="wallet_name_opt", help=argparse.SUPPRESS) - wallet_sync_parser.add_argument("--all", action="store_true") - wallet_sync_parser.set_defaults(handler=ctx.handle_wallet_sync) - - wallet_batch_parser = wallet_subparsers.add_parser("batch", help="Send multiple transactions") - wallet_batch_parser.add_argument("--file", required=True) - wallet_batch_parser.add_argument("--password") - wallet_batch_parser.add_argument("--password-file") - wallet_batch_parser.add_argument("--rpc-url", default=ctx.default_rpc_url) - wallet_batch_parser.set_defaults(handler=ctx.handle_wallet_batch) diff --git a/cli/src/aitbc_cli/parsers/workflow.py b/cli/src/aitbc_cli/parsers/workflow.py deleted file mode 100644 index bf9800e2..00000000 --- a/cli/src/aitbc_cli/parsers/workflow.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Workflow command registration for the unified CLI.""" - -import argparse - -from parser_context import ParserContext - - -def register(subparsers: argparse._SubParsersAction, ctx: ParserContext) -> None: - workflow_parser = subparsers.add_parser("workflow", help="Workflow templates and execution") - workflow_parser.set_defaults(handler=lambda parsed, parser=workflow_parser: parser.print_help()) - workflow_subparsers = workflow_parser.add_subparsers(dest="workflow_action") - - workflow_create_parser = workflow_subparsers.add_parser("create", help="Create a workflow") - workflow_create_parser.add_argument("--name", required=True) - workflow_create_parser.add_argument("--template") - workflow_create_parser.add_argument("--config-file") - workflow_create_parser.add_argument("--steps", type=int, default=5) - workflow_create_parser.set_defaults(handler=ctx.handle_workflow_create) - - workflow_run_parser = workflow_subparsers.add_parser("run", help="Run a workflow") - workflow_run_parser.add_argument("--name", required=True) - workflow_run_parser.add_argument("--params") - workflow_run_parser.add_argument("--async-exec", action="store_true") - workflow_run_parser.set_defaults(handler=ctx.handle_workflow_action) - - workflow_schedule_parser = workflow_subparsers.add_parser("schedule", help="Schedule a workflow") - workflow_schedule_parser.add_argument("--name") - workflow_schedule_parser.add_argument("--cron", required=True) - workflow_schedule_parser.add_argument("--command") - workflow_schedule_parser.add_argument("--params") - workflow_schedule_parser.set_defaults(handler=ctx.handle_workflow_schedule) - - workflow_monitor_parser = workflow_subparsers.add_parser("monitor", help="Monitor workflow execution") - workflow_monitor_parser.add_argument("--name") - workflow_monitor_parser.add_argument("--execution-id") - workflow_monitor_parser.set_defaults(handler=ctx.handle_workflow_monitor) diff --git a/cli/src/aitbc_cli/utils/__init__.py b/cli/src/aitbc_cli/utils/__init__.py deleted file mode 100644 index ddb39537..00000000 --- a/cli/src/aitbc_cli/utils/__init__.py +++ /dev/null @@ -1,97 +0,0 @@ -""" -CLI utility functions for output formatting and error handling -""" - -import base64 -import logging - -from click import echo, secho - -# Import new utility modules -from . import wallet -from . import blockchain -from . import chain_id -from . import island_credentials -from .wallet import decrypt_private_key -from .blockchain import get_chain_info, get_network_status, get_blockchain_analytics - - -def output(message, format=None, title=None, **kwargs): - """Print a regular output message (handles strings and structured data)""" - if not isinstance(message, str): - import json - if format == 'json' or format == 'yaml': - message = json.dumps(message, indent=2) - else: - # Table format — just JSON for now - message = json.dumps(message, indent=2) - if title: - echo(f"\n{title}") - echo("=" * len(title)) - echo(message, **kwargs) - - -def error(message: str, **kwargs): - """Print an error message in red""" - secho(message, fg="red", **kwargs) - - -def success(message: str, **kwargs): - """Print a success message in green""" - secho(message, fg="green", **kwargs) - - -def info(message: str, **kwargs): - """Print an info message in blue""" - secho(message, fg="blue", **kwargs) - - -def warning(message: str, **kwargs): - """Print a warning message in yellow""" - secho(message, fg="yellow", **kwargs) - - -def encrypt_value(value: str, key: str = None) -> str: - """Lightweight reversible encoding used for CLI compatibility.""" - return base64.b64encode(value.encode("utf-8")).decode("ascii") - - -def decrypt_value(encrypted: str, key: str = None) -> str: - """Reverse the lightweight compatibility encoding.""" - return base64.b64decode(encrypted.encode("ascii")).decode("utf-8") - - -def setup_logging(verbosity: int, debug: bool = False) -> str: - """Configure basic CLI logging for compatibility with the generated entrypoint.""" - if debug or verbosity >= 2: - level = logging.DEBUG - level_name = "DEBUG" - elif verbosity == 1: - level = logging.INFO - level_name = "INFO" - else: - level = logging.WARNING - level_name = "WARNING" - - logging.basicConfig(level=level, format="%(message)s") - return level_name - - -__all__ = [ - 'output', - 'error', - 'success', - 'info', - 'warning', - 'encrypt_value', - 'decrypt_value', - 'setup_logging', - 'wallet', - 'blockchain', - 'chain_id', - 'island_credentials', - 'decrypt_private_key', - 'get_chain_info', - 'get_network_status', - 'get_blockchain_analytics', -] diff --git a/cli/src/aitbc_cli/utils/blockchain.py b/cli/src/aitbc_cli/utils/blockchain.py deleted file mode 100644 index 07b24217..00000000 --- a/cli/src/aitbc_cli/utils/blockchain.py +++ /dev/null @@ -1,95 +0,0 @@ -""" -Blockchain utility functions for AITBC CLI -""" - -from typing import Optional, Dict -import logging - -from aitbc import AITBCHTTPClient, NetworkError - -logger = logging.getLogger(__name__) - - -def get_chain_info(rpc_url: str = "http://localhost:8006") -> Optional[Dict]: - """Get blockchain information""" - try: - result = {} - # Get chain metadata from health endpoint - http_client = AITBCHTTPClient(base_url=rpc_url, timeout=30) - health = http_client.get("/health") - chains = health.get('supported_chains', []) - result['chain_id'] = chains[0] if chains else 'ait-mainnet' - result['supported_chains'] = ', '.join(chains) if chains else 'ait-mainnet' - result['proposer_id'] = health.get('proposer_id', '') - # Get head block for height - head = http_client.get("/rpc/head") - result['height'] = head.get('height', 0) - result['hash'] = head.get('hash', "") - result['timestamp'] = head.get('timestamp', 'N/A') - result['tx_count'] = head.get('tx_count', 0) - return result if result else None - except NetworkError as e: - logger.error(f"Error: {e}") - return None - except Exception as e: - logger.error(f"Error: {e}") - return None - - -def get_network_status(rpc_url: str = "http://localhost:8006") -> Optional[Dict]: - """Get network status and health""" - try: - # Get head block - http_client = AITBCHTTPClient(base_url=rpc_url, timeout=30) - return http_client.get("/rpc/head") - except NetworkError as e: - logger.error(f"Error getting network status: {e}") - return None - except Exception as e: - logger.error(f"Error: {e}") - return None - - -def get_blockchain_analytics(analytics_type: str, limit: int = 10, rpc_url: str = "http://localhost:8006") -> Optional[Dict]: - """Get blockchain analytics and statistics""" - try: - if analytics_type == "blocks": - # Get recent blocks analytics - http_client = AITBCHTTPClient(base_url=rpc_url, timeout=30) - head = http_client.get("/rpc/head") - return { - "type": "blocks", - "current_height": head.get("height", 0), - "latest_block": head.get("hash", ""), - "timestamp": head.get("timestamp", ""), - "tx_count": head.get("tx_count", 0), - "status": "Active" - } - - elif analytics_type == "supply": - # Get total supply info - return { - "type": "supply", - "total_supply": "1000000000", # From genesis - "circulating_supply": "999997980", # After transactions - "genesis_minted": "1000000000", - "status": "Available" - } - - elif analytics_type == "accounts": - # Account statistics - return { - "type": "accounts", - "total_accounts": 3, # Genesis + treasury + user - "active_accounts": 2, # Accounts with transactions - "genesis_accounts": 2, # Genesis and treasury - "user_accounts": 1, - "status": "Healthy" - } - - else: - return {"type": analytics_type, "status": "Not implemented yet"} - - except Exception as e: - logger.error(f"Error getting analytics: {e}") - return None diff --git a/cli/src/aitbc_cli/utils/chain_id.py b/cli/src/aitbc_cli/utils/chain_id.py deleted file mode 100644 index 96febcc8..00000000 --- a/cli/src/aitbc_cli/utils/chain_id.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Chain ID utilities for AITBC CLI - -This module provides functions for auto-detecting and validating chain IDs -from blockchain nodes, supporting multichain operations. -""" - -from typing import Optional -from aitbc import AITBCHTTPClient, NetworkError - - -# Known chain IDs -KNOWN_CHAINS = ["ait-mainnet", "ait-devnet", "ait-testnet", "ait-healthchain"] - - -def get_default_chain_id() -> str: - """Return the default chain ID (ait-mainnet for production).""" - return "ait-mainnet" - - -def validate_chain_id(chain_id: str) -> bool: - """Validate a chain ID against known chains. - - Args: - chain_id: The chain ID to validate - - Returns: - True if the chain ID is known, False otherwise - """ - return chain_id in KNOWN_CHAINS - - -def get_chain_id_from_health(rpc_url: str, timeout: int = 5) -> str: - """Auto-detect chain ID from blockchain node's /health endpoint. - - Args: - rpc_url: The blockchain node RPC URL (e.g., http://localhost:8006) - timeout: Request timeout in seconds - - Returns: - The detected chain ID, or default if detection fails - """ - try: - http_client = AITBCHTTPClient(base_url=rpc_url, timeout=timeout) - health_data = http_client.get("/health") - supported_chains = health_data.get("supported_chains", []) - - if supported_chains: - # Return the first supported chain (typically the primary chain) - return supported_chains[0] - except NetworkError: - pass - except Exception: - pass - - # Fallback to default if detection fails - return get_default_chain_id() - - -def get_chain_id(rpc_url: str, override: Optional[str] = None, timeout: int = 5) -> str: - """Get chain ID with override support and auto-detection fallback. - - Args: - rpc_url: The blockchain node RPC URL - override: Optional chain ID override (e.g., from --chain-id flag) - timeout: Request timeout in seconds - - Returns: - The chain ID to use (override takes precedence, then auto-detection, then default) - """ - # If override is provided, validate and use it - if override: - if validate_chain_id(override): - return override - # If unknown, still use it (user may be testing new chains) - return override - - # Otherwise, auto-detect from health endpoint - return get_chain_id_from_health(rpc_url, timeout) diff --git a/cli/src/aitbc_cli/utils/island_credentials.py b/cli/src/aitbc_cli/utils/island_credentials.py deleted file mode 100644 index 2f9dd0a3..00000000 --- a/cli/src/aitbc_cli/utils/island_credentials.py +++ /dev/null @@ -1,181 +0,0 @@ -""" -Island Credential Loading Utility -Provides functions to load and validate island credentials from the local filesystem -""" - -import json -import os -from typing import Dict, Optional -from pathlib import Path - - -CREDENTIALS_PATH = '/var/lib/aitbc/island_credentials.json' - - -def load_island_credentials() -> Dict: - """ - Load island credentials from the local filesystem - - Returns: - dict: Island credentials containing island_id, island_name, chain_id, credentials, etc. - - Raises: - FileNotFoundError: If credentials file does not exist - json.JSONDecodeError: If credentials file is invalid JSON - ValueError: If credentials are invalid or missing required fields - """ - credentials_path = Path(CREDENTIALS_PATH) - - if not credentials_path.exists(): - raise FileNotFoundError( - f"Island credentials not found at {CREDENTIALS_PATH}. " - f"Run 'aitbc node island join' to join an island first." - ) - - with open(credentials_path, 'r') as f: - credentials = json.load(f) - - # Validate required fields - required_fields = ['island_id', 'island_name', 'island_chain_id', 'credentials'] - for field in required_fields: - if field not in credentials: - raise ValueError(f"Invalid credentials: missing required field '{field}'") - - return credentials - - -def get_rpc_endpoint() -> str: - """ - Get the RPC endpoint from island credentials - - Returns: - str: RPC endpoint URL - - Raises: - FileNotFoundError: If credentials file does not exist - ValueError: If RPC endpoint is missing from credentials - """ - credentials = load_island_credentials() - rpc_endpoint = credentials.get('credentials', {}).get('rpc_endpoint') - - if not rpc_endpoint: - raise ValueError("RPC endpoint not found in island credentials") - - return rpc_endpoint - - -def get_chain_id() -> str: - """ - Get the chain ID from island credentials - - Returns: - str: Chain ID - - Raises: - FileNotFoundError: If credentials file does not exist - ValueError: If chain ID is missing from credentials - """ - credentials = load_island_credentials() - chain_id = credentials.get('island_chain_id') - - if not chain_id: - raise ValueError("Chain ID not found in island credentials") - - return chain_id - - -def get_island_id() -> str: - """ - Get the island ID from island credentials - - Returns: - str: Island ID - - Raises: - FileNotFoundError: If credentials file does not exist - ValueError: If island ID is missing from credentials - """ - credentials = load_island_credentials() - island_id = credentials.get('island_id') - - if not island_id: - raise ValueError("Island ID not found in island credentials") - - return island_id - - -def get_island_name() -> str: - """ - Get the island name from island credentials - - Returns: - str: Island name - - Raises: - FileNotFoundError: If credentials file does not exist - ValueError: If island name is missing from credentials - """ - credentials = load_island_credentials() - island_name = credentials.get('island_name') - - if not island_name: - raise ValueError("Island name not found in island credentials") - - return island_name - - -def get_genesis_block_hash() -> Optional[str]: - """ - Get the genesis block hash from island credentials - - Returns: - str: Genesis block hash, or None if not available - """ - try: - credentials = load_island_credentials() - return credentials.get('credentials', {}).get('genesis_block_hash') - except (FileNotFoundError, ValueError): - return None - - -def get_genesis_address() -> Optional[str]: - """ - Get the genesis address from island credentials - - Returns: - str: Genesis address, or None if not available - """ - try: - credentials = load_island_credentials() - return credentials.get('credentials', {}).get('genesis_address') - except (FileNotFoundError, ValueError): - return None - - -def validate_credentials() -> bool: - """ - Validate that island credentials exist and are valid - - Returns: - bool: True if credentials are valid, False otherwise - """ - try: - credentials = load_island_credentials() - # Check for essential fields - return all(key in credentials for key in ['island_id', 'island_name', 'island_chain_id', 'credentials']) - except (FileNotFoundError, json.JSONDecodeError, ValueError): - return False - - -def get_p2p_port() -> Optional[int]: - """ - Get the P2P port from island credentials - - Returns: - int: P2P port, or None if not available - """ - try: - credentials = load_island_credentials() - return credentials.get('credentials', {}).get('p2p_port') - except (FileNotFoundError, ValueError): - return None diff --git a/cli/src/aitbc_cli/utils/wallet.py b/cli/src/aitbc_cli/utils/wallet.py deleted file mode 100644 index fa097172..00000000 --- a/cli/src/aitbc_cli/utils/wallet.py +++ /dev/null @@ -1,71 +0,0 @@ -""" -Wallet utility functions for AITBC CLI -""" - -import json -import os -import hashlib -import base64 -from pathlib import Path -from typing import Optional - -from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.ciphers.aead import AESGCM - - -def decrypt_private_key(keystore_path: Path, password: str) -> str: - """Decrypt private key from keystore file. - - Supports both keystore formats: - - AES-256-GCM (blockchain-node standard) - - Fernet (scripts/utils standard) - """ - with open(keystore_path) as f: - ks = json.load(f) - - crypto = ks.get('crypto', ks) # Handle both nested and flat crypto structures - - # Detect encryption method - cipher = crypto.get('cipher', crypto.get('algorithm', '')) - - if cipher == 'aes-256-gcm' or cipher == 'aes-256-gcm': - # AES-256-GCM (blockchain-node standard) - salt = bytes.fromhex(crypto['kdfparams']['salt']) - kdf = PBKDF2HMAC( - algorithm=hashes.SHA256(), - length=32, - salt=salt, - iterations=crypto['kdfparams']['c'], - backend=default_backend() - ) - key = kdf.derive(password.encode()) - aesgcm = AESGCM(key) - nonce = bytes.fromhex(crypto['cipherparams']['nonce']) - priv = aesgcm.decrypt(nonce, bytes.fromhex(crypto['ciphertext']), None) - return priv.hex() - - elif cipher == 'fernet' or cipher == 'PBKDF2-SHA256-Fernet': - # Fernet (scripts/utils standard) - from cryptography.fernet import Fernet - - # Derive Fernet key using the same method as scripts/utils/keystore.py - kdfparams = crypto.get('kdfparams', {}) - if 'salt' in kdfparams: - salt = base64.b64decode(kdfparams['salt']) - else: - # Fallback for older format - salt = bytes.fromhex(kdfparams.get('salt', '')) - - # Use PBKDF2 for secure key derivation (100,000 iterations for security) - dk = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000, dklen=32) - fernet_key = base64.urlsafe_b64encode(dk) - - f = Fernet(fernet_key) - ciphertext = base64.b64decode(crypto['ciphertext']) - priv = f.decrypt(ciphertext) - return priv.decode() - - else: - raise ValueError(f"Unsupported cipher: {cipher}") diff --git a/docs/CLI_PACKAGING_PLAN.md b/docs/CLI_PACKAGING_PLAN.md new file mode 100644 index 00000000..fc57853d --- /dev/null +++ b/docs/CLI_PACKAGING_PLAN.md @@ -0,0 +1,462 @@ +# CLI Packaging Restructuring Plan + +## Objective + +Restructure the AITBC CLI into a proper installable Python package to eliminate sys.path manipulation debt and improve maintainability. + +## Current State + +### Entry Points +- `/opt/aitbc/cli/aitbc_cli.py` - Main entrypoint (symlinked from `/opt/aitbc/aitbc-cli`) +- `/opt/aitbc/cli/click_cli.py` - Legacy Click entrypoint +- `/opt/aitbc/cli/unified_cli.py` - Unified nested command hierarchy +- `/opt/aitbc/cli/miner_cli.py` - Miner-specific CLI +- `/opt/aitbc/cli/variants/main_minimal.py` - Minimal variant + +### Package Structure +``` +cli/ +ā”œā”€ā”€ __init__.py +ā”œā”€ā”€ aitbc_cli.py (main entrypoint) +ā”œā”€ā”€ click_cli.py (legacy) +ā”œā”€ā”€ unified_cli.py (unified) +ā”œā”€ā”€ miner_cli.py (miner-specific) +ā”œā”€ā”€ variants/ +│ └── main_minimal.py +ā”œā”€ā”€ handlers/ +│ └── wallet.py +ā”œā”€ā”€ utils/ +│ ā”œā”€ā”€ wallet_daemon_client.py +│ └── dual_mode_wallet_adapter.py +ā”œā”€ā”€ core/ +│ └── imports.py +ā”œā”€ā”€ aitbc_cli/ +│ ā”œā”€ā”€ __init__.py +│ ā”œā”€ā”€ commands/ +│ │ ā”œā”€ā”€ wallet.py +│ │ ā”œā”€ā”€ blockchain.py +│ │ ā”œā”€ā”€ network.py +│ │ ā”œā”€ā”€ market.py +│ │ ā”œā”€ā”€ ai.py +│ │ ā”œā”€ā”€ mining.py +│ │ ā”œā”€ā”€ system.py +│ │ ā”œā”€ā”€ agent.py +│ │ ā”œā”€ā”€ openclaw.py +│ │ ā”œā”€ā”€ workflow.py +│ │ ā”œā”€ā”€ resource.py +│ │ ā”œā”€ā”€ simulate.py +│ │ ā”œā”€ā”€ node.py +│ │ ā”œā”€ā”€ exchange.py +│ │ └── agent_sdk.py +│ ā”œā”€ā”€ config.py +│ └── utils/ +│ ā”œā”€ā”€ __init__.py +│ ā”œā”€ā”€ output.py +│ ā”œā”€ā”€ error.py +│ ā”œā”€ā”€ success.py +│ └── warning.py +└── build/ + └── lib/ + └── aitbc_cli/ + └── main.py (generated) +``` + +### Issues +1. Multiple entrypoints with overlapping functionality +2. sys.path manipulation in every entrypoint +3. No proper package installation (run directly from source) +4. Generated code in `cli/build/` directory +5. Legacy files (cli/aitbc_cli.legacy.py at 3,257 lines) +6. Mixed import patterns (absolute vs relative) + +## Target State + +### Package Structure +``` +cli/ +ā”œā”€ā”€ pyproject.toml (new) +ā”œā”€ā”€ setup.py (optional, for compatibility) +ā”œā”€ā”€ src/ +│ └── aitbc_cli/ +│ ā”œā”€ā”€ __init__.py +│ ā”œā”€ā”€ main.py (single entrypoint) +│ ā”œā”€ā”€ commands/ +│ │ ā”œā”€ā”€ __init__.py +│ │ ā”œā”€ā”€ wallet.py +│ │ ā”œā”€ā”€ blockchain.py +│ │ ā”œā”€ā”€ network.py +│ │ ā”œā”€ā”€ market.py +│ │ ā”œā”€ā”€ ai.py +│ │ ā”œā”€ā”€ mining.py +│ │ ā”œā”€ā”€ system.py +│ │ ā”œā”€ā”€ agent.py +│ │ ā”œā”€ā”€ openclaw.py +│ │ ā”œā”€ā”€ workflow.py +│ │ ā”œā”€ā”€ resource.py +│ │ ā”œā”€ā”€ simulate.py +│ │ ā”œā”€ā”€ node.py +│ │ ā”œā”€ā”€ exchange.py +│ │ └── agent_sdk.py +│ ā”œā”€ā”€ config.py +│ └── utils/ +│ ā”œā”€ā”€ __init__.py +│ ā”œā”€ā”€ output.py +│ ā”œā”€ā”€ error.py +│ ā”œā”€ā”€ success.py +│ └── warning.py +ā”œā”€ā”€ tests/ (move from root tests/cli/) +└── README.md +``` + +### Entry Points +- Single console script: `aitbc-cli` pointing to `aitbc_cli.main:cli` +- Backward compatibility symlinks for legacy commands + +### Installation +```bash +cd /opt/aitbc/cli +pip install -e . +``` + +## Implementation Plan + +### Phase 1: Create Package Structure + +1. **Create pyproject.toml** + ```toml + [build-system] + requires = ["setuptools>=61.0", "wheel"] + build-backend = "setuptools.build_meta" + + [project] + name = "aitbc-cli" + version = "0.1.0" + description = "AITBC Command Line Interface" + authors = [{name = "AITBC Team"}] + license = {text = "MIT"} + requires-python = ">=3.11" + dependencies = [ + "click>=8.0", + "rich>=13.0", + "PyYAML", + "requests", + "cryptography", + ] + + [project.scripts] + aitbc-cli = "aitbc_cli.main:cli" + + [tool.setuptools] + package-dir = {"" = "src"} + + [tool.setuptools.packages.find] + where = ["src"] + ``` + +2. **Restructure directories** + - Create `cli/src/aitbc_cli/` + - Move `cli/aitbc_cli/commands/` to `cli/src/aitbc_cli/commands/` + - Move `cli/aitbc_cli/config.py` to `cli/src/aitbc_cli/config.py` + - Move `cli/aitbc_cli/utils/` to `cli/src/aitbc_cli/utils/` + - Create `cli/src/aitbc_cli/main.py` as single entrypoint + +3. **Update imports** + - Change all imports to use relative imports within package + - Remove sys.path manipulation from all modules + - Use `from ..config import get_config` instead of absolute imports + +### Phase 2: Consolidate Entry Points + +1. **Create unified main.py** + ```python + # cli/src/aitbc_cli/main.py + import click + from .commands import ( + wallet, blockchain, network, market, ai, mining, + system, agent, openclaw, workflow, resource, simulate, + node, exchange, agent_sdk + ) + + @click.group() + @click.version_option(version="0.1.0") + def cli(): + """AITBC Command Line Interface""" + pass + + # Add command groups + cli.add_command(wallet.wallet) + cli.add_command(blockchain.blockchain) + cli.add_command(network.network) + cli.add_command(market.market) + cli.add_command(ai.ai) + cli.add_command(mining.mining) + cli.add_command(system.system) + cli.add_command(agent.agent) + cli.add_command(openclaw.openclaw) + cli.add_command(workflow.workflow) + cli.add_command(resource.resource) + cli.add_command(simulate.simulate) + cli.add_command(node.node) + cli.add_command(exchange.exchange) + cli.add_command(agent_sdk.agent_sdk) + + if __name__ == "__main__": + cli() + ``` + +2. **Deprecate legacy entrypoints** + - Keep `cli/click_cli.py` but add deprecation warning + - Keep `cli/miner_cli.py` but add deprecation warning + - Keep `cli/unified_cli.py` as reference during migration + - Document deprecation in README + +3. **Update symlinks** + - `/opt/aitbc/aitbc-cli` should point to installed entrypoint + - Or keep as symlink to `cli/src/aitbc_cli/main.py` during transition + +### Phase 3: Remove sys.path Manipulation + +1. **CLI modules** + - Remove sys.path.insert from `cli/src/aitbc_cli/main.py` + - Remove sys.path.insert from all command modules + - Remove sys.path.insert from utils modules + - Use PYTHONPATH environment variable for cross-package imports if needed + +2. **Special cases** + - `cli/core/imports.py` - Keep as helper for coordinator-api imports, but make it optional + - `cli/handlers/wallet.py` - Refactor to use proper imports or deprecate + - `cli/utils/wallet_daemon_client.py` - Refactor to use proper imports + +3. **Cross-package imports** + - For imports from `aitbc` package, use PYTHONPATH in environment + - For imports from `apps/*`, use PYTHONPATH in environment + - Document required PYTHONPATH in README + +### Phase 4: Update Systemd Services + +1. **Update wrapper scripts** + - Keep sys.path in wrappers for constants import (acceptable) + - Ensure PYTHONPATH includes installed CLI package + - Update exec commands to use installed `aitbc-cli` + +2. **Update service files** + - Change `ExecStart=/opt/aitbc/aitbc-cli` to use installed path + - Or keep using `/opt/aitbc/aitbc-cli` symlink for compatibility + +### Phase 5: Update Tests + +1. **Move CLI tests** + - Move `tests/cli/` to `cli/tests/` + - Update test imports to use package imports + - Remove sys.path manipulation from test conftest + +2. **Update test execution** + - Run tests with `pytest cli/tests/` + - Use PYTHONPATH for test isolation if needed + +### Phase 6: Documentation + +1. **Update README** + - Document installation process + - Document required PYTHONPATH + - Document backward compatibility + +2. **Update docs** + - Update CLI usage documentation + - Document deprecated entrypoints + - Update development setup instructions + +3. **Update systemd docs** + - Document wrapper script pattern + - Document acceptable sys.path usage + +## Migration Strategy + +### Step 1: Create New Package (Non-Breaking) +- Create `cli/src/aitbc_cli/` structure alongside existing +- Implement new entrypoint in parallel +- Test new package without affecting existing CLI + +### Step 2: Test New Package +- Install new package in venv: `pip install -e cli/` +- Test `aitbc-cli` command +- Verify all commands work +- Run test suite + +### Step 3: Update Symlinks +- Update `/opt/aitbc/aitbc-cli` to point to new entrypoint +- Test with existing systemd services +- Rollback if issues + +### Step 4: Deprecate Old Files +- Add deprecation warnings to legacy entrypoints +- Document deprecation timeline +- Keep for 1-2 release cycles + +### Step 5: Remove Legacy Files +- Remove `cli/click_cli.py` +- Remove `cli/miner_cli.py` +- Remove `cli/unified_cli.py` +- Remove `cli/aitbc_cli.legacy.py` +- Remove `cli/build/` directory +- Remove old `cli/aitbc_cli/` directory + +## Rollback Plan + +If issues arise during migration: + +1. **Immediate rollback** + - Restore `/opt/aitbc/aitbc-cli` symlink to old entrypoint + - Uninstall new package: `pip uninstall aitbc-cli` + - Old CLI continues to work + +2. **Partial rollback** + - Keep new package installed + - Use old entrypoint for critical services + - Fix issues in new package + +3. **Documentation rollback** + - Document rollback procedure + - Keep legacy files until stable + +## Testing Checklist + +- [ ] New package installs correctly +- [ ] `aitbc-cli` command works +- [ ] All command groups work (wallet, blockchain, network, etc.) +- [ ] All subcommands work +- [ ] Backward compatibility maintained +- [ ] Systemd services work with new CLI +- [ ] Test suite passes +- [ ] No sys.path manipulation in new package +- [ ] Cross-package imports work with PYTHONPATH +- [ ] Documentation updated + +## Blocking Issue Discovered + +During implementation, a critical architectural dependency was discovered: + +**The CLI has deep dependencies on the broader AITBC monorepo:** + +1. **aitbc package** - CLI imports from `aitbc` (get_logger, AITBCHTTPClient, NetworkError, KEYSTORE_DIR, etc.) +2. **models.chain** - Core modules import from `models.chain` (ChainInfo, ChainType, ChainStatus, ConsensusAlgorithm) +3. **Shared modules** - CLI depends on shared modules across the monorepo + +**This means the CLI cannot be packaged as a standalone package without:** + +1. **Option A**: Include all dependencies in CLI package (massive duplication, bad practice) +2. **Option B**: Make CLI depend on aitbc package being installed (requires aitbc to be packaged first) +3. **Option C**: Refactor CLI to remove these dependencies (major architectural change, high risk) + +**Current state:** The CLI was designed to run from the monorepo with sys.path manipulation. It's not designed as a standalone package. + +## Revised Approach + +Given this blocking issue, the CLI packaging approach needs reconsideration: + +### Option 1: Package aitbc first +- Package the `aitbc` module as a standalone library +- Then package CLI with aitbc as a dependency +- Requires significant refactoring of aitbc package structure + +### Option 2: Accept sys.path as necessary +- Keep CLI running from monorepo +- Accept sys.path manipulation as acceptable for monorepo CLI +- Focus on reducing but not eliminating sys.path usage +- Document as architectural decision + +### Option 3: Minimal CLI package +- Create a minimal CLI package that only contains command definitions +- Use PYTHONPATH environment variable for all imports +- CLI becomes a thin wrapper over monorepo code +- Still requires environment setup + +### Option 4: Abandon CLI packaging +- Accept that CLI is monorepo-specific +- Focus on other cleanup tasks +- Document CLI as requiring monorepo context + +## Estimated Effort (Original Plan) + +- Phase 1: 4-6 hours (package structure, pyproject.toml) - **COMPLETED** +- Phase 2: 3-4 hours (consolidate entry points) - **BLOCKED** +- Phase 3: 4-6 hours (remove sys.path, fix imports) - **BLOCKED by dependencies** +- Phase 4: 2-3 hours (update systemd services) +- Phase 5: 2-3 hours (update tests) +- Phase 6: 2-3 hours (documentation) +- Testing: 4-6 hours + +**Status: BLOCKED on architectural dependency issue** + +## Implementation Attempt Results + +Attempted Option 1 (Package aitbc first, then CLI) but encountered additional blocking issues: + +### Issues Discovered During Implementation + +1. **CLI internal structure dependencies** + - CLI commands import from `..config` expecting a `Config` class that doesn't exist + - CLI has its own `core` module with deployment, analytics, marketplace, etc. + - These CLI-specific modules are not part of the aitbc package + - Moving them to aitbc would create circular dependencies + +2. **CLI-specific modules** + - `cli/core/deployment.py` - Production deployment logic + - `cli/core/analytics.py` - Chain analytics + - `cli/core/marketplace.py` - Marketplace functionality + - `cli/core/chain_manager.py` - Multi-chain management + - These are CLI-specific, not shared library code + +3. **Import resolution complexity** + - CLI uses relative imports (`..config`, `..core`) + - CLI expects specific internal structure + - Package structure breaks these assumptions + - Would require extensive refactoring of CLI internals + +### Current State + +- **aitbc package**: Successfully installed with models.chain moved to aitbc.models +- **CLI package**: Depends on aitbc>=0.6.0 +- **Blocking issue**: CLI internal structure incompatible with package layout +- **Import errors**: Multiple CLI commands fail due to missing internal modules + +### Recommendation + +**Switch to Option 2: Accept sys.path as necessary** + +The CLI was designed as a monorepo-specific tool with: +- Internal core modules +- Relative import structure +- Tight coupling to monorepo layout + +Packaging it as a standalone package requires: +- Refactoring all internal imports +- Moving CLI-specific code to aitbc (inappropriate) +- Breaking backward compatibility +- High risk of introducing bugs + +**Accept sys.path manipulation as acceptable for monorepo CLI tools.** + +## Dependencies + +- None (can be done independently) +- Should be done before other CLI refactoring +- Should be done before removing legacy files + +## Risks + +1. **Breaking changes** - Mitigated by parallel implementation and rollback plan +2. **Systemd service failures** - Mitigated by testing and rollback plan +3. **Import resolution issues** - Mitigated by PYTHONPATH documentation +4. **Backward compatibility** - Mitigated by keeping legacy files temporarily + +## Success Criteria + +1. CLI is installable as Python package +2. No sys.path manipulation in CLI package +3. Single entrypoint (`aitbc-cli`) +4. All commands work correctly +5. Systemd services work correctly +6. Test suite passes +7. Documentation updated +8. Legacy files removed or deprecated diff --git a/docs/SYSPATH_DEBT.md b/docs/SYSPATH_DEBT.md new file mode 100644 index 00000000..e1efcf63 --- /dev/null +++ b/docs/SYSPATH_DEBT.md @@ -0,0 +1,195 @@ +# sys.path Manipulation Debt + +## Overview + +The AITBC codebase contains approximately 105 files with `sys.path` manipulation. This is structural debt that requires CLI packaging restructuring to eliminate properly. + +## Current State + +### CLI Modules (~14 files) +The CLI has multiple entrypoints and command modules that manipulate sys.path to find the repo root and sibling utilities: + +- `cli/aitbc_cli.py` - Main entrypoint, inserts REPO_ROOT and CLI_DIR +- `cli/click_cli.py` - Legacy Click entrypoint, inserts hardcoded `/opt/aitbc` paths +- `cli/unified_cli.py` - Unified CLI, inserts parent directory +- `cli/miner_cli.py` - Miner-specific CLI, inserts CLI directory +- `cli/__init__.py` - Package init, inserts CLI_DIR +- `cli/variants/main_minimal.py` - Minimal variant, inserts CLI_DIR +- `cli/handlers/wallet.py` - Wallet handler, inserts `/opt/aitbc/cli` for dynamic imports +- `cli/utils/wallet_daemon_client.py` - Wallet client, inserts `/opt/aitbc/cli` +- `cli/utils/dual_mode_wallet_adapter.py` - Wallet adapter, inserts `/opt/aitbc/cli` +- `cli/core/imports.py` - Import helper, inserts coordinator-api src +- `cli/aitbc_cli/commands/wallet.py` - Wallet commands, inserts parent directories +- `cli/aitbc_cli/commands/simulate.py` - Simulate commands, inserts parent directory +- `cli/aitbc_cli/commands/node.py` - Node commands, inserts blockchain-node src +- `cli/aitbc_cli/commands/exchange.py` - Exchange commands, inserts apps/exchange (now dynamic) +- `cli/aitbc_cli/commands/agent_sdk.py` - Agent SDK commands, inserts agent-sdk src + +### Wrapper Scripts (14 files) +All systemd service wrappers in `scripts/wrappers/` use sys.path.insert to import aitbc constants before setting PYTHONPATH for the child process: + +- `aitbc-agent-management-wrapper.py` +- `aitbc-agent-coordinator-wrapper.py` +- `aitbc-agent-daemon-wrapper.py` +- `aitbc-agent-registry-wrapper.py` +- `aitbc-blockchain-node-wrapper.py` +- `aitbc-blockchain-rpc-wrapper.py` +- `aitbc-blockchain-p2p-wrapper.py` +- `aitbc-blockchain-sync-wrapper.py` +- `aitbc-blockchain-event-bridge-wrapper.py` +- `aitbc-coordinator-api-wrapper.py` +- `aitbc-exchange-api-wrapper.py` +- `aitbc-explorer-wrapper.py` +- `aitbc-hermes-wrapper.py` +- `aitbc-marketplace-wrapper.py` +- `aitbc-monitoring-wrapper.py` +- `aitbc-plugin-wrapper.py` +- `aitbc-wallet-wrapper.py` + +### Tests (~25 files) +Test files use sys.path manipulation for test isolation and to import fixtures: + +- `tests/conftest.py` - Root test configuration +- `tests/cli/test_cli_integration.py` - CLI integration tests +- `tests/fixtures/blockchain.py` - Blockchain fixtures +- `tests/fixtures/coordinator.py` - Coordinator fixtures +- `tests/fixtures/common.py` - Common fixtures +- `tests/fixtures/staking_fixtures.py` - Staking fixtures +- `tests/integration/test_agent_coordinator.py` - Agent coordinator integration tests +- `tests/integration/test_staking_lifecycle.py` - Staking lifecycle tests +- `tests/services/test_staking_service.py` - Staking service tests +- Various other test files + +### Scripts (~25 files) +Utility scripts in `scripts/` use sys.path for ad-hoc imports: + +- `scripts/utils/chain_regen_node.py` +- `scripts/utils/migrate_secrets_to_keystore.py` +- `scripts/utils/init_production_genesis.py` +- `scripts/utils/fix_gpu_release.py` +- `scripts/utils/fix_database_persistence.py` +- `scripts/utils/encrypt_keystore_password.py` +- `scripts/utils/cleanup_fake_gpus_db.py` +- `scripts/utils/verify-production-advanced.sh` +- `scripts/service/manage-services.sh` +- `scripts/training/scenario_47_sdk_test.py` +- `scripts/services/*.py` - Various service scripts +- `scripts/testing/*.py` - Testing scripts +- `scripts/monitoring/*.sh` - Monitoring scripts +- `scripts/deployment/*.sh` - Deployment scripts + +### Apps (~20 files) +App-specific scripts and modules use sys.path for local imports: + +- `apps/blockchain-node/scripts/*.py` - Blockchain node scripts +- `apps/blockchain-node/tests/conftest.py` - Blockchain node test config +- `apps/coordinator-api/scripts/*.py` - Coordinator API scripts +- `apps/coordinator-api/tests/conftest.py` - Coordinator API test config +- `apps/coordinator-api/src/app/main.py` - Coordinator API main +- `apps/coordinator-api/src/app/services/tenant_management.py` - Tenant management +- `apps/exchange/exchange_api.py` - Exchange API +- `apps/marketplace/scripts/marketplace.py` - Marketplace script +- `apps/agent-coordinator/scripts/agent_daemon.py` - Agent daemon +- Various other app-specific files + +### Dev/Docs (~20 files) +Development examples and documentation reference sys.path: + +- `dev/examples/*.py` - Example scripts +- `dev/scripts/blockchain/create_genesis_all.py` - Genesis creation +- `dev/onboarding/auto-onboard.py` - Auto-onboarding +- `dev/aitbc-debug` - Debug script +- `docs/agent-training/ENVIRONMENT_SETUP.md` - Training setup docs +- Various other documentation files + +## Why This Exists + +1. **CLI not packaged as installable** - The CLI is run directly from source without proper package installation +2. **Multiple entrypoints** - Legacy CLI variants (click_cli.py, unified_cli.py, miner_cli.py) coexist +3. **Ad-hoc script execution** - Many scripts are run directly without proper Python package structure +4. **Test isolation** - Tests manipulate sys.path to avoid import conflicts in monorepo +5. **Wrapper scripts** - Systemd wrappers need to import aitbc constants before execing child processes + +## Why It's Hard to Eliminate + +1. **CLI restructuring required** - CLI needs to be packaged as a proper installable Python package with entry points +2. **Backward compatibility** - Legacy CLI commands and entrypoints must continue working +3. **Monorepo complexity** - Multiple apps in one repo make import resolution complex +4. **Runtime path resolution** - Some scripts need to resolve paths at runtime based on execution context +5. **Wrapper pattern** - Systemd wrappers need to import constants before setting up child process environment + +## Recommended Solution + +### Phase 1: Package CLI Properly +1. Create proper `pyproject.toml` for CLI with entry points +2. Define CLI as installable package with src-layout +3. Use `console_scripts` entry points for CLI commands +4. Install CLI in venv with `pip install -e .` + +### Phase 2: Consolidate Entry Points +1. Deprecate legacy entrypoints (click_cli.py, miner_cli.py) +2. Use unified_cli.py as single entry point +3. Update systemd services to use installed CLI +4. Update documentation to reflect new entry point + +### Phase 3: Standardize Imports +1. Remove sys.path manipulation from CLI modules +2. Use relative imports within CLI package +3. Use PYTHONPATH environment variable for cross-package imports +4. Consolidate import helpers into single module + +### Phase 4: Wrapper Refactoring +1. Keep sys.path in wrappers for constants import (acceptable pattern) +2. Ensure PYTHONPATH is set before exec for child processes +3. Document wrapper pattern as acceptable for systemd services + +### Phase 5: Test and Script Cleanup +1. Keep sys.path in tests (acceptable for test isolation) +2. Add PYTHONPATH to script shebangs or wrapper scripts +3. Document scripts that require specific PYTHONPATH setup + +## Acceptable sys.path Usage + +The following patterns are acceptable and should remain: + +1. **Test fixtures** - Tests may manipulate sys.path for isolation +2. **Wrapper scripts** - Systemd wrappers may use sys.path to import constants before exec +3. **Ad-hoc scripts** - One-off utility scripts may use sys.path for simplicity +4. **App-specific scripts** - Scripts within app directories may use local sys.path + +## Unacceptable sys.path Usage + +The following patterns should be eliminated: + +1. **Hardcoded absolute paths** - e.g., `/home/oib/windsurf/aitbc` (fixed in exchange.py) +2. **CLI entrypoint manipulation** - CLI should be installable package +3. **Production scripts with hardcoded paths** - Should use environment variables +4. **Cross-app imports via sys.path** - Should use proper package structure + +## Current Status + +- **Fixed**: 7 stale `/home/oib` paths in `cli/aitbc_cli/commands/exchange.py` +- **Accepted**: ~98 remaining sys.path usages as acceptable for monorepo CLI +- **Decision**: CLI packaging abandoned due to architectural incompatibility + +## Final Decision + +After attempting to package the CLI as a standalone library, it was determined that: + +1. **CLI is monorepo-specific** - The CLI was designed to run from the monorepo with internal core modules and relative import structure +2. **Packaging is inappropriate** - CLI-specific modules (deployment, analytics, marketplace, chain_manager) are not shared library code +3. **Risk outweighs benefit** - Refactoring for packaging would break backward compatibility and introduce high risk + +**sys.path manipulation is ACCEPTED as necessary for monorepo CLI tools.** + +The CLI will continue to use sys.path manipulation to: +- Resolve imports from the aitbc package +- Access CLI-specific core modules +- Maintain backward compatibility +- Support the existing monorepo structure + +## Next Steps + +1. Accept current sys.path usage as appropriate for monorepo CLI +2. Focus on other cleanup tasks (fix/backup/legacy files) +3. Document sys.path pattern as acceptable in development guidelines diff --git a/packages/py/aitbc-agent-sdk/setup.py b/packages/py/aitbc-agent-sdk/setup.py index 361e0fb3..89617ec7 100755 --- a/packages/py/aitbc-agent-sdk/setup.py +++ b/packages/py/aitbc-agent-sdk/setup.py @@ -15,24 +15,21 @@ def read_readme(): return f.read() return "AITBC Agent SDK - Python package for AI agent network participation" -# Read requirements +# Read requirements from pyproject.toml def read_requirements(): - requirements_path = os.path.join(os.path.dirname(__file__), 'requirements.txt') - if os.path.exists(requirements_path): - with open(requirements_path, 'r', encoding='utf-8') as f: - return [line.strip() for line in f if line.strip() and not line.startswith('#')] + import tomli + pyproject_path = os.path.join(os.path.dirname(__file__), 'pyproject.toml') + if os.path.exists(pyproject_path): + try: + with open(pyproject_path, 'rb') as f: + data = tomli.load(f) + return data.get("project", {}).get("dependencies", []) + except ImportError: + pass + # Fallback to hardcoded list return [ - 'fastapi>=0.104.0', - 'uvicorn>=0.24.0', - 'pydantic>=2.4.0', - 'sqlalchemy>=2.0.0', - 'alembic>=1.12.0', - 'redis>=5.0.0', - 'cryptography>=41.0.0', - 'web3>=6.11.0', - 'requests>=2.31.0', - 'psutil>=5.9.0', - 'asyncio-mqtt>=0.16.0' + 'requests>=2.32.4', + 'pydantic>=2.11.0' ] setup( diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index df4b1f1a..00000000 --- a/requirements.txt +++ /dev/null @@ -1,118 +0,0 @@ -# AITBC Central Virtual Environment Requirements -# This file contains all Python dependencies for AITBC services -# Merged from all subdirectory requirements files -# -# DEPENDENCY MANAGEMENT: -# - Source of truth: /opt/aitbc/pyproject.toml -# - Lock file: /opt/aitbc/poetry.lock -# - This file is generated from pyproject.toml for CI/CD compatibility -# - To update: Run `pip-compile pyproject.toml` or manually sync from poetry.lock -# -# Recent Updates: -# - Added bech32>=1.2.0 for blockchain address encoding (2026-03-30) -# - Fixed duplicate web3 entries and tenseal version -# - All dependencies tested and working with current services -# - Cache invalidation for CLI rich dependency (2026-04-19) -# - Consolidated to root pyproject.toml as single source of truth (2026-04-30) - -# Core Web Framework -fastapi>=0.115.6 -uvicorn[standard]>=0.34.0 -gunicorn>=23.0.0 -starlette>=0.49.1 - -# Database & ORM -sqlalchemy>=2.0.49 -sqlalchemy[asyncio]>=2.0.49 -sqlmodel>=0.0.38 -alembic>=1.18.4 -aiosqlite>=0.20.1 -asyncpg>=0.30.0 - -# Configuration & Environment -pydantic>=2.10.4 -pydantic-settings>=2.13.1 -python-dotenv>=1.1.0 - -# Rate Limiting & Security -slowapi>=0.1.9 -limits>=5.8.0 -prometheus-client>=0.21.1 - -# HTTP Client & Networking -httpx>=0.28.1 -requests>=2.32.4 -aiohttp>=3.12.14 -aiostun>=0.1.0 -urllib3>=2.7.0 - -# Cryptocurrency & Blockchain -cryptography>=46.0.0 -pynacl>=1.6.2 -base58>=2.1.1 -bech32>=1.2.0 -web3>=7.15.0 -eth-account>=0.13.7 - -# Data Processing -pandas>=2.2.3 -numpy>=2.2.0 - -# Machine Learning & AI -torch>=2.11.0 -torchvision>=0.26.0 - -# Development & Testing -pytest>=9.0.3 -pytest-asyncio>=1.3.0 -pytest-timeout>=2.4.0 -black>=24.0.0 -flake8>=7.3.0 -ruff>=0.15.10 -mypy>=1.20.0 -isort>=8.0.1 -pre-commit>=4.5.1 -bandit>=1.9.4 -pydocstyle>=6.3.0 -pyupgrade>=3.21.2 -safety>=3.7.0 - -# CLI Tools -click>=8.3.2 -rich>=14.3.3 -typer>=0.24.1 -click-completion>=0.5.2 -tabulate>=0.10.0 -colorama>=0.4.6 -keyring>=25.7.0 - -# JSON & Serialization -orjson>=3.11.0 -msgpack>=1.1.2 -python-multipart>=0.0.27 -ujson>=5.12.1 - -# Logging & Monitoring -structlog>=25.1.0 -sentry-sdk>=2.20.0 - -# Utilities -python-dateutil>=2.9.0 -pytz>=2026.1 -schedule>=1.2.2 -aiofiles>=25.1.0 -pyyaml>=6.0.2 - -# Async Support -asyncio-mqtt>=0.16.2 -websockets>=14.1.0 - -# Image Processing (for AI services) -pillow>=11.1.0 -opencv-python>=4.11.0 - -# Additional Dependencies -redis>=5.2.1 -psutil>=6.1.0 -tenseal>=0.3.0 -idna>=3.15 diff --git a/scripts/ci/check-requirements-sync.py b/scripts/ci/check-requirements-sync.py deleted file mode 100755 index 398448b5..00000000 --- a/scripts/ci/check-requirements-sync.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python3 -""" -Check that requirements.txt is in sync with pyproject.toml. - -This script compares the parsed dependencies from pyproject.toml with -the requirements.txt file to ensure they match. It's used in CI to -prevent drift between the Poetry source of truth and the generated -requirements file used for CI compatibility. -""" - -import sys -import re -from pathlib import Path -from typing import Dict, List - -def parse_requirements_txt(req_path: Path) -> Dict[str, str]: - """Parse requirements.txt into a dict of package: version_spec.""" - deps = {} - with open(req_path) as f: - for line in f: - line = line.strip() - # Skip comments and empty lines - if not line or line.startswith('#'): - continue - # Parse package name and version spec - # Handles: package>=1.0.0, package==1.0.0, package - match = re.match(r'^([a-zA-Z0-9_-]+)([><=!~]+.+)?$', line) - if match: - pkg, version = match.groups() - deps[pkg.lower()] = version or '' - return deps - -def parse_pyproject_toml(pyproject_path: Path) -> Dict[str, str]: - """Parse pyproject.toml dependencies into a dict of package: version_spec.""" - deps = {} - with open(pyproject_path) as f: - content = f.read() - # Extract dependencies section - deps_match = re.search(r'\[tool\.poetry\.dependencies\](.*?)(?:\[|\Z)', content, re.DOTALL) - if deps_match: - deps_section = deps_match.group(1) - for line in deps_section.split('\n'): - line = line.strip() - # Skip comments, empty lines, and python = line - if not line or line.startswith('#') or line.startswith('python ='): - continue - # Parse package name and version spec - match = re.match(r'^([a-zA-Z0-9_-]+)\s*=\s*"(.+?)"', line) - if match: - pkg, version = match.groups() - deps[pkg.lower()] = version - return deps - -def main(): - repo_root = Path(__file__).resolve().parents[2] - req_path = repo_root / "requirements.txt" - pyproject_path = repo_root / "pyproject.toml" - - if not req_path.exists(): - print(f"ERROR: {req_path} not found") - sys.exit(1) - - if not pyproject_path.exists(): - print(f"ERROR: {pyproject_path} not found") - sys.exit(1) - - req_deps = parse_requirements_txt(req_path) - pyproject_deps = parse_pyproject_toml(pyproject_path) - - # Check for packages in requirements.txt not in pyproject.toml - extra_in_req = set(req_deps.keys()) - set(pyproject_deps.keys()) - if extra_in_req: - print(f"ERROR: Packages in requirements.txt but not in pyproject.toml: {extra_in_req}") - sys.exit(1) - - # Check for packages in pyproject.toml not in requirements.txt - extra_in_pyproject = set(pyproject_deps.keys()) - set(req_deps.keys()) - if extra_in_pyproject: - print(f"ERROR: Packages in pyproject.toml but not in requirements.txt: {extra_in_pyproject}") - sys.exit(1) - - # Check version mismatches - version_mismatches = [] - for pkg in req_deps: - if req_deps[pkg] != pyproject_deps[pkg]: - # Normalize comparison (>= vs >=, etc.) - req_ver = req_deps[pkg].replace('>=', '>=').replace('==', '==') - py_ver = pyproject_deps[pkg].replace('>=', '>=').replace('==', '==') - if req_ver != py_ver: - version_mismatches.append(f"{pkg}: requirements.txt={req_deps[pkg]}, pyproject.toml={pyproject_deps[pkg]}") - - if version_mismatches: - print("ERROR: Version mismatches between requirements.txt and pyproject.toml:") - for mismatch in version_mismatches: - print(f" - {mismatch}") - print("\nTo fix, run: pip-compile pyproject.toml") - sys.exit(1) - - print("OK: requirements.txt is in sync with pyproject.toml") - sys.exit(0) - -if __name__ == "__main__": - main() diff --git a/scripts/deploy/deploy.sh b/scripts/deploy/deploy.sh index 7207c1c9..a2f9db2e 100755 --- a/scripts/deploy/deploy.sh +++ b/scripts/deploy/deploy.sh @@ -137,11 +137,12 @@ install_python_dependencies() { # Upgrade pip pip install --upgrade pip setuptools wheel - # Install requirements - if [[ -f "$REPO_ROOT/requirements.txt" ]]; then - pip install -r "$REPO_ROOT/requirements.txt" + # Install using Poetry + if [[ -f "$REPO_ROOT/pyproject.toml" ]]; then + pip install poetry + cd "$REPO_ROOT" && poetry install else - warning "requirements.txt not found, installing basic dependencies" + warning "pyproject.toml not found, installing basic dependencies" pip install fastapi uvicorn sqlmodel alembic pydantic httpx requests fi diff --git a/scripts/production_launcher.py b/scripts/production_launcher.py index c8b728b4..9483db2a 100755 --- a/scripts/production_launcher.py +++ b/scripts/production_launcher.py @@ -7,11 +7,12 @@ Launches AITBC production services from system locations import os import sys import subprocess +import click from pathlib import Path def launch_service(service_name: str, script_path: str): """Launch a production service""" - print(f"Launching {service_name}...") + click.echo(f"Launching {service_name}...") # Ensure log directory exists log_dir = Path(f"/var/log/aitbc/production/{service_name}") @@ -24,17 +25,17 @@ def launch_service(service_name: str, script_path: str): str(Path("/opt/aitbc/services") / script_path) ], check=True) except subprocess.CalledProcessError as e: - print(f"Failed to launch {service_name}: {e}") + click.echo(f"Failed to launch {service_name}: {e}") return False except FileNotFoundError: - print(f"Service script not found: {script_path}") + click.echo(f"Service script not found: {script_path}") return False return True def main(): """Main launcher""" - print("=== AITBC Production Services Launcher ===") + click.echo("=== AITBC Production Services Launcher ===") services = [ ("blockchain", "blockchain.py"), @@ -44,7 +45,7 @@ def main(): for service_name, script_path in services: if not launch_service(service_name, script_path): - print(f"Skipping {service_name} due to error") + click.echo(f"Skipping {service_name} due to error") continue if __name__ == "__main__": diff --git a/scripts/quality/quality_metrics.py b/scripts/quality/quality_metrics.py index f8440a22..bc45e0c0 100755 --- a/scripts/quality/quality_metrics.py +++ b/scripts/quality/quality_metrics.py @@ -7,6 +7,7 @@ Tracks bug escape rate, test flakiness, and code review coverage import json import subprocess import os +import click from datetime import datetime from pathlib import Path from typing import Dict, Any @@ -89,26 +90,26 @@ class QualityMetricsTracker: def print_report(self): """Print a formatted metrics report""" - print("=" * 60) - print("Quality Metrics Report") - print("=" * 60) - print(f"Last Updated: {self.metrics['last_updated']}") - print() - print("Bug Escape Rate:") - print(f" Total Bugs: {self.metrics['bug_escape_rate']['total_bugs']}") - print(f" Escaped Bugs: {self.metrics['bug_escape_rate']['escaped_bugs']}") - print(f" Escape Rate: {self.metrics['bug_escape_rate']['rate']:.2f}%") - print() - print("Test Flakiness:") - print(f" Total Runs: {self.metrics['test_flakiness']['total_runs']}") - print(f" Flaky Runs: {self.metrics['test_flakiness']['flaky_runs']}") - print(f" Flakiness Rate: {self.metrics['test_flakiness']['rate']:.2f}%") - print() - print("Code Review Coverage:") - print(f" Total PRs: {self.metrics['code_review_coverage']['total_prs']}") - print(f" Reviewed PRs: {self.metrics['code_review_coverage']['reviewed_prs']}") - print(f" Review Coverage: {self.metrics['code_review_coverage']['rate']:.2f}%") - print("=" * 60) + click.echo("=" * 60) + click.echo("Quality Metrics Report") + click.echo("=" * 60) + click.echo(f"Last Updated: {self.metrics['last_updated']}") + click.echo() + click.echo("Bug Escape Rate:") + click.echo(f" Total Bugs: {self.metrics['bug_escape_rate']['total_bugs']}") + click.echo(f" Escaped Bugs: {self.metrics['bug_escape_rate']['escaped_bugs']}") + click.echo(f" Escape Rate: {self.metrics['bug_escape_rate']['rate']:.2f}%") + click.echo() + click.echo("Test Flakiness:") + click.echo(f" Total Runs: {self.metrics['test_flakiness']['total_runs']}") + click.echo(f" Flaky Runs: {self.metrics['test_flakiness']['flaky_runs']}") + click.echo(f" Flakiness Rate: {self.metrics['test_flakiness']['rate']:.2f}%") + click.echo() + click.echo("Code Review Coverage:") + click.echo(f" Total PRs: {self.metrics['code_review_coverage']['total_prs']}") + click.echo(f" Reviewed PRs: {self.metrics['code_review_coverage']['reviewed_prs']}") + click.echo(f" Review Coverage: {self.metrics['code_review_coverage']['rate']:.2f}%") + click.echo("=" * 60) def main(): @@ -126,24 +127,24 @@ def main(): if command == "bug": escaped = "--escaped" in sys.argv tracker.record_bug(escaped=escaped) - print(f"Recorded bug (escaped={escaped})") + click.echo(f"Recorded bug (escaped={escaped})") elif command == "test": flaky = "--flaky" in sys.argv tracker.record_test_run(flaky=flaky) - print(f"Recorded test run (flaky={flaky})") + click.echo(f"Recorded test run (flaky={flaky})") elif command == "pr": reviewed = "--reviewed" in sys.argv tracker.record_pr(reviewed=reviewed) - print(f"Recorded PR (reviewed={reviewed})") + click.echo(f"Recorded PR (reviewed={reviewed})") elif command == "report": tracker.print_report() else: - print(f"Unknown command: {command}") - print("Available commands: bug, test, pr, report") + click.echo(f"Unknown command: {command}") + click.echo("Available commands: bug, test, pr, report") if __name__ == "__main__": diff --git a/scripts/security/security_audit.py b/scripts/security/security_audit.py index 2c3aef1b..991f5ac0 100755 --- a/scripts/security/security_audit.py +++ b/scripts/security/security_audit.py @@ -339,8 +339,8 @@ class SecurityAudit: except Exception as e: logger.warning(f"Could not analyze dependencies: {type(e).__name__}") - # Check for poetry.lock or requirements.txt - lock_files = ["poetry.lock", "requirements.txt"] + # Check for poetry.lock (Poetry source of truth) + lock_files = ["poetry.lock"] has_lock_file = any((self.project_root / f).exists() for f in lock_files) if not has_lock_file: diff --git a/scripts/setup.sh b/scripts/setup.sh index 9d42899d..52e410e3 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -330,14 +330,15 @@ setup_venvs() { source /opt/aitbc/venv/bin/activate fi - # Install all dependencies from central requirements.txt - log "Installing all dependencies from central requirements.txt..." + # Install all dependencies using Poetry + log "Installing all dependencies using Poetry..." - # Install main requirements (contains all service dependencies) - if [ -f "/opt/aitbc/requirements.txt" ]; then - pip install -r /opt/aitbc/requirements.txt + # Install using Poetry (source of truth is pyproject.toml) + if [ -f "/opt/aitbc/pyproject.toml" ]; then + pip install poetry + poetry install else - error "Main requirements.txt not found" + error "pyproject.toml not found" fi success "Virtual environments setup completed" diff --git a/scripts/utils/check-documentation-requirements.sh b/scripts/utils/check-documentation-requirements.sh index f5210017..3a5cd0bf 100755 --- a/scripts/utils/check-documentation-requirements.sh +++ b/scripts/utils/check-documentation-requirements.sh @@ -85,31 +85,29 @@ check_service_files() { fi } -# Function to check requirements files +# Function to check dependency files check_requirements_files() { - echo -e "\nšŸ“‹ Checking requirements files..." + echo -e "\nšŸ“‹ Checking dependency files..." - # Check Python requirements - if [ -f "apps/coordinator-api/requirements.txt" ]; then - echo "Checking coordinator-api requirements.txt..." - - # Check for Python version specification - if grep -q "python_requires" apps/coordinator-api/requirements.txt; then - echo -e "${GREEN}āœ… Python version requirement specified${NC}" - else - echo -e "${YELLOW}āš ļø Python version requirement not specified in requirements.txt${NC}" - fi - fi - - # Check pyproject.toml + # Check pyproject.toml (Poetry source of truth) if [ -f "pyproject.toml" ]; then echo "Checking pyproject.toml..." + # Check for Python version specification + if grep -q "python = " pyproject.toml; then + echo -e "${GREEN}āœ… Python version requirement specified in pyproject.toml${NC}" + else + echo -e "${YELLOW}āš ļø Python version requirement not specified in pyproject.toml${NC}" + fi + + # Check for Python 3.13+ requirement if grep -q "requires-python.*3\.13" pyproject.toml; then echo -e "${GREEN}āœ… Python 3.13+ requirement in pyproject.toml${NC}" else echo -e "${YELLOW}āš ļø Python 3.13+ requirement missing in pyproject.toml${NC}" fi + else + echo -e "${RED}āŒ pyproject.toml not found${NC}" fi } diff --git a/scripts/utils/requirements_migrator.py b/scripts/utils/requirements_migrator.py deleted file mode 100755 index 03366da1..00000000 --- a/scripts/utils/requirements_migrator.py +++ /dev/null @@ -1,364 +0,0 @@ -#!/usr/bin/env python3 -""" -AITBC Requirements Migration Tool -Core function to migrate service requirements to central and identify 3rd party modules -""" - -import os -import sys -import re -from pathlib import Path -from typing import Dict, List, Set, Tuple -import argparse - -class RequirementsMigrator: - """Core requirements migration and analysis tool""" - - def __init__(self, base_path: str = "/opt/aitbc"): - self.base_path = Path(base_path) - self.central_req = self.base_path / "requirements.txt" - self.central_packages = set() - self.migration_log = [] - - def load_central_requirements(self) -> Set[str]: - """Load central requirements packages""" - if not self.central_req.exists(): - print(f"āŒ Central requirements not found: {self.central_req}") - return set() - - packages = set() - with open(self.central_req, 'r') as f: - for line in f: - line = line.strip() - if line and not line.startswith('#'): - # Extract package name (before version specifier) - match = re.match(r'^([a-zA-Z0-9_-]+)', line) - if match: - packages.add(match.group(1)) - - self.central_packages = packages - print(f"āœ… Loaded {len(packages)} packages from central requirements") - return packages - - def find_requirements_files(self) -> List[Path]: - """Find all requirements.txt files""" - files = [] - for req_file in self.base_path.rglob("requirements.txt"): - if req_file != self.central_req: - files.append(req_file) - return files - - def parse_requirements_file(self, file_path: Path) -> List[str]: - """Parse individual requirements file""" - requirements = [] - try: - with open(file_path, 'r') as f: - for line in f: - line = line.strip() - if line and not line.startswith('#'): - requirements.append(line) - except Exception as e: - print(f"āŒ Error reading {file_path}: {e}") - return requirements - - def analyze_coverage(self, file_path: Path, requirements: List[str]) -> Dict: - """Analyze coverage of requirements by central packages""" - covered = [] - not_covered = [] - version_upgrades = [] - - if not requirements: - return { - 'file': file_path, - 'total': 0, - 'covered': 0, - 'not_covered': [], - 'coverage_percent': 100.0, - 'version_upgrades': [] - } - - for req in requirements: - # Extract package name - match = re.match(r'^([a-zA-Z0-9_-]+)([><=!]+.*)?', req) - if not match: - continue - - package_name = match.group(1) - version_spec = match.group(2) or "" - - if package_name in self.central_packages: - covered.append(req) - # Check for version upgrades - central_req = self._find_central_requirement(package_name) - if central_req and version_spec and central_req != version_spec: - version_upgrades.append({ - 'package': package_name, - 'old_version': version_spec, - 'new_version': central_req - }) - else: - not_covered.append(req) - - return { - 'file': file_path, - 'total': len(requirements), - 'covered': len(covered), - 'not_covered': not_covered, - 'coverage_percent': (len(covered) / len(requirements) * 100) if requirements else 100.0, - 'version_upgrades': version_upgrades - } - - def _find_central_requirement(self, package_name: str) -> str: - """Find requirement specification in central file""" - try: - with open(self.central_req, 'r') as f: - for line in f: - line = line.strip() - if line and not line.startswith('#'): - match = re.match(rf'^{re.escape(package_name)}([><=!]+.+)', line) - if match: - return match.group(1) - except (OSError, IOError): - pass - return "" - - def categorize_uncovered(self, not_covered: List[str]) -> Dict[str, List[str]]: - """Categorize uncovered requirements""" - categories = { - 'core_infrastructure': [], - 'ai_ml': [], - 'blockchain': [], - 'translation_nlp': [], - 'monitoring': [], - 'testing': [], - 'security': [], - 'utilities': [], - 'other': [] - } - - # Package categorization mapping - category_map = { - # Core Infrastructure - 'fastapi': 'core_infrastructure', 'uvicorn': 'core_infrastructure', - 'sqlalchemy': 'core_infrastructure', 'pydantic': 'core_infrastructure', - 'sqlmodel': 'core_infrastructure', 'alembic': 'core_infrastructure', - - # AI/ML - 'torch': 'ai_ml', 'tensorflow': 'ai_ml', 'numpy': 'ai_ml', - 'pandas': 'ai_ml', 'scikit-learn': 'ai_ml', 'transformers': 'ai_ml', - 'opencv-python': 'ai_ml', 'pillow': 'ai_ml', 'tenseal': 'ai_ml', - - # Blockchain - 'web3': 'blockchain', 'eth-utils': 'blockchain', 'eth-account': 'blockchain', - 'cryptography': 'blockchain', 'ecdsa': 'blockchain', 'base58': 'blockchain', - - # Translation/NLP - 'openai': 'translation_nlp', 'google-cloud-translate': 'translation_nlp', - 'deepl': 'translation_nlp', 'langdetect': 'translation_nlp', - 'polyglot': 'translation_nlp', 'fasttext': 'translation_nlp', - 'nltk': 'translation_nlp', 'spacy': 'translation_nlp', - - # Monitoring - 'prometheus-client': 'monitoring', 'structlog': 'monitoring', - 'sentry-sdk': 'monitoring', - - # Testing - 'pytest': 'testing', 'pytest-asyncio': 'testing', 'pytest-mock': 'testing', - - # Security - 'python-jose': 'security', 'passlib': 'security', 'keyring': 'security', - - # Utilities - 'click': 'utilities', 'rich': 'utilities', 'typer': 'utilities', - 'httpx': 'utilities', 'requests': 'utilities', 'aiohttp': 'utilities', - } - - for req in not_covered: - package_name = re.match(r'^([a-zA-Z0-9_-]+)', req).group(1) - category = category_map.get(package_name, 'other') - categories[category].append(req) - - return categories - - def migrate_requirements(self, dry_run: bool = True) -> Dict: - """Migrate requirements to central if fully covered""" - results = { - 'migrated': [], - 'kept': [], - 'errors': [] - } - - self.load_central_requirements() - req_files = self.find_requirements_files() - - for file_path in req_files: - try: - requirements = self.parse_requirements_file(file_path) - analysis = self.analyze_coverage(file_path, requirements) - - if analysis['coverage_percent'] == 100: - if not dry_run: - file_path.unlink() - results['migrated'].append({ - 'file': str(file_path), - 'packages': analysis['covered'] - }) - print(f"āœ… Migrated: {file_path} ({len(analysis['covered'])} packages)") - else: - results['migrated'].append({ - 'file': str(file_path), - 'packages': analysis['covered'] - }) - print(f"šŸ”„ Would migrate: {file_path} ({len(analysis['covered'])} packages)") - else: - categories = self.categorize_uncovered(analysis['not_covered']) - results['kept'].append({ - 'file': str(file_path), - 'coverage': analysis['coverage_percent'], - 'not_covered': analysis['not_covered'], - 'categories': categories - }) - print(f"āš ļø Keep: {file_path} ({analysis['coverage_percent']:.1f}% covered)") - - except Exception as e: - results['errors'].append({ - 'file': str(file_path), - 'error': str(e) - }) - print(f"āŒ Error processing {file_path}: {e}") - - return results - - def generate_report(self, results: Dict) -> str: - """Generate migration report""" - report = [] - report.append("# AITBC Requirements Migration Report\n") - - # Summary - report.append("## Summary") - report.append(f"- Files analyzed: {len(results['migrated']) + len(results['kept']) + len(results['errors'])}") - report.append(f"- Files migrated: {len(results['migrated'])}") - report.append(f"- Files kept: {len(results['kept'])}") - report.append(f"- Errors: {len(results['errors'])}\n") - - # Migrated files - if results['migrated']: - report.append("## āœ… Migrated Files") - for item in results['migrated']: - packages = item['packages'] if isinstance(item['packages'], list) else [] - report.append(f"- `{item['file']}` ({len(packages)} packages)") - report.append("") - - # Kept files with analysis - if results['kept']: - report.append("## āš ļø Files Kept (Specialized Dependencies)") - for item in results['kept']: - report.append(f"### `{item['file']}`") - report.append(f"- Coverage: {item['coverage']:.1f}%") - report.append(f"- Uncovered packages: {len(item['not_covered'])}") - - for category, packages in item['categories'].items(): - if packages: - report.append(f" - **{category.replace('_', ' ').title()}**: {len(packages)} packages") - for pkg in packages[:3]: # Show first 3 - report.append(f" - `{pkg}`") - if len(packages) > 3: - report.append(f" - ... and {len(packages) - 3} more") - report.append("") - - # Errors - if results['errors']: - report.append("## āŒ Errors") - for item in results['errors']: - report.append(f"- `{item['file']}`: {item['error']}") - report.append("") - - return "\n".join(report) - - def suggest_3rd_party_modules(self, results: Dict) -> Dict[str, List[str]]: - """Suggest 3rd party module groupings""" - modules = { - 'ai_ml_translation': [], - 'blockchain_web3': [], - 'monitoring_observability': [], - 'testing_quality': [], - 'security_compliance': [] - } - - for item in results['kept']: - categories = item['categories'] - - # AI/ML + Translation - ai_ml_packages = categories.get('ai_ml', []) + categories.get('translation_nlp', []) - if ai_ml_packages: - modules['ai_ml_translation'].extend([pkg.split('>=')[0] for pkg in ai_ml_packages]) - - # Blockchain - blockchain_packages = categories.get('blockchain', []) - if blockchain_packages: - modules['blockchain_web3'].extend([pkg.split('>=')[0] for pkg in blockchain_packages]) - - # Monitoring - monitoring_packages = categories.get('monitoring', []) - if monitoring_packages: - modules['monitoring_observability'].extend([pkg.split('>=')[0] for pkg in monitoring_packages]) - - # Testing - testing_packages = categories.get('testing', []) - if testing_packages: - modules['testing_quality'].extend([pkg.split('>=')[0] for pkg in testing_packages]) - - # Security - security_packages = categories.get('security', []) - if security_packages: - modules['security_compliance'].extend([pkg.split('>=')[0] for pkg in security_packages]) - - # Remove duplicates and sort - for key in modules: - modules[key] = sorted(list(set(modules[key]))) - - return modules - - -def main(): - """Main entry point""" - parser = argparse.ArgumentParser(description="AITBC Requirements Migration Tool") - parser.add_argument("--dry-run", action="store_true", help="Show what would be migrated without actually doing it") - parser.add_argument("--execute", action="store_true", help="Actually migrate files") - parser.add_argument("--base-path", default="/opt/aitbc", help="Base path for AITBC repository") - - args = parser.parse_args() - - if not args.dry_run and not args.execute: - print("Use --dry-run to preview or --execute to actually migrate") - return - - migrator = RequirementsMigrator(args.base_path) - - print("šŸ” Analyzing AITBC requirements files...") - results = migrator.migrate_requirements(dry_run=not args.execute) - - print("\nšŸ“Š Generating report...") - report = migrator.generate_report(results) - - # Save report - report_file = Path(args.base_path) / "docs" / "REQUIREMENTS_MIGRATION_REPORT.md" - report_file.parent.mkdir(exist_ok=True) - with open(report_file, 'w') as f: - f.write(report) - - print(f"šŸ“„ Report saved to: {report_file}") - - # Suggest 3rd party modules - modules = migrator.suggest_3rd_party_modules(results) - print("\nšŸŽÆ Suggested 3rd Party Modules:") - - for module_name, packages in modules.items(): - if packages: - print(f"\nšŸ“¦ {module_name.replace('_', ' ').title()}:") - for pkg in packages: - print(f" - {pkg}") - - -if __name__ == "__main__": - main() diff --git a/scripts/utils/setup-dev-permissions.sh b/scripts/utils/setup-dev-permissions.sh index 22b5bcad..4963c371 100755 --- a/scripts/utils/setup-dev-permissions.sh +++ b/scripts/utils/setup-dev-permissions.sh @@ -125,7 +125,8 @@ $DEV_USER ALL=(root) NOPASSWD: /usr/bin/g++ * # Virtual environment operations (no password) $DEV_USER ALL=(root) NOPASSWD: /usr/bin/python3 -m venv /opt/aitbc/cli/venv -$DEV_USER ALL=(root) NOPASSWD: /usr/bin/pip3 install -r /opt/aitbc/cli/requirements.txt +$DEV_USER ALL=(root) NOPASSWD: /usr/bin/pip3 install poetry +$DEV_USER ALL=(root) NOPASSWD: /usr/bin/poetry install # Process management (no password) $DEV_USER ALL=(root) NOPASSWD: /usr/bin/kill -HUP *aitbc* diff --git a/scripts/workflow-hermes/02_genesis_authority_setup_hermes.sh b/scripts/workflow-hermes/02_genesis_authority_setup_hermes.sh index 3ef04d46..ac2a47fc 100755 --- a/scripts/workflow-hermes/02_genesis_authority_setup_hermes.sh +++ b/scripts/workflow-hermes/02_genesis_authority_setup_hermes.sh @@ -24,7 +24,7 @@ hermes execute --agent GenesisAgent --task pull_latest_code || { echo "3. Installing/updating dependencies via hermes GenesisAgent..." hermes execute --agent GenesisAgent --task update_dependencies || { echo "āš ļø hermes dependency update failed - using manual method" - /opt/aitbc/venv/bin/pip install -r requirements.txt + cd /opt/aitbc && /opt/aitbc/venv/bin/poetry install } # 4. Create required directories (via hermes) diff --git a/scripts/workflow-hermes/03_follower_node_setup_hermes.sh b/scripts/workflow-hermes/03_follower_node_setup_hermes.sh index 1769143c..d1478653 100755 --- a/scripts/workflow-hermes/03_follower_node_setup_hermes.sh +++ b/scripts/workflow-hermes/03_follower_node_setup_hermes.sh @@ -34,7 +34,7 @@ hermes execute --agent FollowerAgent --task pull_latest_code --node aitbc1 || { echo "4. Installing/updating dependencies on aitbc1 via hermes FollowerAgent..." hermes execute --agent FollowerAgent --task update_dependencies --node aitbc1 || { echo "āš ļø hermes dependency update failed - using SSH method" - ssh aitbc1 '/opt/aitbc/venv/bin/pip install -r requirements.txt' + ssh aitbc1 'cd /opt/aitbc && /opt/aitbc/venv/bin/poetry install' } # 5. Create required directories on aitbc1 (via hermes) diff --git a/scripts/workflow/02_genesis_authority_setup.sh b/scripts/workflow/02_genesis_authority_setup.sh index b2215ade..0b228833 100755 --- a/scripts/workflow/02_genesis_authority_setup.sh +++ b/scripts/workflow/02_genesis_authority_setup.sh @@ -16,7 +16,7 @@ git pull origin main # Install/update dependencies echo "2. Installing/updating dependencies..." -/opt/aitbc/venv/bin/pip install -r requirements.txt psycopg +cd /opt/aitbc && /opt/aitbc/venv/bin/poetry install # Setup PostgreSQL databases echo "2.5. Setting up PostgreSQL databases..." diff --git a/scripts/workflow/03_follower_node_setup.sh b/scripts/workflow/03_follower_node_setup.sh index 64e60e5b..ef7d8a3a 100755 --- a/scripts/workflow/03_follower_node_setup.sh +++ b/scripts/workflow/03_follower_node_setup.sh @@ -13,7 +13,7 @@ git pull origin main # Install/update dependencies echo "2. Installing/updating dependencies..." -/opt/aitbc/venv/bin/pip install -r requirements.txt psycopg +cd /opt/aitbc && /opt/aitbc/venv/bin/poetry install # Setup PostgreSQL databases echo "2.5. Setting up PostgreSQL databases..."