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)