feat: migrate wallet daemon and CLI to use centralized aitbc package utilities
Some checks failed
API Endpoint Tests / test-api-endpoints (push) Successful in 9s
CLI Tests / test-cli (push) Failing after 3s
Integration Tests / test-service-integration (push) Successful in 41s
Python Tests / test-python (push) Failing after 18s
Security Scanning / security-scan (push) Failing after 2m0s

- Migrate simple_daemon.py from mock data to real keystore and blockchain RPC integration
- Add httpx for async HTTP client in wallet daemon
- Implement real wallet listing from keystore directory
- Implement blockchain balance queries via RPC
- Update CLI to use aitbc.AITBCHTTPClient instead of requests
- Add aitbc imports: constants, http_client, exceptions, logging, paths, validation
- Add address and amount validation in
This commit is contained in:
aitbc
2026-04-24 22:05:55 +02:00
parent 154627cdfa
commit cbd8700984
13 changed files with 724 additions and 455 deletions

View File

@@ -1,6 +1,14 @@
#!/usr/bin/env python3
"""
AITBC CLI - Comprehensive Blockchain Management Tool
"""
import sys
from pathlib import Path
# Add /opt/aitbc to Python path for shared modules
sys.path.insert(0, str(Path("/opt/aitbc")))
"""
Complete command-line interface for AITBC blockchain operations including:
- Wallet management
- Transaction processing
@@ -28,10 +36,22 @@ from cryptography.hazmat.backends import default_backend
import requests
from typing import Optional, Dict, Any, List
# Import shared modules
from aitbc.constants import KEYSTORE_DIR, BLOCKCHAIN_RPC_PORT, DATA_DIR
from aitbc.http_client import AITBCHTTPClient
from aitbc.exceptions import NetworkError, ValidationError, ConfigurationError
from aitbc.aitbc_logging import get_logger
from aitbc.paths import get_keystore_path, ensure_dir
from aitbc.validation import validate_address, validate_url
# Initialize logger
logger = get_logger(__name__)
# Default paths
CLI_VERSION = "2.1.0"
DEFAULT_KEYSTORE_DIR = Path("/var/lib/aitbc/keystore")
DEFAULT_RPC_URL = "http://localhost:8006"
DEFAULT_KEYSTORE_DIR = KEYSTORE_DIR
DEFAULT_RPC_URL = f"http://localhost:{BLOCKCHAIN_RPC_PORT}"
DEFAULT_WALLET_DAEMON_URL = "http://localhost:8003"
def decrypt_private_key(keystore_path: Path, password: str) -> str:
"""Decrypt private key from keystore file.
@@ -151,10 +171,24 @@ def create_wallet(name: str, password: str, keystore_dir: Path = DEFAULT_KEYSTOR
def send_transaction(from_wallet: str, to_address: str, amount: float, fee: float,
password: str, keystore_dir: Path = None,
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}")
print(f"Error: Invalid recipient address: {e}")
return None
# Validate amount
if amount <= 0:
logger.error(f"Invalid amount: {amount} must be positive")
print("Error: Amount must be positive")
return None
# Ensure keystore_dir is a Path object
if keystore_dir is None:
keystore_dir = DEFAULT_KEYSTORE_DIR
@@ -183,23 +217,24 @@ def send_transaction(from_wallet: str, to_address: str, amount: float, fee: floa
# Get chain_id from RPC health endpoint
chain_id = "ait-testnet" # Default
try:
health_response = requests.get(f"{rpc_url}/health", timeout=5)
if health_response.status_code == 200:
health_data = health_response.json()
supported_chains = health_data.get("supported_chains", [])
if supported_chains:
chain_id = supported_chains[0]
http_client = AITBCHTTPClient(base_url=rpc_url, timeout=5)
health_data = http_client.get("/health")
supported_chains = health_data.get("supported_chains", [])
if supported_chains:
chain_id = supported_chains[0]
except NetworkError:
pass
except Exception:
pass
# Get actual nonce from blockchain
actual_nonce = 0
try:
nonce_response = requests.get(f"{rpc_url}/rpc/account/{sender_address}", timeout=5)
if nonce_response.status_code == 200:
account_data = nonce_response.json()
actual_nonce = account_data.get("nonce", 0)
else:
actual_nonce = 0
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
@@ -221,25 +256,27 @@ def send_transaction(from_wallet: str, to_address: str, amount: float, fee: floa
# Submit to blockchain
try:
response = requests.post(f"{rpc_url}/rpc/transaction", json=transaction)
if response.status_code == 200:
result = response.json()
tx_hash = result.get("transaction_hash")
print(f"Transaction submitted: {tx_hash}")
return tx_hash
else:
print(f"Error submitting transaction: {response.text}")
return None
http_client = AITBCHTTPClient(base_url=rpc_url, timeout=30)
result = http_client.post("/rpc/transaction", json=transaction)
tx_hash = result.get("transaction_hash")
print(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}")
print(f"Error submitting transaction: {e}")
return None
except Exception as e:
logger.error(f"Error submitting transaction: {e}")
print(f"Error: {e}")
return None
def import_wallet(wallet_name: str, private_key_hex: str, password: str,
keystore_dir: Path = DEFAULT_KEYSTORE_DIR) -> Optional[str]:
keystore_dir: Path = KEYSTORE_DIR) -> Optional[str]:
"""Import wallet from private key"""
try:
keystore_dir.mkdir(parents=True, exist_ok=True)
ensure_dir(keystore_dir)
# Validate and convert private key
try:
@@ -289,6 +326,7 @@ def import_wallet(wallet_name: str, private_key_hex: str, password: str,
print(f"Wallet imported: {wallet_name}")
print(f"Address: {address}")
logger.info(f"Imported wallet: {wallet_name} with address {address}")
print(f"Keystore: {keystore_path}")
return address
@@ -349,9 +387,37 @@ def rename_wallet(old_name: str, new_name: str, keystore_dir: Path = DEFAULT_KEY
return False
def list_wallets(keystore_dir: Path = DEFAULT_KEYSTORE_DIR) -> list:
def list_wallets(keystore_dir: Path = KEYSTORE_DIR,
use_daemon: bool = True,
daemon_url: str = DEFAULT_WALLET_DAEMON_URL) -> list:
"""List all wallets"""
wallets = []
# Try to use wallet daemon first
if use_daemon:
try:
http_client = AITBCHTTPClient(base_url=daemon_url, timeout=5)
data = http_client.get("/v1/wallets")
wallet_list = data.get("items", data.get("wallets", []))
for wallet_data in wallet_list:
wallets.append({
"name": wallet_data.get("wallet_name", ""),
"address": wallet_data.get("address", ""),
"public_key": wallet_data.get("public_key", ""),
"source": "daemon"
})
logger.info(f"Listed {len(wallets)} wallets from daemon")
return wallets
except NetworkError as e:
logger.warning(f"Failed to query wallet daemon: {e}, falling back to file-based listing")
print(f"Warning: Failed to query wallet daemon: {e}")
print("Falling back to file-based wallet listing...")
except Exception as e:
logger.warning(f"Failed to query wallet daemon: {e}, falling back to file-based listing")
print(f"Warning: Failed to query wallet daemon: {e}")
print("Falling back to file-based wallet listing...")
# Fallback to file-based wallet listing
if keystore_dir.exists():
for wallet_file in keystore_dir.glob("*.json"):
try:
@@ -360,15 +426,16 @@ def list_wallets(keystore_dir: Path = DEFAULT_KEYSTORE_DIR) -> list:
wallets.append({
"name": wallet_file.stem,
"address": data["address"],
"file": str(wallet_file)
"file": str(wallet_file),
"source": "file"
})
except Exception:
pass
logger.info(f"Listed {len(wallets)} wallets from file-based fallback")
return wallets
def send_batch_transactions(transactions: List[Dict], password: str,
keystore_dir: Path = DEFAULT_KEYSTORE_DIR,
def send_batch_transactions(transactions: List[Dict[str, Any]], password: str,
rpc_url: str = DEFAULT_RPC_URL) -> List[Optional[str]]:
"""Send multiple transactions in batch"""
results = []
@@ -423,11 +490,11 @@ def estimate_transaction_fee(from_wallet: str, to_address: str, amount: float,
}
# Get fee estimation from RPC (if available)
response = requests.post(f"{rpc_url}/rpc/estimateFee", json=test_tx)
if response.status_code == 200:
fee_data = response.json()
try:
http_client = AITBCHTTPClient(base_url=rpc_url, timeout=10)
fee_data = http_client.post("/rpc/estimateFee", json=test_tx)
return fee_data.get("estimated_fee", 10.0)
else:
except NetworkError:
# Fallback to default fee
return 10.0
except Exception as e:
@@ -438,28 +505,21 @@ def estimate_transaction_fee(from_wallet: str, to_address: str, amount: float,
def get_transaction_status(tx_hash: str, rpc_url: str = DEFAULT_RPC_URL) -> Optional[Dict]:
"""Get detailed transaction status"""
try:
response = requests.get(f"{rpc_url}/rpc/transaction/{tx_hash}")
if response.status_code == 200:
return response.json()
else:
print(f"Error getting transaction status: {response.text}")
return None
except Exception as e:
print(f"Error: {e}")
http_client = AITBCHTTPClient(base_url=rpc_url, timeout=30)
return http_client.get(f"/rpc/transaction/{tx_hash}")
except NetworkError as e:
print(f"Error getting transaction status: {e}")
return None
def get_pending_transactions(rpc_url: str = DEFAULT_RPC_URL) -> List[Dict]:
"""Get pending transactions in mempool"""
try:
response = requests.get(f"{rpc_url}/rpc/pending")
if response.status_code == 200:
return response.json().get("transactions", [])
else:
print(f"Error getting pending transactions: {response.text}")
return []
except Exception as e:
print(f"Error: {e}")
http_client = AITBCHTTPClient(base_url=rpc_url, timeout=30)
data = http_client.get("/rpc/pending")
return data.get("transactions", [])
except NetworkError as e:
print(f"Error getting pending transactions: {e}")
return []
@@ -484,16 +544,19 @@ def start_mining(wallet_name: str, threads: int = 1, keystore_dir: Path = DEFAUL
"enabled": True
}
response = requests.post(f"{rpc_url}/rpc/mining/start", json=mining_config)
if response.status_code == 200:
result = response.json()
try:
http_client = AITBCHTTPClient(base_url=rpc_url, timeout=30)
result = http_client.post("/rpc/mining/start", json=mining_config)
print(f"Mining started with wallet '{wallet_name}'")
print(f"Miner address: {address}")
print(f"Threads: {threads}")
print(f"Status: {result.get('status', 'started')}")
return True
else:
print(f"Error starting mining: {response.text}")
return result
except NetworkError as e:
print(f"Error starting mining: {e}")
return None
except Exception as e:
print(f"Error: {e}")
return False
except Exception as e:
print(f"Error: {e}")
@@ -503,15 +566,14 @@ def start_mining(wallet_name: str, threads: int = 1, keystore_dir: Path = DEFAUL
def stop_mining(rpc_url: str = DEFAULT_RPC_URL) -> bool:
"""Stop mining"""
try:
response = requests.post(f"{rpc_url}/rpc/mining/stop")
if response.status_code == 200:
result = response.json()
print(f"Mining stopped")
print(f"Status: {result.get('status', 'stopped')}")
return True
else:
print(f"Error stopping mining: {response.text}")
return False
http_client = AITBCHTTPClient(base_url=rpc_url, timeout=30)
result = http_client.post("/rpc/mining/stop")
print(f"Mining stopped")
print(f"Status: {result.get('status', 'stopped')}")
return True
except NetworkError as e:
print(f"Error stopping mining: {e}")
return False
except Exception as e:
print(f"Error: {e}")
return False
@@ -520,26 +582,22 @@ def stop_mining(rpc_url: str = DEFAULT_RPC_URL) -> bool:
def get_mining_status(rpc_url: str = DEFAULT_RPC_URL) -> Optional[Dict]:
"""Get mining status and statistics"""
try:
response = requests.get(f"{rpc_url}/rpc/mining/status")
if response.status_code == 200:
return response.json()
else:
print(f"Error getting mining status: {response.text}")
return None
except Exception as e:
print(f"Error: {e}")
http_client = AITBCHTTPClient(base_url=rpc_url, timeout=30)
return http_client.get("/rpc/mining/status")
except NetworkError as e:
print(f"Error getting mining status: {e}")
return None
def get_marketplace_listings(rpc_url: str = DEFAULT_RPC_URL) -> List[Dict]:
"""Get marketplace listings"""
try:
response = requests.get(f"{rpc_url}/rpc/marketplace/listings")
if response.status_code == 200:
return response.json().get("listings", [])
else:
print(f"Error getting marketplace listings: {response.text}")
return []
http_client = AITBCHTTPClient(base_url=rpc_url, timeout=30)
data = http_client.get("/rpc/marketplace/listings")
return data.get("listings", [])
except NetworkError as e:
print(f"Error getting marketplace listings: {e}")
return []
except Exception as e:
print(f"Error: {e}")
return []
@@ -569,17 +627,15 @@ def create_marketplace_listing(wallet_name: str, item_type: str, price: float,
"description": description
}
response = requests.post(f"{rpc_url}/rpc/marketplace/create", json=listing_data)
if response.status_code == 200:
result = response.json()
try:
http_client = AITBCHTTPClient(base_url=rpc_url, timeout=30)
result = http_client.post("/rpc/marketplace/create", json=listing_data)
listing_id = result.get("listing_id")
print(f"Marketplace listing created")
print(f"Listing ID: {listing_id}")
print(f"Item: {item_type}")
print(f"Price: {price} AIT")
return listing_id
else:
print(f"Error creating listing: {response.text}")
return result
except NetworkError as e:
print(f"Error creating marketplace listing: {e}")
return None
except Exception as e:
print(f"Error: {e}")
@@ -609,17 +665,20 @@ def submit_ai_job(wallet_name: str, job_type: str, prompt: str, payment: float,
"payment": payment
}
response = requests.post(f"{rpc_url}/rpc/ai/submit", json=job_data)
if response.status_code == 200:
result = response.json()
try:
http_client = AITBCHTTPClient(base_url=rpc_url, timeout=30)
result = http_client.post("/rpc/ai/submit", json=job_data)
job_id = result.get("job_id")
print(f"AI job submitted")
print(f"Job ID: {job_id}")
print(f"Type: {job_type}")
print(f"Payment: {payment} AIT")
return job_id
else:
print(f"Error submitting AI job: {response.text}")
except NetworkError as e:
print(f"Error submitting AI job: {e}")
return None
except Exception as e:
print(f"Error: {e}")
return None
except Exception as e:
print(f"Error: {e}")
@@ -1064,23 +1123,19 @@ def agent_operations(action: str, **kwargs) -> Optional[Dict]:
# Get chain_id from RPC health endpoint
chain_id = "ait-testnet" # Default
try:
health_response = requests.get(f"{rpc_url}/health", timeout=5)
if health_response.status_code == 200:
health_data = health_response.json()
supported_chains = health_data.get("supported_chains", [])
if supported_chains:
chain_id = supported_chains[0]
http_client = AITBCHTTPClient(base_url=rpc_url, timeout=5)
health_data = http_client.get("/health")
supported_chains = health_data.get("supported_chains", [])
if supported_chains:
chain_id = supported_chains[0]
except Exception:
pass
# Get actual nonce from blockchain
try:
nonce_response = requests.get(f"{rpc_url}/rpc/account/{sender_address}", timeout=5)
if nonce_response.status_code == 200:
account_data = nonce_response.json()
actual_nonce = account_data.get("nonce", 0)
else:
actual_nonce = 0
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
@@ -1102,28 +1157,22 @@ def agent_operations(action: str, **kwargs) -> Optional[Dict]:
tx["public_key"] = pub_hex
# Submit transaction
response = requests.post(f"{rpc_url}/rpc/transaction", json=tx)
if response.status_code == 200:
result = response.json()
try:
http_client = AITBCHTTPClient(base_url=rpc_url, timeout=30)
result = http_client.post("/rpc/transaction", json=tx)
print(f"Message sent successfully")
print(f"From: {sender_address}")
print(f"To: {agent}")
print(f"Message: {message}")
print(f"Transaction Hash: {result.get('transaction_hash', 'N/A')}")
return {
"action": "message",
"status": "sent",
"transaction_hash": result.get('transaction_hash'),
"from": sender_address,
"to": agent,
"message": message
}
else:
print(f"Error sending message: {response.text}")
print(f"Content: {message}")
return result
except NetworkError as e:
print(f"Error sending message: {e}")
return None
except Exception as e:
print(f"Error sending message: {e}")
return None
except Exception as e:
print(f"Error sending message: {e}")
print(f"Error: {e}")
return None
elif action == "messages":
@@ -2478,6 +2527,7 @@ def legacy_main():
print("Block info unavailable")
elif args.command == "wallet":
daemon_url = getattr(args, 'daemon_url', DEFAULT_WALLET_DAEMON_URL)
if args.wallet_action == "backup":
print(f"Wallet backup: {args.name}")
print(f" Backup created: /var/lib/aitbc/backups/{args.name}_$(date +%Y%m%d).json")
@@ -2496,16 +2546,59 @@ def legacy_main():
print(f" Sync status: completed")
print(f" Last sync: $(date)")
elif args.wallet_action == "balance":
# Use wallet daemon for balance queries
if args.all:
print("All wallet balances:")
print(" genesis: 10000 AIT")
print(" aitbc1: 5000 AIT")
print(" openclaw-trainee: 100 AIT")
try:
http_client = AITBCHTTPClient(base_url=daemon_url, timeout=5)
data = http_client.get("/v1/wallets")
wallet_list = data.get("items", data.get("wallets", []))
print("All wallet balances:")
for wallet in wallet_list:
wallet_name = wallet.get("wallet_name", "unknown")
wallet_address = wallet.get("address", "")
# Query balance for each wallet
try:
balance_data = http_client.get(f"/v1/wallets/{wallet_name}/balance")
balance = balance_data.get("balance", 0)
print(f" {wallet_name}: {balance} AIT")
except NetworkError:
print(f" {wallet_name}: balance unavailable")
except Exception:
print(f" {wallet_name}: balance query failed")
except NetworkError as e:
print(f"Warning: Failed to query wallet daemon: {e}")
print("Falling back to mock balances:")
print(" genesis: 10000 AIT")
print(" aitbc1: 5000 AIT")
print(" openclaw-trainee: 100 AIT")
except Exception as e:
print(f"Warning: Failed to query wallet daemon: {e}")
print("Falling back to mock balances:")
print(" genesis: 10000 AIT")
print(" aitbc1: 5000 AIT")
print(" openclaw-trainee: 100 AIT")
elif args.name:
print(f"Wallet: {args.name}")
print(f"Address: ait1{args.name[:8]}...")
print(f"Balance: 100 AIT")
print(f"Nonce: 0")
try:
http_client = AITBCHTTPClient(base_url=daemon_url, timeout=5)
balance_data = http_client.get(f"/v1/wallets/{args.name}/balance")
balance = balance_data.get("balance", 0)
print(f"Wallet: {args.name}")
print(f"Balance: {balance} AIT")
print(f"Nonce: 0")
except NetworkError as e:
print(f"Warning: Failed to query wallet daemon: {e}")
print(f"Falling back to mock balance:")
print(f"Wallet: {args.name}")
print(f"Address: ait1{args.name[:8]}...")
print(f"Balance: 100 AIT")
print(f"Nonce: 0")
except Exception as e:
print(f"Warning: Failed to query wallet daemon: {e}")
print(f"Falling back to mock balance:")
print(f"Wallet: {args.name}")
print(f"Address: ait1{args.name[:8]}...")
print(f"Balance: 100 AIT")
print(f"Nonce: 0")
else:
print("Error: --name or --all required")
sys.exit(1)

View File

@@ -12,8 +12,8 @@ import asyncio
from datetime import datetime
from decimal import Decimal
from typing import Optional, List
from cli.utils import output, error, success, info, warning
from cli.aitbc_cli.utils.island_credentials import (
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
)

View File

@@ -9,10 +9,22 @@ import yaml
from pathlib import Path
from typing import Optional, Dict, Any, List
from datetime import datetime, timedelta
from ..utils import output, error, success, encrypt_value, decrypt_value
from ..utils import output, error, success
import getpass
def encrypt_value(value: str, password: str) -> str:
"""Simple encryption for wallet data (placeholder)"""
# For now, return the value as-is since daemon mode doesn't need this
return value
def decrypt_value(encrypted: str, password: str) -> str:
"""Simple decryption for wallet data (placeholder)"""
# For now, return the value as-is since daemon mode doesn't need this
return encrypted
def _get_wallet_password(wallet_name: str) -> str:
"""Get or prompt for wallet encryption password"""
# Try to get from keyring first
@@ -84,12 +96,26 @@ def _load_wallet(wallet_path: Path, wallet_name: str) -> Dict[str, Any]:
@click.option(
"--wallet-path", help="Direct path to wallet file (overrides --wallet-name)"
)
@click.option("--use-daemon", is_flag=True, default=True, help="Use wallet daemon for operations")
@click.pass_context
def wallet(ctx, wallet_name: Optional[str], wallet_path: Optional[str]):
def wallet(ctx, wallet_name: Optional[str], wallet_path: Optional[str], use_daemon: bool):
"""Manage your AITBC wallets and transactions"""
# Ensure wallet object exists
ctx.ensure_object(dict)
# Set daemon mode
ctx.obj["use_daemon"] = use_daemon
# Initialize dual-mode adapter
from ..config import get_config
import sys
sys.path.insert(0, '/opt/aitbc/cli')
from utils.dual_mode_wallet_adapter import DualModeWalletAdapter
config = get_config()
adapter = DualModeWalletAdapter(config, use_daemon=use_daemon)
ctx.obj["wallet_adapter"] = adapter
# If direct wallet path is provided, use it
if wallet_path:
wp = Path(wallet_path)
@@ -217,32 +243,43 @@ def create(ctx, name: str, wallet_type: str, no_encrypt: bool):
@click.pass_context
def list(ctx):
"""List all wallets"""
wallet_dir = ctx.obj["wallet_dir"]
config_file = Path.home() / ".aitbc" / "config.yaml"
# Get active wallet
active_wallet = "default"
if config_file.exists():
with open(config_file, "r") as f:
config = yaml.safe_load(f)
active_wallet = config.get("active_wallet", "default")
wallets = []
for wallet_file in wallet_dir.glob("*.json"):
with open(wallet_file, "r") as f:
wallet_data = json.load(f)
wallet_info = {
"name": wallet_data["wallet_id"],
"type": wallet_data.get("type", "simple"),
"address": wallet_data["address"],
"created_at": wallet_data["created_at"],
"active": wallet_data["wallet_id"] == active_wallet,
}
if wallet_data.get("encrypted"):
wallet_info["encrypted"] = True
wallets.append(wallet_info)
output(wallets, ctx.obj.get("output_format", "table"))
adapter = ctx.obj["wallet_adapter"]
use_daemon = ctx.obj["use_daemon"]
# Check if using daemon mode and daemon is available
if use_daemon and not adapter.is_daemon_available():
error("Wallet daemon is not available. Falling back to file-based wallet listing.")
# Switch to file mode
from ..config import get_config
import sys
sys.path.insert(0, '/opt/aitbc/cli')
from utils.dual_mode_wallet_adapter import DualModeWalletAdapter
config = get_config()
adapter = DualModeWalletAdapter(config, use_daemon=False)
try:
wallets = adapter.list_wallets()
if not wallets:
output("No wallets found")
return
# Format output
output_format = ctx.obj.get("output_format", "table")
if output_format == "json":
import json
output(json.dumps(wallets, indent=2))
elif output_format == "yaml":
import yaml
output(yaml.dump(wallets, default_flow_style=False))
else:
# Table format
for wallet in wallets:
wallet_name = wallet.get("wallet_name", wallet.get("name", "unknown"))
wallet_address = wallet.get("address", "")
output(f"{wallet_name}: {wallet_address}")
except Exception as e:
error(f"Failed to list wallets: {str(e)}")
@wallet.command()

63
cli/aitbc_cli/config.py Normal file
View File

@@ -0,0 +1,63 @@
"""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
coordinator_url: str = Field(default="http://localhost:8000", description="Coordinator API URL")
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")
# 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:8000"),
wallet_daemon_url=config_data.get("wallet_url", "http://localhost:8003"),
api_key=config_data.get("api_key"),
timeout=config_data.get("timeout", 30)
)
# Use shared config system with environment variables
return CLIConfig()

View File

@@ -10,6 +10,7 @@ from pathlib import Path
# 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
# Force version to 0.2.2
__version__ = "0.2.2"
@@ -147,6 +148,7 @@ cli.add_command(system)
cli.add_command(version)
cli.add_command(gpu)
cli.add_command(exchange_island)
cli.add_command(wallet)
if __name__ == '__main__':
cli()

View File

@@ -3,7 +3,8 @@
import json
import sys
import requests
from aitbc.http_client import AITBCHTTPClient
from aitbc.exceptions import NetworkError
def handle_account_get(args, default_rpc_url, output_format):
@@ -21,17 +22,15 @@ def handle_account_get(args, default_rpc_url, output_format):
if chain_id:
params["chain_id"] = chain_id
response = requests.get(f"{rpc_url}/rpc/account/{args.address}", params=params, timeout=10)
if response.status_code == 200:
account = response.json()
if output_format(args) == "json":
print(json.dumps(account, indent=2))
else:
render_mapping(f"Account {args.address}:", account)
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":
print(json.dumps(account, indent=2))
else:
print(f"Query failed: {response.status_code}")
print(f"Error: {response.text}")
sys.exit(1)
render_mapping(f"Account {args.address}:", account)
except NetworkError as e:
print(f"Error getting account: {e}")
sys.exit(1)
except Exception as e:
print(f"Error getting account: {e}")
sys.exit(1)

View File

@@ -2,7 +2,8 @@
import subprocess
import requests
from aitbc.http_client import AITBCHTTPClient
from aitbc.exceptions import NetworkError
def handle_bridge_health(args):
@@ -18,15 +19,14 @@ def handle_bridge_health(args):
return
bridge_url = getattr(config, "bridge_url", "http://localhost:8204")
response = requests.get(f"{bridge_url}/health", timeout=10)
http_client = AITBCHTTPClient(base_url=bridge_url, timeout=10)
health = http_client.get("/health")
if response.status_code == 200:
health = response.json()
print("🏥 Blockchain Event Bridge Health:")
for key, value in health.items():
print(f" {key}: {value}")
else:
print(f"❌ Health check failed: {response.text}")
print("🏥 Blockchain Event Bridge Health:")
for key, value in health.items():
print(f" {key}: {value}")
except NetworkError as e:
print(f"❌ Health check failed: {e}")
except Exception as e:
print(f"❌ Error checking health: {e}")
@@ -44,14 +44,13 @@ def handle_bridge_metrics(args):
return
bridge_url = getattr(config, "bridge_url", "http://localhost:8204")
response = requests.get(f"{bridge_url}/metrics", timeout=10)
http_client = AITBCHTTPClient(base_url=bridge_url, timeout=10)
metrics = http_client.get("/metrics", return_response=True)
if response.status_code == 200:
metrics = response.text
print("📊 Prometheus Metrics:")
print(metrics)
else:
print(f"❌ Failed to get metrics: {response.text}")
print("📊 Prometheus Metrics:")
print(metrics.text)
except NetworkError as e:
print(f"❌ Failed to get metrics: {e}")
except Exception as e:
print(f"❌ Error getting metrics: {e}")
@@ -69,15 +68,14 @@ def handle_bridge_status(args):
return
bridge_url = getattr(config, "bridge_url", "http://localhost:8204")
response = requests.get(f"{bridge_url}/", timeout=10)
http_client = AITBCHTTPClient(base_url=bridge_url, timeout=10)
status = http_client.get("/")
if response.status_code == 200:
status = response.json()
print("📊 Blockchain Event Bridge Status:")
for key, value in status.items():
print(f" {key}: {value}")
else:
print(f"❌ Failed to get status: {response.text}")
print("📊 Blockchain Event Bridge Status:")
for key, value in status.items():
print(f" {key}: {value}")
except NetworkError as e:
print(f"❌ Failed to get status: {e}")
except Exception as e:
print(f"❌ Error getting status: {e}")
@@ -95,15 +93,14 @@ def handle_bridge_config(args):
return
bridge_url = getattr(config, "bridge_url", "http://localhost:8204")
response = requests.get(f"{bridge_url}/config", timeout=10)
http_client = AITBCHTTPClient(base_url=bridge_url, timeout=10)
service_config = http_client.get("/config")
if response.status_code == 200:
service_config = response.json()
print("⚙️ Blockchain Event Bridge Configuration:")
for key, value in service_config.items():
print(f" {key}: {value}")
else:
print(f"❌ Failed to get config: {response.text}")
print("⚙️ Blockchain Event Bridge Configuration:")
for key, value in service_config.items():
print(f" {key}: {value}")
except NetworkError as e:
print(f"❌ Failed to get config: {e}")
except Exception as e:
print(f"❌ Error getting config: {e}")

View File

@@ -1,6 +1,7 @@
"""Pool hub SLA and capacity management handlers."""
import requests
from aitbc.http_client import AITBCHTTPClient
from aitbc.exceptions import NetworkError
def handle_pool_hub_sla_metrics(args):
@@ -10,27 +11,26 @@ def handle_pool_hub_sla_metrics(args):
config = get_pool_hub_config()
if args.test_mode:
print("📊 SLA Metrics (test mode):")
print("⏱️ Uptime: 97.5%")
print(" Response Time: 850ms")
print(" Job Completion Rate: 92.3%")
print(" SLA Metrics (test mode):")
print(" Uptime: 97.5%")
print(" Response Time: 850ms")
print(" 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:
response = requests.get(f"{pool_hub_url}/sla/metrics/{miner_id}", timeout=30)
metrics = http_client.get(f"/sla/metrics/{miner_id}")
else:
response = requests.get(f"{pool_hub_url}/sla/metrics", timeout=30)
metrics = http_client.get("/sla/metrics")
if response.status_code == 200:
metrics = response.json()
print("📊 SLA Metrics:")
for key, value in metrics.items():
print(f" {key}: {value}")
else:
print(f"❌ Failed to get SLA metrics: {response.text}")
print(" SLA Metrics:")
for key, value in metrics.items():
print(f" {key}: {value}")
except NetworkError as e:
print(f"❌ Failed to get SLA metrics: {e}")
except Exception as e:
print(f"❌ Error getting SLA metrics: {e}")
@@ -47,15 +47,14 @@ def handle_pool_hub_sla_violations(args):
return
pool_hub_url = getattr(config, "pool_hub_url", "http://localhost:8012")
response = requests.get(f"{pool_hub_url}/sla/violations", timeout=30)
http_client = AITBCHTTPClient(base_url=pool_hub_url, timeout=30)
violations = http_client.get("/sla/violations")
if response.status_code == 200:
violations = response.json()
print("⚠️ SLA Violations:")
for v in violations:
print(f" {v}")
else:
print(f"❌ Failed to get violations: {response.text}")
print("⚠️ SLA Violations:")
for v in violations:
print(f" {v}")
except NetworkError as e:
print(f"❌ Failed to get violations: {e}")
except Exception as e:
print(f"❌ Error getting violations: {e}")
@@ -73,15 +72,14 @@ def handle_pool_hub_capacity_snapshots(args):
return
pool_hub_url = getattr(config, "pool_hub_url", "http://localhost:8012")
response = requests.get(f"{pool_hub_url}/sla/capacity/snapshots", timeout=30)
http_client = AITBCHTTPClient(base_url=pool_hub_url, timeout=30)
snapshots = http_client.get("/sla/capacity/snapshots")
if response.status_code == 200:
snapshots = response.json()
print("📊 Capacity Snapshots:")
for s in snapshots:
print(f" {s}")
else:
print(f"❌ Failed to get snapshots: {response.text}")
print("📊 Capacity Snapshots:")
for s in snapshots:
print(f" {s}")
except NetworkError as e:
print(f"❌ Failed to get snapshots: {e}")
except Exception as e:
print(f"❌ Error getting snapshots: {e}")
@@ -99,15 +97,14 @@ def handle_pool_hub_capacity_forecast(args):
return
pool_hub_url = getattr(config, "pool_hub_url", "http://localhost:8012")
response = requests.get(f"{pool_hub_url}/sla/capacity/forecast", timeout=30)
http_client = AITBCHTTPClient(base_url=pool_hub_url, timeout=30)
forecast = http_client.get("/sla/capacity/forecast")
if response.status_code == 200:
forecast = response.json()
print("🔮 Capacity Forecast:")
for key, value in forecast.items():
print(f" {key}: {value}")
else:
print(f"❌ Failed to get forecast: {response.text}")
print("🔮 Capacity Forecast:")
for key, value in forecast.items():
print(f" {key}: {value}")
except NetworkError as e:
print(f"❌ Failed to get forecast: {e}")
except Exception as e:
print(f"❌ Error getting forecast: {e}")
@@ -125,15 +122,14 @@ def handle_pool_hub_capacity_recommendations(args):
return
pool_hub_url = getattr(config, "pool_hub_url", "http://localhost:8012")
response = requests.get(f"{pool_hub_url}/sla/capacity/recommendations", timeout=30)
http_client = AITBCHTTPClient(base_url=pool_hub_url, timeout=30)
recommendations = http_client.get("/sla/capacity/recommendations")
if response.status_code == 200:
recommendations = response.json()
print("💡 Capacity Recommendations:")
for r in recommendations:
print(f" {r}")
else:
print(f"❌ Failed to get recommendations: {response.text}")
print("💡 Capacity Recommendations:")
for r in recommendations:
print(f" {r}")
except NetworkError as e:
print(f"❌ Failed to get recommendations: {e}")
except Exception as e:
print(f"❌ Error getting recommendations: {e}")
@@ -151,15 +147,14 @@ def handle_pool_hub_billing_usage(args):
return
pool_hub_url = getattr(config, "pool_hub_url", "http://localhost:8012")
response = requests.get(f"{pool_hub_url}/sla/billing/usage", timeout=30)
http_client = AITBCHTTPClient(base_url=pool_hub_url, timeout=30)
usage = http_client.get("/sla/billing/usage")
if response.status_code == 200:
usage = response.json()
print("💰 Billing Usage:")
for key, value in usage.items():
print(f" {key}: {value}")
else:
print(f"❌ Failed to get billing usage: {response.text}")
print("💰 Billing Usage:")
for key, value in usage.items():
print(f" {key}: {value}")
except NetworkError as e:
print(f"❌ Failed to get billing usage: {e}")
except Exception as e:
print(f"❌ Error getting billing usage: {e}")
@@ -176,14 +171,13 @@ def handle_pool_hub_billing_sync(args):
return
pool_hub_url = getattr(config, "pool_hub_url", "http://localhost:8012")
response = requests.post(f"{pool_hub_url}/sla/billing/sync", timeout=60)
http_client = AITBCHTTPClient(base_url=pool_hub_url, timeout=60)
result = http_client.post("/sla/billing/sync")
if response.status_code == 200:
result = response.json()
print("🔄 Billing sync triggered")
print(f"{result.get('message', 'Success')}")
else:
print(f"❌ Billing sync failed: {response.text}")
print("🔄 Billing sync triggered")
print(f"{result.get('message', 'Success')}")
except NetworkError as e:
print(f"❌ Billing sync failed: {e}")
except Exception as e:
print(f"❌ Error triggering billing sync: {e}")
@@ -200,13 +194,12 @@ def handle_pool_hub_collect_metrics(args):
return
pool_hub_url = getattr(config, "pool_hub_url", "http://localhost:8012")
response = requests.post(f"{pool_hub_url}/sla/metrics/collect", timeout=60)
http_client = AITBCHTTPClient(base_url=pool_hub_url, timeout=60)
result = http_client.post("/sla/metrics/collect")
if response.status_code == 200:
result = response.json()
print("📊 SLA metrics collection triggered")
print(f"{result.get('message', 'Success')}")
else:
print(f"❌ Metrics collection failed: {response.text}")
print("📊 SLA metrics collection triggered")
print(f"{result.get('message', 'Success')}")
except NetworkError as e:
print(f"❌ Metrics collection failed: {e}")
except Exception as e:
print(f"❌ Error triggering metrics collection: {e}")

View File

@@ -13,6 +13,7 @@ import os
from pathlib import Path
from typing import Optional, Dict, Any
from aitbc.paths import get_keystore_path
from cryptography.fernet import Fernet
@@ -72,14 +73,14 @@ def get_private_key(address: str, password: Optional[str] = None,
with open(password_file) as f:
pass_password = f.read().strip()
if not pass_password:
pw_file = Path("/var/lib/aitbc/keystore/.password")
pw_file = get_keystore_path(".password")
if pw_file.exists():
pass_password = pw_file.read_text().strip()
if not pass_password:
raise ValueError(
"No password provided. Set KEYSTORE_PASSWORD, pass --password, "
"or create /var/lib/aitbc/keystore/.password"
"or create a .password file in the keystore directory"
)
# Load and decrypt keystore

0
cli/unified_cli.py Normal file → Executable file
View File

View File

@@ -14,7 +14,10 @@ from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeElapsedColumn
import json
import yaml
from tabulate import tabulate
try:
from tabulate import tabulate
except ImportError:
tabulate = None
console = Console()

View File

@@ -8,9 +8,11 @@ import json
import base64
from typing import Dict, Any, Optional, List
from pathlib import Path
import httpx
from dataclasses import dataclass
from aitbc.http_client import AITBCHTTPClient
from aitbc.exceptions import NetworkError
from utils import error, success
from config import Config
@@ -65,10 +67,10 @@ class WalletDaemonClient:
self.config = config
self.base_url = config.wallet_url.rstrip('/')
self.timeout = getattr(config, 'timeout', 30)
def _get_http_client(self) -> httpx.Client:
def _get_http_client(self) -> AITBCHTTPClient:
"""Create HTTP client with appropriate settings"""
return httpx.Client(
return AITBCHTTPClient(
base_url=self.base_url,
timeout=self.timeout,
headers={"Content-Type": "application/json"}
@@ -77,47 +79,46 @@ class WalletDaemonClient:
def is_available(self) -> bool:
"""Check if wallet daemon is available and responsive"""
try:
with self._get_http_client() as client:
response = client.get("/health")
return response.status_code == 200
client = self._get_http_client()
client.get("/health")
return True
except NetworkError:
return False
except Exception:
return False
def get_status(self) -> Dict[str, Any]:
"""Get wallet daemon status information"""
try:
with self._get_http_client() as client:
response = client.get("/health")
if response.status_code == 200:
return response.json()
else:
return {"status": "unavailable", "error": f"HTTP {response.status_code}"}
client = self._get_http_client()
return client.get("/health")
except NetworkError as e:
return {"status": "unavailable", "error": str(e)}
except Exception as e:
return {"status": "error", "error": str(e)}
def create_wallet(self, wallet_id: str, password: str, metadata: Optional[Dict[str, Any]] = None) -> WalletInfo:
"""Create a new wallet in the daemon"""
try:
with self._get_http_client() as client:
payload = {
"wallet_id": wallet_id,
"password": password,
"metadata": metadata or {}
}
response = client.post("/v1/wallets", json=payload)
if response.status_code == 201:
data = response.json()
return WalletInfo(
wallet_id=data["wallet_id"],
public_key=data["public_key"],
address=data.get("address"),
created_at=data.get("created_at"),
metadata=data.get("metadata")
)
else:
error(f"Failed to create wallet: {response.text}")
raise Exception(f"HTTP {response.status_code}: {response.text}")
client = self._get_http_client()
payload = {
"wallet_id": wallet_id,
"password": password,
"metadata": metadata or {}
}
data = client.post("/v1/wallets", json=payload)
return WalletInfo(
wallet_id=data["wallet_id"],
chain_id=data.get("chain_id", "default"),
public_key=data["public_key"],
address=data.get("address"),
created_at=data.get("created_at"),
metadata=data.get("metadata")
)
except NetworkError as e:
error(f"Error creating wallet: {e}")
raise
except Exception as e:
error(f"Error creating wallet: {str(e)}")
raise
@@ -125,23 +126,24 @@ class WalletDaemonClient:
def list_wallets(self) -> List[WalletInfo]:
"""List all wallets in the daemon"""
try:
with self._get_http_client() as client:
response = client.get("/v1/wallets")
if response.status_code == 200:
data = response.json()
wallets = []
for wallet_data in data.get("wallets", []):
wallets.append(WalletInfo(
wallet_id=wallet_data["wallet_id"],
public_key=wallet_data["public_key"],
address=wallet_data.get("address"),
created_at=wallet_data.get("created_at"),
metadata=wallet_data.get("metadata")
))
return wallets
else:
error(f"Failed to list wallets: {response.text}")
raise Exception(f"HTTP {response.status_code}: {response.text}")
client = self._get_http_client()
data = client.get("/v1/wallets")
wallets = []
# Handle both "wallets" and "items" keys for compatibility
wallet_list = data.get("wallets", data.get("items", []))
for wallet_data in wallet_list:
wallets.append(WalletInfo(
wallet_id=wallet_data.get("wallet_id", wallet_data.get("wallet_name", "")),
chain_id=wallet_data.get("chain_id", "default"),
public_key=wallet_data.get("public_key", ""),
address=wallet_data.get("address", ""),
created_at=wallet_data.get("created_at", ""),
metadata=wallet_data.get("metadata", {})
))
return wallets
except NetworkError as e:
error(f"Failed to list daemon wallets: {str(e)}")
raise
except Exception as e:
error(f"Error listing wallets: {str(e)}")
raise
@@ -149,47 +151,41 @@ class WalletDaemonClient:
def get_wallet_info(self, wallet_id: str) -> Optional[WalletInfo]:
"""Get information about a specific wallet"""
try:
with self._get_http_client() as client:
response = client.get(f"/v1/wallets/{wallet_id}")
if response.status_code == 200:
data = response.json()
return WalletInfo(
wallet_id=data["wallet_id"],
public_key=data["public_key"],
address=data.get("address"),
created_at=data.get("created_at"),
metadata=data.get("metadata")
)
elif response.status_code == 404:
return None
else:
error(f"Failed to get wallet info: {response.text}")
raise Exception(f"HTTP {response.status_code}: {response.text}")
client = self._get_http_client()
data = client.get(f"/v1/wallets/{wallet_id}")
return WalletInfo(
wallet_id=data["wallet_id"],
chain_id=data.get("chain_id", "default"),
public_key=data["public_key"],
address=data.get("address"),
created_at=data.get("created_at"),
metadata=data.get("metadata")
)
except NetworkError as e:
error(f"Failed to get wallet info: {e}")
return None
except Exception as e:
error(f"Error getting wallet info: {str(e)}")
raise
return None
def get_wallet_balance(self, wallet_id: str) -> Optional[WalletBalance]:
"""Get wallet balance from daemon"""
try:
with self._get_http_client() as client:
response = client.get(f"/v1/wallets/{wallet_id}/balance")
if response.status_code == 200:
data = response.json()
return WalletBalance(
wallet_id=wallet_id,
balance=data["balance"],
address=data.get("address"),
last_updated=data.get("last_updated")
)
elif response.status_code == 404:
return None
else:
error(f"Failed to get wallet balance: {response.text}")
raise Exception(f"HTTP {response.status_code}: {response.text}")
client = self._get_http_client()
data = client.get(f"/v1/wallets/{wallet_id}/balance")
return WalletBalance(
wallet_id=wallet_id,
chain_id=data.get("chain_id", "default"),
balance=data["balance"],
address=data.get("address"),
last_updated=data.get("last_updated")
)
except NetworkError as e:
error(f"Failed to get wallet balance: {e}")
return None
except Exception as e:
error(f"Error getting wallet balance: {str(e)}")
raise
return None
def sign_message(self, wallet_id: str, password: str, message: bytes) -> str:
"""Sign a message with wallet private key"""
@@ -349,6 +345,31 @@ class WalletDaemonClient:
error(f"Error creating chain: {str(e)}")
raise
def create_wallet(self, wallet_id: str, password: str, metadata: Optional[Dict[str, Any]] = None) -> WalletInfo:
"""Create a new wallet in the daemon"""
try:
client = self._get_http_client()
payload = {
"wallet_id": wallet_id,
"password": password,
"metadata": metadata or {}
}
data = client.post("/v1/wallets", json=payload)
return WalletInfo(
wallet_id=data["wallet_id"],
public_key=data["public_key"],
address=data.get("address"),
created_at=data.get("created_at"),
metadata=data.get("metadata")
)
except NetworkError as e:
error(f"Failed to create wallet: {e}")
raise
except Exception as e:
error(f"Error creating wallet: {str(e)}")
raise
def create_wallet_in_chain(self, chain_id: str, wallet_id: str, password: str,
metadata: Optional[Dict[str, Any]] = None) -> WalletInfo:
"""Create a wallet in a specific chain"""