Some checks failed
AITBC CI/CD Pipeline / lint-and-test (3.13.5) (push) Has been cancelled
AITBC CI/CD Pipeline / test-cli (push) Has been cancelled
AITBC CI/CD Pipeline / test-services (push) Has been cancelled
AITBC CI/CD Pipeline / test-production-services (push) Has been cancelled
AITBC CI/CD Pipeline / security-scan (push) Has been cancelled
AITBC CI/CD Pipeline / build (push) Has been cancelled
AITBC CI/CD Pipeline / deploy-staging (push) Has been cancelled
AITBC CI/CD Pipeline / deploy-production (push) Has been cancelled
AITBC CI/CD Pipeline / performance-test (push) Has been cancelled
AITBC CI/CD Pipeline / docs (push) Has been cancelled
AITBC CI/CD Pipeline / release (push) Has been cancelled
AITBC CI/CD Pipeline / notify (push) Has been cancelled
GPU Benchmark CI / gpu-benchmark (3.13.5) (push) Has been cancelled
Security Scanning / Bandit Security Scan (apps/coordinator-api/src) (push) Has been cancelled
Security Scanning / Bandit Security Scan (cli/aitbc_cli) (push) Has been cancelled
Security Scanning / Bandit Security Scan (packages/py/aitbc-core/src) (push) Has been cancelled
Security Scanning / Bandit Security Scan (packages/py/aitbc-crypto/src) (push) Has been cancelled
Security Scanning / Bandit Security Scan (packages/py/aitbc-sdk/src) (push) Has been cancelled
Security Scanning / Bandit Security Scan (tests) (push) Has been cancelled
Security Scanning / CodeQL Security Analysis (javascript) (push) Has been cancelled
Security Scanning / CodeQL Security Analysis (python) (push) Has been cancelled
Security Scanning / Dependency Security Scan (push) Has been cancelled
Security Scanning / Container Security Scan (push) Has been cancelled
Security Scanning / OSSF Scorecard (push) Has been cancelled
Security Scanning / Security Summary Report (push) Has been cancelled
AITBC CLI Level 1 Commands Test / test-cli-level1 (3.13.5) (push) Has been cancelled
AITBC CLI Level 1 Commands Test / test-summary (push) Has been cancelled
DIRECTORY REORGANIZATION: - Organized 13 scattered root files into 4 logical subdirectories - Eliminated clutter in CLI root directory - Improved maintainability and navigation FILE MOVES: core/ (Core CLI functionality): ├── __init__.py # Package metadata ├── main.py # Main CLI entry point ├── imports.py # Import utilities └── plugins.py # Plugin system utils/ (Utilities & Services): ├── dual_mode_wallet_adapter.py ├── wallet_daemon_client.py ├── wallet_migration_service.py ├── kyc_aml_providers.py └── [other utility files] docs/ (Documentation): ├── README.md ├── DISABLED_COMMANDS_CLEANUP.md └── FILE_ORGANIZATION_SUMMARY.md variants/ (CLI Variants): └── main_minimal.py # Minimal CLI version REWIRED IMPORTS: ✅ Updated main.py: 'from .plugins import plugin, load_plugins' ✅ Updated 6 commands: 'from core.imports import ensure_coordinator_api_imports' ✅ Updated wallet.py: 'from utils.dual_mode_wallet_adapter import DualModeWalletAdapter' ✅ Updated compliance.py: 'from utils.kyc_aml_providers import ...' ✅ Fixed internal utils imports: 'from utils import error, success' ✅ Updated test files: 'from core.main_minimal import cli' ✅ Updated setup.py: entry point 'aitbc=core.main:main' ✅ Updated setup.py: README path 'docs/README.md' ✅ Created root __init__.py: redirects to core.main BENEFITS: ✅ Logical file grouping by functionality ✅ Clean root directory with only essential files ✅ Easier navigation and maintenance ✅ Clear separation of concerns ✅ Better code organization ✅ Zero breaking changes - all functionality preserved VERIFICATION: ✅ CLI works: 'aitbc --help' functional ✅ All imports resolve correctly ✅ Installation successful: 'pip install -e .' ✅ Entry points properly updated ✅ Tests import correctly STATUS: Complete - Successfully organized and rewired
537 lines
22 KiB
Python
Executable File
537 lines
22 KiB
Python
Executable File
"""Wallet Daemon Client for AITBC CLI
|
|
|
|
This module provides a client for communicating with the AITBC wallet daemon,
|
|
supporting both REST and JSON-RPC APIs for wallet operations.
|
|
"""
|
|
|
|
import json
|
|
import base64
|
|
from typing import Dict, Any, Optional, List
|
|
from pathlib import Path
|
|
import httpx
|
|
from dataclasses import dataclass
|
|
|
|
from utils import error, success
|
|
from config import Config
|
|
|
|
|
|
@dataclass
|
|
class ChainInfo:
|
|
"""Chain information from daemon"""
|
|
chain_id: str
|
|
name: str
|
|
status: str
|
|
coordinator_url: str
|
|
created_at: str
|
|
updated_at: str
|
|
wallet_count: int
|
|
recent_activity: int
|
|
|
|
|
|
@dataclass
|
|
class WalletInfo:
|
|
"""Wallet information from daemon"""
|
|
wallet_id: str
|
|
chain_id: str
|
|
public_key: str
|
|
address: Optional[str] = None
|
|
created_at: Optional[str] = None
|
|
metadata: Optional[Dict[str, Any]] = None
|
|
|
|
|
|
@dataclass
|
|
class WalletBalance:
|
|
"""Wallet balance information"""
|
|
wallet_id: str
|
|
chain_id: str
|
|
balance: float
|
|
address: Optional[str] = None
|
|
last_updated: Optional[str] = None
|
|
|
|
|
|
@dataclass
|
|
class WalletMigrationResult:
|
|
"""Result of wallet migration between chains"""
|
|
success: bool
|
|
source_wallet: WalletInfo
|
|
target_wallet: WalletInfo
|
|
migration_timestamp: str
|
|
|
|
|
|
class WalletDaemonClient:
|
|
"""Client for interacting with AITBC wallet daemon"""
|
|
|
|
def __init__(self, config: Config):
|
|
self.config = config
|
|
self.base_url = config.wallet_url.rstrip('/')
|
|
self.timeout = getattr(config, 'timeout', 30)
|
|
|
|
def _get_http_client(self) -> httpx.Client:
|
|
"""Create HTTP client with appropriate settings"""
|
|
return httpx.Client(
|
|
base_url=self.base_url,
|
|
timeout=self.timeout,
|
|
headers={"Content-Type": "application/json"}
|
|
)
|
|
|
|
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
|
|
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}"}
|
|
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}")
|
|
except Exception as e:
|
|
error(f"Error creating wallet: {str(e)}")
|
|
raise
|
|
|
|
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}")
|
|
except Exception as e:
|
|
error(f"Error listing wallets: {str(e)}")
|
|
raise
|
|
|
|
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}")
|
|
except Exception as e:
|
|
error(f"Error getting wallet info: {str(e)}")
|
|
raise
|
|
|
|
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}")
|
|
except Exception as e:
|
|
error(f"Error getting wallet balance: {str(e)}")
|
|
raise
|
|
|
|
def sign_message(self, wallet_id: str, password: str, message: bytes) -> str:
|
|
"""Sign a message with wallet private key"""
|
|
try:
|
|
with self._get_http_client() as client:
|
|
# Encode message as base64 for transmission
|
|
message_b64 = base64.b64encode(message).decode()
|
|
|
|
payload = {
|
|
"password": password,
|
|
"message": message_b64
|
|
}
|
|
|
|
response = client.post(f"/v1/wallets/{wallet_id}/sign", json=payload)
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
return data["signature_base64"]
|
|
else:
|
|
error(f"Failed to sign message: {response.text}")
|
|
raise Exception(f"HTTP {response.status_code}: {response.text}")
|
|
except Exception as e:
|
|
error(f"Error signing message: {str(e)}")
|
|
raise
|
|
|
|
def send_transaction(self, wallet_id: str, password: str, to_address: str, amount: float,
|
|
description: Optional[str] = None) -> Dict[str, Any]:
|
|
"""Send a transaction via the daemon"""
|
|
try:
|
|
with self._get_http_client() as client:
|
|
payload = {
|
|
"password": password,
|
|
"to_address": to_address,
|
|
"amount": amount,
|
|
"description": description or ""
|
|
}
|
|
|
|
response = client.post(f"/v1/wallets/{wallet_id}/send", json=payload)
|
|
if response.status_code == 201:
|
|
return response.json()
|
|
else:
|
|
error(f"Failed to send transaction: {response.text}")
|
|
raise Exception(f"HTTP {response.status_code}: {response.text}")
|
|
except Exception as e:
|
|
error(f"Error sending transaction: {str(e)}")
|
|
raise
|
|
|
|
def unlock_wallet(self, wallet_id: str, password: str) -> bool:
|
|
"""Unlock a wallet for operations"""
|
|
try:
|
|
with self._get_http_client() as client:
|
|
payload = {"password": password}
|
|
response = client.post(f"/v1/wallets/{wallet_id}/unlock", json=payload)
|
|
return response.status_code == 200
|
|
except Exception:
|
|
return False
|
|
|
|
def lock_wallet(self, wallet_id: str) -> bool:
|
|
"""Lock a wallet"""
|
|
try:
|
|
with self._get_http_client() as client:
|
|
response = client.post(f"/v1/wallets/{wallet_id}/lock")
|
|
return response.status_code == 200
|
|
except Exception:
|
|
return False
|
|
|
|
def delete_wallet(self, wallet_id: str, password: str) -> bool:
|
|
"""Delete a wallet from daemon"""
|
|
try:
|
|
with self._get_http_client() as client:
|
|
payload = {"password": password}
|
|
response = client.delete(f"/v1/wallets/{wallet_id}", json=payload)
|
|
return response.status_code == 200
|
|
except Exception:
|
|
return False
|
|
|
|
def jsonrpc_call(self, method: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
"""Make a JSON-RPC call to the daemon"""
|
|
try:
|
|
with self._get_http_client() as client:
|
|
payload = {
|
|
"jsonrpc": "2.0",
|
|
"method": method,
|
|
"params": params or {},
|
|
"id": 1
|
|
}
|
|
|
|
response = client.post("/rpc", json=payload)
|
|
if response.status_code == 200:
|
|
return response.json()
|
|
else:
|
|
error(f"JSON-RPC call failed: {response.text}")
|
|
raise Exception(f"HTTP {response.status_code}: {response.text}")
|
|
except Exception as e:
|
|
error(f"Error making JSON-RPC call: {str(e)}")
|
|
raise
|
|
|
|
# Multi-Chain Methods
|
|
|
|
def list_chains(self) -> List[ChainInfo]:
|
|
"""List all blockchain chains"""
|
|
try:
|
|
with self._get_http_client() as client:
|
|
response = client.get("/v1/chains")
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
chains = []
|
|
for chain_data in data.get("chains", []):
|
|
chains.append(ChainInfo(
|
|
chain_id=chain_data["chain_id"],
|
|
name=chain_data["name"],
|
|
status=chain_data["status"],
|
|
coordinator_url=chain_data["coordinator_url"],
|
|
created_at=chain_data["created_at"],
|
|
updated_at=chain_data["updated_at"],
|
|
wallet_count=chain_data["wallet_count"],
|
|
recent_activity=chain_data["recent_activity"]
|
|
))
|
|
return chains
|
|
else:
|
|
error(f"Failed to list chains: {response.text}")
|
|
raise Exception(f"HTTP {response.status_code}: {response.text}")
|
|
except Exception as e:
|
|
error(f"Error listing chains: {str(e)}")
|
|
raise
|
|
|
|
def create_chain(self, chain_id: str, name: str, coordinator_url: str,
|
|
coordinator_api_key: str, metadata: Optional[Dict[str, Any]] = None) -> ChainInfo:
|
|
"""Create a new blockchain chain"""
|
|
try:
|
|
with self._get_http_client() as client:
|
|
payload = {
|
|
"chain_id": chain_id,
|
|
"name": name,
|
|
"coordinator_url": coordinator_url,
|
|
"coordinator_api_key": coordinator_api_key,
|
|
"metadata": metadata or {}
|
|
}
|
|
|
|
response = client.post("/v1/chains", json=payload)
|
|
if response.status_code == 201:
|
|
data = response.json()
|
|
chain_data = data["chain"]
|
|
return ChainInfo(
|
|
chain_id=chain_data["chain_id"],
|
|
name=chain_data["name"],
|
|
status=chain_data["status"],
|
|
coordinator_url=chain_data["coordinator_url"],
|
|
created_at=chain_data["created_at"],
|
|
updated_at=chain_data["updated_at"],
|
|
wallet_count=chain_data["wallet_count"],
|
|
recent_activity=chain_data["recent_activity"]
|
|
)
|
|
else:
|
|
error(f"Failed to create chain: {response.text}")
|
|
raise Exception(f"HTTP {response.status_code}: {response.text}")
|
|
except Exception as e:
|
|
error(f"Error creating chain: {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"""
|
|
try:
|
|
with self._get_http_client() as client:
|
|
payload = {
|
|
"chain_id": chain_id,
|
|
"wallet_id": wallet_id,
|
|
"password": password,
|
|
"metadata": metadata or {}
|
|
}
|
|
|
|
response = client.post(f"/v1/chains/{chain_id}/wallets", json=payload)
|
|
if response.status_code == 201:
|
|
data = response.json()
|
|
wallet_data = data["wallet"]
|
|
return WalletInfo(
|
|
wallet_id=wallet_data["wallet_id"],
|
|
chain_id=wallet_data["chain_id"],
|
|
public_key=wallet_data["public_key"],
|
|
address=wallet_data.get("address"),
|
|
created_at=wallet_data.get("created_at"),
|
|
metadata=wallet_data.get("metadata")
|
|
)
|
|
else:
|
|
error(f"Failed to create wallet in chain {chain_id}: {response.text}")
|
|
raise Exception(f"HTTP {response.status_code}: {response.text}")
|
|
except Exception as e:
|
|
error(f"Error creating wallet in chain {chain_id}: {str(e)}")
|
|
raise
|
|
|
|
def list_wallets_in_chain(self, chain_id: str) -> List[WalletInfo]:
|
|
"""List wallets in a specific chain"""
|
|
try:
|
|
with self._get_http_client() as client:
|
|
response = client.get(f"/v1/chains/{chain_id}/wallets")
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
wallets = []
|
|
for wallet_data in data.get("items", []):
|
|
wallets.append(WalletInfo(
|
|
wallet_id=wallet_data["wallet_id"],
|
|
chain_id=wallet_data["chain_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 in chain {chain_id}: {response.text}")
|
|
raise Exception(f"HTTP {response.status_code}: {response.text}")
|
|
except Exception as e:
|
|
error(f"Error listing wallets in chain {chain_id}: {str(e)}")
|
|
raise
|
|
|
|
def get_wallet_info_in_chain(self, chain_id: str, wallet_id: str) -> Optional[WalletInfo]:
|
|
"""Get wallet information from a specific chain"""
|
|
try:
|
|
wallets = self.list_wallets_in_chain(chain_id)
|
|
for wallet in wallets:
|
|
if wallet.wallet_id == wallet_id:
|
|
return wallet
|
|
return None
|
|
except Exception as e:
|
|
error(f"Error getting wallet info from chain {chain_id}: {str(e)}")
|
|
return None
|
|
|
|
def unlock_wallet_in_chain(self, chain_id: str, wallet_id: str, password: str) -> bool:
|
|
"""Unlock a wallet in a specific chain"""
|
|
try:
|
|
with self._get_http_client() as client:
|
|
payload = {"password": password}
|
|
response = client.post(f"/v1/chains/{chain_id}/wallets/{wallet_id}/unlock", json=payload)
|
|
return response.status_code == 200
|
|
except Exception:
|
|
return False
|
|
|
|
def sign_message_in_chain(self, chain_id: str, wallet_id: str, password: str, message: bytes) -> Optional[str]:
|
|
"""Sign a message with a wallet in a specific chain"""
|
|
try:
|
|
with self._get_http_client() as client:
|
|
payload = {
|
|
"password": password,
|
|
"message_base64": base64.b64encode(message).decode()
|
|
}
|
|
|
|
response = client.post(f"/v1/chains/{chain_id}/wallets/{wallet_id}/sign", json=payload)
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
return data.get("signature_base64")
|
|
else:
|
|
return None
|
|
except Exception:
|
|
return None
|
|
|
|
def get_wallet_balance_in_chain(self, chain_id: str, wallet_id: str) -> Optional[WalletBalance]:
|
|
"""Get wallet balance in a specific chain"""
|
|
try:
|
|
# For now, return a placeholder balance
|
|
# In a real implementation, this would call the chain-specific balance endpoint
|
|
wallet_info = self.get_wallet_info_in_chain(chain_id, wallet_id)
|
|
if wallet_info:
|
|
return WalletBalance(
|
|
wallet_id=wallet_id,
|
|
chain_id=chain_id,
|
|
balance=0.0, # Placeholder
|
|
address=wallet_info.address
|
|
)
|
|
return None
|
|
except Exception as e:
|
|
error(f"Error getting wallet balance in chain {chain_id}: {str(e)}")
|
|
return None
|
|
|
|
def migrate_wallet(self, source_chain_id: str, target_chain_id: str, wallet_id: str,
|
|
password: str, new_password: Optional[str] = None) -> Optional[WalletMigrationResult]:
|
|
"""Migrate a wallet from one chain to another"""
|
|
try:
|
|
with self._get_http_client() as client:
|
|
payload = {
|
|
"source_chain_id": source_chain_id,
|
|
"target_chain_id": target_chain_id,
|
|
"wallet_id": wallet_id,
|
|
"password": password
|
|
}
|
|
if new_password:
|
|
payload["new_password"] = new_password
|
|
|
|
response = client.post("/v1/wallets/migrate", json=payload)
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
|
|
source_wallet = WalletInfo(
|
|
wallet_id=data["source_wallet"]["wallet_id"],
|
|
chain_id=data["source_wallet"]["chain_id"],
|
|
public_key=data["source_wallet"]["public_key"],
|
|
address=data["source_wallet"].get("address"),
|
|
metadata=data["source_wallet"].get("metadata")
|
|
)
|
|
|
|
target_wallet = WalletInfo(
|
|
wallet_id=data["target_wallet"]["wallet_id"],
|
|
chain_id=data["target_wallet"]["chain_id"],
|
|
public_key=data["target_wallet"]["public_key"],
|
|
address=data["target_wallet"].get("address"),
|
|
metadata=data["target_wallet"].get("metadata")
|
|
)
|
|
|
|
return WalletMigrationResult(
|
|
success=data["success"],
|
|
source_wallet=source_wallet,
|
|
target_wallet=target_wallet,
|
|
migration_timestamp=data["migration_timestamp"]
|
|
)
|
|
else:
|
|
error(f"Failed to migrate wallet: {response.text}")
|
|
return None
|
|
except Exception as e:
|
|
error(f"Error migrating wallet: {str(e)}")
|
|
return None
|
|
|
|
def get_chain_status(self) -> Dict[str, Any]:
|
|
"""Get overall chain status and statistics"""
|
|
try:
|
|
chains = self.list_chains()
|
|
active_chains = [c for c in chains if c.status == "active"]
|
|
|
|
return {
|
|
"total_chains": len(chains),
|
|
"active_chains": len(active_chains),
|
|
"total_wallets": sum(c.wallet_count for c in chains),
|
|
"chains": [
|
|
{
|
|
"chain_id": chain.chain_id,
|
|
"name": chain.name,
|
|
"status": chain.status,
|
|
"wallet_count": chain.wallet_count,
|
|
"recent_activity": chain.recent_activity
|
|
}
|
|
for chain in chains
|
|
]
|
|
}
|
|
except Exception as e:
|
|
error(f"Error getting chain status: {str(e)}")
|
|
return {"error": str(e)}
|