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
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:
@@ -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]
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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]:
|
||||||
|
|||||||
@@ -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__":
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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"]
|
|
||||||
19
cli/setup.py
19
cli/setup.py
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
"""AITBC Command Line Interface."""
|
|
||||||
|
|
||||||
__version__ = "0.1.0"
|
|
||||||
@@ -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))
|
|
||||||
@@ -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()
|
|
||||||
@@ -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
|
|
||||||
@@ -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()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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)
|
|
||||||
@@ -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}")
|
|
||||||
@@ -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()
|
|
||||||
@@ -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)}")
|
|
||||||
@@ -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}")
|
|
||||||
@@ -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()
|
|
||||||
@@ -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}")
|
|
||||||
@@ -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()
|
|
||||||
@@ -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
|
|
||||||
@@ -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()
|
|
||||||
@@ -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}")
|
|
||||||
@@ -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
@@ -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}")
|
|
||||||
|
|
||||||
@@ -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
|
|
||||||
@@ -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()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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
@@ -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
|
|
||||||
@@ -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()
|
|
||||||
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
"""AITBC CLI - Command Line Interface for AITBC Network"""
|
|
||||||
|
|
||||||
__version__ = "0.1.0"
|
|
||||||
__author__ = "AITBC Team"
|
|
||||||
__email__ = "team@aitbc.net"
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""AITBC CLI Version Information"""
|
|
||||||
|
|
||||||
__version__ = "0.2.2"
|
|
||||||
@@ -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
2
cli/src/aitbc_cli/core/analytics.py
Executable file → Normal 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__)
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
|
||||||
@@ -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()
|
|
||||||
@@ -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
|
|
||||||
@@ -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))
|
|
||||||
@@ -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())
|
|
||||||
@@ -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
2
cli/src/aitbc_cli/core/node_client.py
Executable file → Normal 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__)
|
||||||
|
|
||||||
|
|||||||
@@ -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'))
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
"""CLI command handlers organized by command group."""
|
|
||||||
@@ -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)
|
|
||||||
@@ -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")
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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}")
|
|
||||||
@@ -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}")
|
|
||||||
@@ -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
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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}")
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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()
|
|
||||||
@@ -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
|
|
||||||
@@ -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)
|
|
||||||
@@ -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")
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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")
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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',
|
|
||||||
]
|
|
||||||
@@ -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
|
|
||||||
@@ -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)
|
|
||||||
@@ -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
|
|
||||||
@@ -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
462
docs/CLI_PACKAGING_PLAN.md
Normal 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
195
docs/SYSPATH_DEBT.md
Normal 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
|
||||||
@@ -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(
|
||||||
|
|||||||
118
requirements.txt
118
requirements.txt
@@ -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
|
|
||||||
@@ -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()
|
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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__":
|
||||||
|
|||||||
@@ -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__":
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user