ci: migrate from requirements.txt to poetry.lock as source of truth
Some checks failed
Coverage Phase 1 (70% Target) / test-coverage-70 (push) Has been cancelled
Coverage Phase 2 (85% Target) / test-coverage-85 (push) Has been cancelled
Cross-Node Transaction Testing / transaction-test (push) Has been cancelled
Deploy to Testnet / deploy-testnet (push) Has been cancelled
Integration Tests / test-service-integration (push) Has been cancelled
Multi-Node Stress Testing / stress-test (push) Has been cancelled
Python Tests / test-python (push) Has been cancelled
Security Scanning / security-scan (push) Has been cancelled
Documentation Validation / validate-docs (push) Has been cancelled
Documentation Validation / validate-policies-strict (push) Has been cancelled
CLI Tests / test-cli (push) Has been cancelled
Package Tests / Python package - aitbc-agent-sdk (push) Has been cancelled
Package Tests / Python package - aitbc-core (push) Has been cancelled
Package Tests / Python package - aitbc-crypto (push) Has been cancelled
Package Tests / Python package - aitbc-sdk (push) Has been cancelled
Package Tests / JavaScript package - aitbc-sdk-js (push) Has been cancelled
Package Tests / JavaScript package - aitbc-token (push) Has been cancelled
Smart Contract Tests / test-solidity (map[name:aitbc-contracts path:contracts]) (push) Has been cancelled
Smart Contract Tests / test-solidity (map[name:aitbc-token path:packages/solidity/aitbc-token]) (push) Has been cancelled
Smart Contract Tests / test-foundry (push) Has been cancelled
Smart Contract Tests / lint-solidity (push) Has been cancelled
Smart Contract Tests / deploy-contracts (push) Has been cancelled

- Updated CI workflows to track poetry.lock instead of requirements.txt
- Removed check-requirements-sync.py step from python-tests.yml
- Updated dependency_scanner.py default from requirements.txt to pyproject.toml
- Replaced all print() with click.echo() in deploy_edge_node.py (CLI script)
- Replaced print() with logger.warning() in zk_cache.py
- Updated setup.py files to read dependencies from pyproject.toml via tomli
- Removed
This commit is contained in:
aitbc
2026-05-25 15:10:12 +02:00
parent 8ef559a12c
commit 214c1b65ec
107 changed files with 808 additions and 19723 deletions

View File

@@ -6,6 +6,7 @@ on:
paths: paths:
- 'packages/**' - 'packages/**'
- 'pyproject.toml' - 'pyproject.toml'
- 'poetry.lock'
- '.gitea/workflows/package-tests.yml' - '.gitea/workflows/package-tests.yml'
pull_request: pull_request:
branches: [main, develop] branches: [main, develop]

View File

@@ -8,7 +8,7 @@ on:
- 'packages/py/**' - 'packages/py/**'
- 'tests/**' - 'tests/**'
- 'pyproject.toml' - 'pyproject.toml'
- 'requirements.txt' - 'poetry.lock'
- '.gitea/workflows/python-tests.yml' - '.gitea/workflows/python-tests.yml'
pull_request: pull_request:
branches: [main, develop] 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" --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" 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 - name: Run linting
run: | run: |
cd "${{ env.WORKSPACE }}/repo" cd "${{ env.WORKSPACE }}/repo"

View File

@@ -39,9 +39,9 @@ class DependencyScanner:
Initialize dependency scanner Initialize dependency scanner
Args: 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] = [] self._vulnerabilities: List[VulnerabilityReport] = []
def scan_with_pip_audit(self) -> List[VulnerabilityReport]: def scan_with_pip_audit(self) -> List[VulnerabilityReport]:

View File

@@ -9,6 +9,7 @@ import subprocess
import sys import sys
import os import os
import json import json
import click
from datetime import datetime from datetime import datetime
def load_config(config_file): def load_config(config_file):
@@ -18,24 +19,24 @@ def load_config(config_file):
def deploy_redis_cache(config): def deploy_redis_cache(config):
"""Deploy Redis cache layer""" """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 # Check if Redis is running
try: try:
result = subprocess.run(['redis-cli', 'ping'], capture_output=True, text=True) result = subprocess.run(['redis-cli', 'ping'], capture_output=True, text=True)
if result.stdout.strip() == 'PONG': if result.stdout.strip() == 'PONG':
print("✅ Redis is already running") click.echo("✅ Redis is already running")
else: else:
print("⚠️ Redis not responding, attempting to start...") click.echo("⚠️ Redis not responding, attempting to start...")
# Start Redis if not running # Start Redis if not running
subprocess.run(['sudo', 'systemctl', 'start', 'redis-server'], check=True) subprocess.run(['sudo', 'systemctl', 'start', 'redis-server'], check=True)
print("✅ Redis started") click.echo("✅ Redis started")
except FileNotFoundError: 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', 'update'], check=True)
subprocess.run(['sudo', 'apt-get', 'install', '-y', 'redis-server'], check=True) subprocess.run(['sudo', 'apt-get', 'install', '-y', 'redis-server'], check=True)
subprocess.run(['sudo', 'systemctl', 'start', '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 # Configure Redis
redis_config = config['edge_node_config']['caching'] redis_config = config['edge_node_config']['caching']
@@ -51,11 +52,11 @@ def deploy_redis_cache(config):
try: try:
subprocess.run(['redis-cli', *cmd.split()], check=True, capture_output=True) subprocess.run(['redis-cli', *cmd.split()], check=True, capture_output=True)
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
print(f"⚠️ Could not set Redis config: {cmd}") click.echo(f"⚠️ Could not set Redis config: {cmd}")
def deploy_monitoring(config): def deploy_monitoring(config):
"""Deploy monitoring agent""" """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'] 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', '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) 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): def optimize_network(config):
"""Apply network optimizations""" """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'] network_config = config['edge_node_config']['network']
@@ -136,13 +137,13 @@ def optimize_network(config):
for param, value in tcp_params.items(): for param, value in tcp_params.items():
try: try:
subprocess.run(['sudo', 'sysctl', '-w', f'{param}={value}'], check=True, capture_output=True) 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: except subprocess.CalledProcessError:
print(f"⚠️ Could not set {param}") click.echo(f"⚠️ Could not set {param}")
def deploy_edge_services(config): def deploy_edge_services(config):
"""Deploy edge node services""" """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 # Create edge service configuration
edge_service_config = { 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: 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) 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): def validate_deployment(config):
"""Validate edge node deployment""" """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 = {} validation_results = {}
@@ -194,29 +195,29 @@ def validate_deployment(config):
except Exception as e: except Exception as e:
validation_results['monitoring'] = f'error: {str(e)}' validation_results['monitoring'] = f'error: {str(e)}'
print(f"📊 Validation Results:") click.echo(f"📊 Validation Results:")
for service, status in validation_results.items(): for service, status in validation_results.items():
print(f" {service}: {status}") click.echo(f" {service}: {status}")
return validation_results return validation_results
def main(): def main():
if len(sys.argv) != 2: if len(sys.argv) != 2:
print("Usage: python deploy_edge_node.py <config_file>") click.echo("Usage: python deploy_edge_node.py <config_file>")
sys.exit(1) sys.exit(1)
config_file = sys.argv[1] config_file = sys.argv[1]
if not os.path.exists(config_file): 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) sys.exit(1)
try: try:
config = load_config(config_file) config = load_config(config_file)
print(f"🚀 Deploying edge node: {config['edge_node_config']['node_id']}") click.echo(f"🚀 Deploying edge node: {config['edge_node_config']['node_id']}")
print(f"📍 Region: {config['edge_node_config']['region']}") click.echo(f"📍 Region: {config['edge_node_config']['region']}")
print(f"🌍 Location: {config['edge_node_config']['location']}") click.echo(f"🌍 Location: {config['edge_node_config']['location']}")
# Deploy components # Deploy components
deploy_redis_cache(config) 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: with open(f'/tmp/aitbc-edge-{config["edge_node_config"]["node_id"]}-deployment.json', 'w') as f:
json.dump(deployment_status, f, indent=2) 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: except Exception as e:
print(f"❌ Deployment failed: {str(e)}") click.echo(f"❌ Deployment failed: {str(e)}")
sys.exit(1) sys.exit(1)
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -9,10 +9,14 @@ Tracks file dependencies and invalidates cache when source files change.
import hashlib import hashlib
import json import json
import os import os
import logging
import click
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional from typing import Dict, List, Optional
import time import time
logger = logging.getLogger(__name__)
class ZKCircuitCache: class ZKCircuitCache:
"""Cache system for ZK circuit compilation artifacts""" """Cache system for ZK circuit compilation artifacts"""
@@ -123,7 +127,7 @@ class ZKCircuitCache:
json.dump(manifest, f, indent=2) json.dump(manifest, f, indent=2)
except Exception as e: 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]: def get_cached_artifacts(self, circuit_file: Path, output_dir: Path) -> Optional[Dict]:
"""Retrieve cached artifacts if valid""" """Retrieve cached artifacts if valid"""
@@ -206,14 +210,14 @@ def main():
if args.action == 'stats': if args.action == 'stats':
stats = cache.get_cache_stats() stats = cache.get_cache_stats()
print(f"Cache Statistics:") click.echo(f"Cache Statistics:")
print(f" Entries: {stats['entries']}") click.echo(f" Entries: {stats['entries']}")
print(f" Total Size: {stats['total_size_mb']:.2f} MB") click.echo(f" Total Size: {stats['total_size_mb']:.2f} MB")
print(f" Cache Directory: {stats['cache_dir']}") click.echo(f" Cache Directory: {stats['cache_dir']}")
elif args.action == 'clear': elif args.action == 'clear':
cache.clear_cache() cache.clear_cache()
print("Cache cleared successfully") click.echo("Cache cleared successfully")
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -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"]

View File

@@ -11,10 +11,23 @@ def read_readme():
with open("docs/README.md", "r", encoding="utf-8") as fh: with open("docs/README.md", "r", encoding="utf-8") as fh:
return fh.read() return fh.read()
# Read requirements # Read requirements from pyproject.toml
def read_requirements(): def read_requirements():
with open("requirements.txt", "r", encoding="utf-8") as fh: import tomli
return [line.strip() for line in fh if line.strip() and not line.startswith("#")] 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( setup(
name="aitbc-cli", name="aitbc-cli",

View File

@@ -11,10 +11,23 @@ def read_readme():
with open("README.md", "r", encoding="utf-8") as fh: with open("README.md", "r", encoding="utf-8") as fh:
return fh.read() return fh.read()
# Read requirements # Read requirements from pyproject.toml
def read_requirements(): def read_requirements():
with open("requirements.txt", "r", encoding="utf-8") as fh: import tomli
return [line.strip() for line in fh if line.strip() and not line.startswith("#")] 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( setup(
name="aitbc-cli", name="aitbc-cli",

View File

@@ -1,3 +0,0 @@
"""AITBC Command Line Interface."""
__version__ = "0.1.0"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,5 +0,0 @@
"""AITBC CLI - Command Line Interface for AITBC Network"""
__version__ = "0.1.0"
__author__ = "AITBC Team"
__email__ = "team@aitbc.net"

View File

@@ -1,3 +0,0 @@
"""AITBC CLI Version Information"""
__version__ = "0.2.2"

View File

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

2
cli/src/aitbc_cli/core/analytics.py Executable file → Normal file
View File

@@ -13,7 +13,7 @@ import statistics
from .config import MultiChainConfig from .config import MultiChainConfig
from .node_client import NodeClient from .node_client import NodeClient
from models.chain import ChainInfo, ChainType, ChainStatus from aitbc.models.chain import ChainInfo, ChainType, ChainStatus
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 {}

2
cli/src/aitbc_cli/core/node_client.py Executable file → Normal file
View File

@@ -9,7 +9,7 @@ import os
import logging import logging
from typing import Dict, List, Optional, Any from typing import Dict, List, Optional, Any
from .config import NodeConfig 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__) logger = logging.getLogger(__name__)

View File

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

View File

@@ -1 +0,0 @@
"""CLI command handlers organized by command group."""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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',
]

View File

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

View File

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

View File

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

View File

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

462
docs/CLI_PACKAGING_PLAN.md Normal file
View File

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

195
docs/SYSPATH_DEBT.md Normal file
View File

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

View File

@@ -15,24 +15,21 @@ def read_readme():
return f.read() return f.read()
return "AITBC Agent SDK - Python package for AI agent network participation" return "AITBC Agent SDK - Python package for AI agent network participation"
# Read requirements # Read requirements from pyproject.toml
def read_requirements(): def read_requirements():
requirements_path = os.path.join(os.path.dirname(__file__), 'requirements.txt') import tomli
if os.path.exists(requirements_path): pyproject_path = os.path.join(os.path.dirname(__file__), 'pyproject.toml')
with open(requirements_path, 'r', encoding='utf-8') as f: if os.path.exists(pyproject_path):
return [line.strip() for line in f if line.strip() and not line.startswith('#')] 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 [ return [
'fastapi>=0.104.0', 'requests>=2.32.4',
'uvicorn>=0.24.0', 'pydantic>=2.11.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'
] ]
setup( setup(

View File

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

View File

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

View File

@@ -137,11 +137,12 @@ install_python_dependencies() {
# Upgrade pip # Upgrade pip
pip install --upgrade pip setuptools wheel pip install --upgrade pip setuptools wheel
# Install requirements # Install using Poetry
if [[ -f "$REPO_ROOT/requirements.txt" ]]; then if [[ -f "$REPO_ROOT/pyproject.toml" ]]; then
pip install -r "$REPO_ROOT/requirements.txt" pip install poetry
cd "$REPO_ROOT" && poetry install
else 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 pip install fastapi uvicorn sqlmodel alembic pydantic httpx requests
fi fi

View File

@@ -7,11 +7,12 @@ Launches AITBC production services from system locations
import os import os
import sys import sys
import subprocess import subprocess
import click
from pathlib import Path from pathlib import Path
def launch_service(service_name: str, script_path: str): def launch_service(service_name: str, script_path: str):
"""Launch a production service""" """Launch a production service"""
print(f"Launching {service_name}...") click.echo(f"Launching {service_name}...")
# Ensure log directory exists # Ensure log directory exists
log_dir = Path(f"/var/log/aitbc/production/{service_name}") 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) str(Path("/opt/aitbc/services") / script_path)
], check=True) ], check=True)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
print(f"Failed to launch {service_name}: {e}") click.echo(f"Failed to launch {service_name}: {e}")
return False return False
except FileNotFoundError: except FileNotFoundError:
print(f"Service script not found: {script_path}") click.echo(f"Service script not found: {script_path}")
return False return False
return True return True
def main(): def main():
"""Main launcher""" """Main launcher"""
print("=== AITBC Production Services Launcher ===") click.echo("=== AITBC Production Services Launcher ===")
services = [ services = [
("blockchain", "blockchain.py"), ("blockchain", "blockchain.py"),
@@ -44,7 +45,7 @@ def main():
for service_name, script_path in services: for service_name, script_path in services:
if not launch_service(service_name, script_path): 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 continue
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -7,6 +7,7 @@ Tracks bug escape rate, test flakiness, and code review coverage
import json import json
import subprocess import subprocess
import os import os
import click
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Dict, Any from typing import Dict, Any
@@ -89,26 +90,26 @@ class QualityMetricsTracker:
def print_report(self): def print_report(self):
"""Print a formatted metrics report""" """Print a formatted metrics report"""
print("=" * 60) click.echo("=" * 60)
print("Quality Metrics Report") click.echo("Quality Metrics Report")
print("=" * 60) click.echo("=" * 60)
print(f"Last Updated: {self.metrics['last_updated']}") click.echo(f"Last Updated: {self.metrics['last_updated']}")
print() click.echo()
print("Bug Escape Rate:") click.echo("Bug Escape Rate:")
print(f" Total Bugs: {self.metrics['bug_escape_rate']['total_bugs']}") click.echo(f" Total Bugs: {self.metrics['bug_escape_rate']['total_bugs']}")
print(f" Escaped Bugs: {self.metrics['bug_escape_rate']['escaped_bugs']}") click.echo(f" Escaped Bugs: {self.metrics['bug_escape_rate']['escaped_bugs']}")
print(f" Escape Rate: {self.metrics['bug_escape_rate']['rate']:.2f}%") click.echo(f" Escape Rate: {self.metrics['bug_escape_rate']['rate']:.2f}%")
print() click.echo()
print("Test Flakiness:") click.echo("Test Flakiness:")
print(f" Total Runs: {self.metrics['test_flakiness']['total_runs']}") click.echo(f" Total Runs: {self.metrics['test_flakiness']['total_runs']}")
print(f" Flaky Runs: {self.metrics['test_flakiness']['flaky_runs']}") click.echo(f" Flaky Runs: {self.metrics['test_flakiness']['flaky_runs']}")
print(f" Flakiness Rate: {self.metrics['test_flakiness']['rate']:.2f}%") click.echo(f" Flakiness Rate: {self.metrics['test_flakiness']['rate']:.2f}%")
print() click.echo()
print("Code Review Coverage:") click.echo("Code Review Coverage:")
print(f" Total PRs: {self.metrics['code_review_coverage']['total_prs']}") click.echo(f" Total PRs: {self.metrics['code_review_coverage']['total_prs']}")
print(f" Reviewed PRs: {self.metrics['code_review_coverage']['reviewed_prs']}") click.echo(f" Reviewed PRs: {self.metrics['code_review_coverage']['reviewed_prs']}")
print(f" Review Coverage: {self.metrics['code_review_coverage']['rate']:.2f}%") click.echo(f" Review Coverage: {self.metrics['code_review_coverage']['rate']:.2f}%")
print("=" * 60) click.echo("=" * 60)
def main(): def main():
@@ -126,24 +127,24 @@ def main():
if command == "bug": if command == "bug":
escaped = "--escaped" in sys.argv escaped = "--escaped" in sys.argv
tracker.record_bug(escaped=escaped) tracker.record_bug(escaped=escaped)
print(f"Recorded bug (escaped={escaped})") click.echo(f"Recorded bug (escaped={escaped})")
elif command == "test": elif command == "test":
flaky = "--flaky" in sys.argv flaky = "--flaky" in sys.argv
tracker.record_test_run(flaky=flaky) tracker.record_test_run(flaky=flaky)
print(f"Recorded test run (flaky={flaky})") click.echo(f"Recorded test run (flaky={flaky})")
elif command == "pr": elif command == "pr":
reviewed = "--reviewed" in sys.argv reviewed = "--reviewed" in sys.argv
tracker.record_pr(reviewed=reviewed) tracker.record_pr(reviewed=reviewed)
print(f"Recorded PR (reviewed={reviewed})") click.echo(f"Recorded PR (reviewed={reviewed})")
elif command == "report": elif command == "report":
tracker.print_report() tracker.print_report()
else: else:
print(f"Unknown command: {command}") click.echo(f"Unknown command: {command}")
print("Available commands: bug, test, pr, report") click.echo("Available commands: bug, test, pr, report")
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -339,8 +339,8 @@ class SecurityAudit:
except Exception as e: except Exception as e:
logger.warning(f"Could not analyze dependencies: {type(e).__name__}") logger.warning(f"Could not analyze dependencies: {type(e).__name__}")
# Check for poetry.lock or requirements.txt # Check for poetry.lock (Poetry source of truth)
lock_files = ["poetry.lock", "requirements.txt"] lock_files = ["poetry.lock"]
has_lock_file = any((self.project_root / f).exists() for f in lock_files) has_lock_file = any((self.project_root / f).exists() for f in lock_files)
if not has_lock_file: if not has_lock_file:

View File

@@ -330,14 +330,15 @@ setup_venvs() {
source /opt/aitbc/venv/bin/activate source /opt/aitbc/venv/bin/activate
fi fi
# Install all dependencies from central requirements.txt # Install all dependencies using Poetry
log "Installing all dependencies from central requirements.txt..." log "Installing all dependencies using Poetry..."
# Install main requirements (contains all service dependencies) # Install using Poetry (source of truth is pyproject.toml)
if [ -f "/opt/aitbc/requirements.txt" ]; then if [ -f "/opt/aitbc/pyproject.toml" ]; then
pip install -r /opt/aitbc/requirements.txt pip install poetry
poetry install
else else
error "Main requirements.txt not found" error "pyproject.toml not found"
fi fi
success "Virtual environments setup completed" success "Virtual environments setup completed"

Some files were not shown because too many files have changed in this diff Show More