refactor: consolidate blockchain explorer into single app and update backup ignore patterns

- Remove standalone explorer-web app (README, HTML, package files)
- Add /web endpoint to blockchain-explorer for web interface access
- Update .gitignore to exclude application backup archives (*.tar.gz, *.zip)
- Add backup documentation files to .gitignore (BACKUP_INDEX.md, README.md)
- Consolidate explorer functionality into main blockchain-explorer application
This commit is contained in:
oib
2026-03-06 18:14:49 +01:00
parent dc1561d457
commit bb5363bebc
295 changed files with 35501 additions and 3734 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -50,8 +50,9 @@ def submit(ctx, job_type: str, prompt: Optional[str], model: Optional[str],
for attempt in range(1, max_attempts + 1):
try:
with httpx.Client() as client:
# Use Exchange API endpoint format
response = client.post(
f"{config.coordinator_url}/v1/jobs",
f"{config.coordinator_url}/v1/miners/default/jobs/submit",
headers={
"Content-Type": "application/json",
"X-Api-Key": config.api_key or ""
@@ -62,7 +63,7 @@ def submit(ctx, job_type: str, prompt: Optional[str], model: Optional[str],
}
)
if response.status_code == 201:
if response.status_code in [200, 201]:
job = response.json()
result = {
"job_id": job.get('job_id'),
@@ -118,24 +119,33 @@ def status(ctx, job_id: str):
@client.command()
@click.option("--limit", default=10, help="Number of blocks to show")
@click.option('--chain-id', help='Specific chain ID to query (default: ait-devnet)')
@click.pass_context
def blocks(ctx, limit: int):
"""List recent blocks"""
def blocks(ctx, limit: int, chain_id: str):
"""List recent blocks from specific chain"""
config = ctx.obj['config']
# Query specific chain (default to ait-devnet if not specified)
target_chain = chain_id or 'ait-devnet'
try:
with httpx.Client() as client:
response = client.get(
f"{config.coordinator_url}/api/v1/blocks",
params={"limit": limit},
params={"limit": limit, "chain_id": target_chain},
headers={"X-Api-Key": config.api_key or ""}
)
if response.status_code == 200:
blocks = response.json()
output(blocks, ctx.obj['output_format'])
output({
"blocks": blocks,
"chain_id": target_chain,
"limit": limit,
"query_type": "single_chain"
}, ctx.obj['output_format'])
else:
error(f"Failed to get blocks: {response.status_code}")
error(f"Failed to get blocks from chain {target_chain}: {response.status_code}")
ctx.exit(1)
except Exception as e:
error(f"Network error: {e}")

View File

@@ -0,0 +1,476 @@
"""Cross-chain trading commands for AITBC CLI"""
import click
import httpx
import json
from typing import Optional
from tabulate import tabulate
from ..config import get_config
from ..utils import success, error, output
@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 httpx.Client() as client:
# Get rates from cross-chain exchange
response = client.get(
f"http://localhost:8001/api/v1/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"]
print(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 httpx.Client() as client:
response = client.get(
f"http://localhost:8001/api/v1/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:
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:
with httpx.Client() as client:
response = client.post(
f"http://localhost:8001/api/v1/cross-chain/swap",
json=swap_data,
timeout=30
)
if response.status_code == 200:
swap_result = response.json()
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')}")
else:
error(f"Failed to create swap: {response.status_code}")
if response.text:
error(f"Details: {response.text}")
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:
with httpx.Client() as client:
response = client.get(
f"http://localhost:8001/api/v1/cross-chain/swap/{swap_id}",
timeout=10
)
if response.status_code == 200:
swap_data = response.json()
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")
else:
error(f"Failed to get swap status: {response.status_code}")
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:
with httpx.Client() as client:
response = client.get(
f"http://localhost:8001/api/v1/cross-chain/swaps",
params=params,
timeout=10
)
if response.status_code == 200:
swaps_data = response.json()
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")
else:
error(f"Failed to get swaps: {response.status_code}")
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:
with httpx.Client() as client:
response = client.post(
f"http://localhost:8001/api/v1/cross-chain/bridge",
json=bridge_data,
timeout=30
)
if response.status_code == 200:
bridge_result = response.json()
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')}")
else:
error(f"Failed to create bridge: {response.status_code}")
if response.text:
error(f"Details: {response.text}")
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:
with httpx.Client() as client:
response = client.get(
f"http://localhost:8001/api/v1/cross-chain/bridge/{bridge_id}",
timeout=10
)
if response.status_code == 200:
bridge_data = response.json()
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...")
else:
error(f"Failed to get bridge status: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")
@cross_chain.command()
@click.pass_context
def pools(ctx):
"""Show cross-chain liquidity pools"""
try:
with httpx.Client() as client:
response = client.get(
f"http://localhost:8001/api/v1/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:
with httpx.Client() as client:
response = client.get(
f"http://localhost:8001/api/v1/cross-chain/stats",
timeout=10
)
if response.status_code == 200:
stats_data = response.json()
success("Cross-Chain Trading Statistics:")
# Show swap stats
swap_stats = stats_data.get('swap_stats', [])
if swap_stats:
success("Swap Statistics:")
swap_table = []
for stat in swap_stats:
swap_table.append([
stat.get('status', ''),
stat.get('count', 0),
f"{stat.get('volume', 0):.2f}"
])
table(["Status", "Count", "Volume"], swap_table)
# Show bridge stats
bridge_stats = stats_data.get('bridge_stats', [])
if bridge_stats:
success("Bridge Statistics:")
bridge_table = []
for stat in bridge_stats:
bridge_table.append([
stat.get('status', ''),
stat.get('count', 0),
f"{stat.get('volume', 0):.2f}"
])
table(["Status", "Count", "Volume"], bridge_table)
# Show overall stats
success("Overall Statistics:")
output({
"Total Volume": f"{stats_data.get('total_volume', 0):.2f}",
"Supported Chains": ", ".join(stats_data.get('supported_chains', [])),
"Last Updated": stats_data.get('timestamp', '')
}, ctx.obj['output_format'])
else:
error(f"Failed to get stats: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")

View File

@@ -84,12 +84,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, 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)
# Store daemon mode preference
ctx.obj["use_daemon"] = use_daemon
# Initialize dual-mode adapter
from ..config import get_config
from ..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)
@@ -140,118 +154,117 @@ def wallet(ctx, wallet_name: Optional[str], wallet_path: Optional[str]):
@click.pass_context
def create(ctx, name: str, wallet_type: str, no_encrypt: bool):
"""Create a new wallet"""
wallet_dir = ctx.obj["wallet_dir"]
wallet_path = wallet_dir / f"{name}.json"
if wallet_path.exists():
error(f"Wallet '{name}' already exists")
return
# Generate new wallet
if wallet_type == "hd":
# Hierarchical Deterministic wallet
import secrets
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.serialization import (
Encoding,
PublicFormat,
NoEncryption,
PrivateFormat,
)
import base64
# Generate private key
private_key_bytes = secrets.token_bytes(32)
private_key = f"0x{private_key_bytes.hex()}"
# Derive public key from private key using ECDSA
priv_key = ec.derive_private_key(
int.from_bytes(private_key_bytes, "big"), ec.SECP256K1()
)
pub_key = priv_key.public_key()
pub_key_bytes = pub_key.public_bytes(
encoding=Encoding.X962, format=PublicFormat.UncompressedPoint
)
public_key = f"0x{pub_key_bytes.hex()}"
# Generate address from public key (simplified)
digest = hashes.Hash(hashes.SHA256())
digest.update(pub_key_bytes)
address_hash = digest.finalize()
address = f"aitbc1{address_hash[:20].hex()}"
else:
# Simple wallet
import secrets
private_key = f"0x{secrets.token_hex(32)}"
public_key = f"0x{secrets.token_hex(32)}"
address = f"aitbc1{secrets.token_hex(20)}"
wallet_data = {
"wallet_id": name,
"type": wallet_type,
"address": address,
"public_key": public_key,
"private_key": private_key,
"created_at": datetime.utcnow().isoformat() + "Z",
"balance": 0,
"transactions": [],
}
# Get password for encryption unless skipped
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.")
# Switch to file mode
from ..config import get_config
from ..dual_mode_wallet_adapter import DualModeWalletAdapter
config = get_config()
adapter = DualModeWalletAdapter(config, use_daemon=False)
ctx.obj["wallet_adapter"] = adapter
# Get password for encryption
password = None
if not no_encrypt:
success(
"Wallet encryption is enabled. Your private key will be encrypted at rest."
)
password = _get_wallet_password(name)
# Save wallet
_save_wallet(wallet_path, wallet_data, password)
success(f"Wallet '{name}' created successfully")
output(
{
"name": name,
"type": wallet_type,
"address": address,
"path": str(wallet_path),
},
ctx.obj.get("output_format", "table"),
)
if use_daemon:
# For daemon mode, use a default password or prompt
password = getpass.getpass(f"Enter password for wallet '{name}' (press Enter for default): ")
if not password:
password = "default_wallet_password"
else:
# For file mode, use existing password prompt logic
password = getpass.getpass(f"Enter password for wallet '{name}': ")
confirm = getpass.getpass("Confirm password: ")
if password != confirm:
error("Passwords do not match")
return
# Create wallet using the adapter
try:
metadata = {
"wallet_type": wallet_type,
"created_by": "aitbc_cli",
"encryption_enabled": not no_encrypt
}
wallet_info = adapter.create_wallet(name, password, wallet_type, metadata)
# Display results
output(wallet_info, ctx.obj.get("output_format", "table"))
# Set as active wallet if successful
if wallet_info:
config_file = Path.home() / ".aitbc" / "config.yaml"
config_data = {}
if config_file.exists():
with open(config_file, "r") as f:
config_data = yaml.safe_load(f) or {}
config_data["active_wallet"] = name
config_file.parent.mkdir(parents=True, exist_ok=True)
with open(config_file, "w") as f:
yaml.dump(config_data, f)
success(f"Wallet '{name}' is now active")
except Exception as e:
error(f"Failed to create wallet: {str(e)}")
return
@wallet.command()
@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)
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
from ..dual_mode_wallet_adapter import DualModeWalletAdapter
config = get_config()
adapter = DualModeWalletAdapter(config, use_daemon=False)
try:
wallets = adapter.list_wallets()
if not wallets:
output({"wallets": [], "count": 0, "mode": "daemon" if use_daemon else "file"},
ctx.obj.get("output_format", "table"))
return
# Format output
wallet_list = []
for wallet in wallets:
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,
"name": wallet.get("wallet_name"),
"address": wallet.get("address"),
"balance": wallet.get("balance", 0.0),
"type": wallet.get("wallet_type", "hd"),
"created_at": wallet.get("created_at"),
"mode": wallet.get("mode", "file")
}
if wallet_data.get("encrypted"):
wallet_info["encrypted"] = True
wallets.append(wallet_info)
wallet_list.append(wallet_info)
output_data = {
"wallets": wallet_list,
"count": len(wallet_list),
"mode": "daemon" if use_daemon else "file"
}
output(output_data, ctx.obj.get("output_format", "table"))
except Exception as e:
error(f"Failed to list wallets: {str(e)}")
output(wallets, ctx.obj.get("output_format", "table"))
@wallet.command()
@@ -259,37 +272,43 @@ def list(ctx):
@click.pass_context
def switch(ctx, name: str):
"""Switch to a different wallet"""
wallet_dir = ctx.obj["wallet_dir"]
wallet_path = wallet_dir / f"{name}.json"
if not wallet_path.exists():
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 switching.")
# Switch to file mode
from ..config import get_config
from ..dual_mode_wallet_adapter import DualModeWalletAdapter
config = get_config()
adapter = DualModeWalletAdapter(config, use_daemon=False)
# Check if wallet exists
wallet_info = adapter.get_wallet_info(name)
if not wallet_info:
error(f"Wallet '{name}' does not exist")
return
# Update config
config_file = Path.home() / ".aitbc" / "config.yaml"
config = {}
if config_file.exists():
import yaml
with open(config_file, "r") as f:
config = yaml.safe_load(f) or {}
config["active_wallet"] = name
# Save config
config_file.parent.mkdir(parents=True, exist_ok=True)
with open(config_file, "w") as f:
yaml.dump(config, f, default_flow_style=False)
success(f"Switched to wallet '{name}'")
# Load wallet to get address (will handle encryption)
wallet_data = _load_wallet(wallet_path, name)
output(
{"active_wallet": name, "address": wallet_data["address"]},
ctx.obj.get("output_format", "table"),
)
yaml.dump(config, f)
success(f"Switched to wallet: {name}")
output({
"active_wallet": name,
"mode": "daemon" if use_daemon else "file",
"wallet_info": wallet_info
}, ctx.obj.get("output_format", "table"))
@wallet.command()
@@ -424,12 +443,12 @@ def info(ctx):
active_wallet = config.get("active_wallet", "default")
wallet_info = {
"name": wallet_data["wallet_id"],
"type": wallet_data.get("type", "simple"),
"name": wallet_data.get("name", wallet_name),
"type": wallet_data.get("type", wallet_data.get("wallet_type", "simple")),
"address": wallet_data["address"],
"public_key": wallet_data["public_key"],
"public_key": wallet_data.get("public_key", "N/A"),
"created_at": wallet_data["created_at"],
"active": wallet_data["wallet_id"] == active_wallet,
"active": wallet_data.get("name", wallet_name) == active_wallet,
"path": str(wallet_path),
}
@@ -734,148 +753,191 @@ def address(ctx):
@click.pass_context
def send(ctx, to_address: str, amount: float, description: Optional[str]):
"""Send AITBC to another address"""
adapter = ctx.obj["wallet_adapter"]
use_daemon = ctx.obj["use_daemon"]
wallet_name = ctx.obj["wallet_name"]
wallet_path = ctx.obj["wallet_path"]
config = ctx.obj.get("config")
if not wallet_path.exists():
error(f"Wallet '{wallet_name}' not found")
# 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 send.")
# Switch to file mode
from ..config import get_config
from ..dual_mode_wallet_adapter import DualModeWalletAdapter
config = get_config()
adapter = DualModeWalletAdapter(config, use_daemon=False)
ctx.obj["wallet_adapter"] = adapter
# Get password for transaction
password = getpass.getpass(f"Enter password for wallet '{wallet_name}': ")
try:
result = adapter.send_transaction(wallet_name, password, to_address, amount, description)
# Display results
output(result, ctx.obj.get("output_format", "table"))
# Update active wallet if successful
if result:
success(f"Transaction sent successfully")
except Exception as e:
error(f"Failed to send transaction: {str(e)}")
return
wallet_data = _load_wallet(wallet_path, wallet_name)
balance = wallet_data.get("balance", 0)
if balance < amount:
error(f"Insufficient balance. Available: {balance}, Required: {amount}")
ctx.exit(1)
return
# Try to send via blockchain
if config:
try:
with httpx.Client() as client:
response = client.post(
f"{config.coordinator_url.rstrip('/')}/rpc/sendTx?chain_id=ait-devnet",
json={
"type": "TRANSFER",
"sender": wallet_data["address"],
"nonce": 0, # Will need to get actual nonce
"fee": 1,
"payload": {
"to": to_address,
"amount": int(amount * 1000000000), # Convert to smallest unit
"description": description or "",
},
"sig": None, # Will need to sign transaction
},
headers={"X-Api-Key": getattr(config, "api_key", "") or ""},
)
if response.status_code == 201:
tx = response.json()
# Update local wallet
transaction = {
"type": "send",
"amount": -amount,
"to_address": to_address,
"tx_hash": tx.get("hash"),
"description": description or "",
"timestamp": datetime.now().isoformat(),
}
wallet_data["transactions"].append(transaction)
wallet_data["balance"] = balance - amount
# Use _save_wallet to preserve encryption
if wallet_data.get("encrypted"):
password = _get_wallet_password(wallet_name)
_save_wallet(wallet_path, wallet_data, password)
else:
_save_wallet(wallet_path, wallet_data)
success(f"Sent {amount} AITBC to {to_address}")
output(
{
"wallet": wallet_name,
"tx_hash": tx.get("hash"),
"amount": amount,
"to": to_address,
"new_balance": wallet_data["balance"],
},
ctx.obj.get("output_format", "table"),
)
return
except Exception as e:
error(f"Network error: {e}")
# Fallback: just record locally
transaction = {
"type": "send",
"amount": -amount,
"to_address": to_address,
"description": description or "",
"timestamp": datetime.now().isoformat(),
"pending": True,
}
wallet_data["transactions"].append(transaction)
wallet_data["balance"] = balance - amount
# Save wallet with encryption
password = None
if wallet_data.get("encrypted"):
password = _get_wallet_password(wallet_name)
_save_wallet(wallet_path, wallet_data, password)
output(
{
"wallet": wallet_name,
"amount": amount,
"to": to_address,
"new_balance": wallet_data["balance"],
"note": "Transaction recorded locally (blockchain RPC not available)",
},
ctx.obj.get("output_format", "table"),
)
@wallet.command()
@click.argument("to_address")
@click.argument("amount", type=float)
@click.option("--description", help="Transaction description")
@click.pass_context
def request_payment(ctx, to_address: str, amount: float, description: Optional[str]):
"""Request payment from another address"""
wallet_name = ctx.obj["wallet_name"]
wallet_path = ctx.obj["wallet_path"]
if not wallet_path.exists():
error(f"Wallet '{wallet_name}' not found")
return
wallet_data = _load_wallet(wallet_path, wallet_name)
# Create payment request
request = {
"from_address": to_address,
"to_address": wallet_data["address"],
"amount": amount,
"description": description or "",
"timestamp": datetime.now().isoformat(),
}
output(
{
"wallet": wallet_name,
"payment_request": request,
"note": "Share this with the payer to request payment",
},
ctx.obj.get("output_format", "table"),
)
@wallet.command()
@click.pass_context
def balance(ctx):
"""Check wallet balance"""
adapter = ctx.obj["wallet_adapter"]
use_daemon = ctx.obj["use_daemon"]
wallet_name = ctx.obj["wallet_name"]
# 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 balance.")
# Switch to file mode
from ..config import get_config
from ..dual_mode_wallet_adapter import DualModeWalletAdapter
config = get_config()
adapter = DualModeWalletAdapter(config, use_daemon=False)
ctx.obj["wallet_adapter"] = adapter
try:
balance = adapter.get_wallet_balance(wallet_name)
wallet_info = adapter.get_wallet_info(wallet_name)
if balance is None:
error(f"Wallet '{wallet_name}' not found")
return
output_data = {
"wallet_name": wallet_name,
"balance": balance,
"address": wallet_info.get("address") if wallet_info else None,
"mode": "daemon" if use_daemon else "file"
}
output(output_data, ctx.obj.get("output_format", "table"))
except Exception as e:
error(f"Failed to get wallet balance: {str(e)}")
@wallet.group()
def daemon():
"""Wallet daemon management commands"""
pass
@daemon.command()
@click.pass_context
def status(ctx):
"""Check wallet daemon status"""
from ..config import get_config
from ..wallet_daemon_client import WalletDaemonClient
config = get_config()
client = WalletDaemonClient(config)
if client.is_available():
status_info = client.get_status()
success("Wallet daemon is available")
output(status_info, ctx.obj.get("output_format", "table"))
else:
error("Wallet daemon is not available")
output({
"status": "unavailable",
"wallet_url": config.wallet_url,
"suggestion": "Start the wallet daemon or check the configuration"
}, ctx.obj.get("output_format", "table"))
@daemon.command()
@click.pass_context
def configure(ctx):
"""Configure wallet daemon settings"""
from ..config import get_config
config = get_config()
output({
"wallet_url": config.wallet_url,
"timeout": getattr(config, 'timeout', 30),
"suggestion": "Use AITBC_WALLET_URL environment variable or config file to change settings"
}, ctx.obj.get("output_format", "table"))
@wallet.command()
@click.argument("wallet_name")
@click.option("--password", help="Wallet password")
@click.option("--new-password", help="New password for daemon wallet")
@click.option("--force", is_flag=True, help="Force migration even if wallet exists")
@click.pass_context
def migrate_to_daemon(ctx, wallet_name: str, password: Optional[str], new_password: Optional[str], force: bool):
"""Migrate a file-based wallet to daemon storage"""
from ..wallet_migration_service import WalletMigrationService
from ..config import get_config
config = get_config()
migration_service = WalletMigrationService(config)
if not migration_service.is_daemon_available():
error("Wallet daemon is not available")
return
try:
result = migration_service.migrate_to_daemon(wallet_name, password, new_password, force)
success(f"Migrated wallet '{wallet_name}' to daemon")
output(result, ctx.obj.get("output_format", "table"))
except Exception as e:
error(f"Failed to migrate wallet: {str(e)}")
@wallet.command()
@click.argument("wallet_name")
@click.option("--password", help="Wallet password")
@click.option("--new-password", help="New password for file wallet")
@click.option("--force", is_flag=True, help="Force migration even if wallet exists")
@click.pass_context
def migrate_to_file(ctx, wallet_name: str, password: Optional[str], new_password: Optional[str], force: bool):
"""Migrate a daemon-based wallet to file storage"""
from ..wallet_migration_service import WalletMigrationService
from ..config import get_config
config = get_config()
migration_service = WalletMigrationService(config)
if not migration_service.is_daemon_available():
error("Wallet daemon is not available")
return
try:
result = migration_service.migrate_to_file(wallet_name, password, new_password, force)
success(f"Migrated wallet '{wallet_name}' to file storage")
output(result, ctx.obj.get("output_format", "table"))
except Exception as e:
error(f"Failed to migrate wallet: {str(e)}")
@wallet.command()
@click.pass_context
def migration_status(ctx):
"""Show wallet migration status"""
from ..wallet_migration_service import WalletMigrationService
from ..config import get_config
config = get_config()
migration_service = WalletMigrationService(config)
try:
status = migration_service.get_migration_status()
output(status, ctx.obj.get("output_format", "table"))
except Exception as e:
error(f"Failed to get migration status: {str(e)}")
def stats(ctx):
"""Show wallet statistics"""
wallet_name = ctx.obj["wallet_name"]
@@ -1603,3 +1665,265 @@ def rewards(ctx):
},
ctx.obj.get("output_format", "table"),
)
# Multi-Chain Commands
@wallet.group()
def chain():
"""Multi-chain wallet operations"""
pass
@chain.command()
@click.pass_context
def list(ctx):
"""List all blockchain chains"""
adapter = ctx.obj["wallet_adapter"]
use_daemon = ctx.obj["use_daemon"]
if not use_daemon:
error("Chain operations require daemon mode. Use --use-daemon flag.")
return
if not adapter.is_daemon_available():
error("Wallet daemon is not available")
return
try:
chains = adapter.list_chains()
output({
"chains": chains,
"count": len(chains),
"mode": "daemon"
}, ctx.obj.get("output_format", "table"))
except Exception as e:
error(f"Failed to list chains: {str(e)}")
@chain.command()
@click.argument("chain_id")
@click.argument("name")
@click.argument("coordinator_url")
@click.argument("coordinator_api_key")
@click.pass_context
def create(ctx, chain_id: str, name: str, coordinator_url: str, coordinator_api_key: str):
"""Create a new blockchain chain"""
adapter = ctx.obj["wallet_adapter"]
use_daemon = ctx.obj["use_daemon"]
if not use_daemon:
error("Chain operations require daemon mode. Use --use-daemon flag.")
return
if not adapter.is_daemon_available():
error("Wallet daemon is not available")
return
try:
chain = adapter.create_chain(chain_id, name, coordinator_url, coordinator_api_key)
if chain:
success(f"Created chain: {chain_id}")
output(chain, ctx.obj.get("output_format", "table"))
else:
error(f"Failed to create chain: {chain_id}")
except Exception as e:
error(f"Failed to create chain: {str(e)}")
@chain.command()
@click.pass_context
def status(ctx):
"""Get chain status and statistics"""
adapter = ctx.obj["wallet_adapter"]
use_daemon = ctx.obj["use_daemon"]
if not use_daemon:
error("Chain operations require daemon mode. Use --use-daemon flag.")
return
if not adapter.is_daemon_available():
error("Wallet daemon is not available")
return
try:
status = adapter.get_chain_status()
output(status, ctx.obj.get("output_format", "table"))
except Exception as e:
error(f"Failed to get chain status: {str(e)}")
@chain.command()
@click.argument("chain_id")
@click.pass_context
def wallets(ctx, chain_id: str):
"""List wallets in a specific chain"""
adapter = ctx.obj["wallet_adapter"]
use_daemon = ctx.obj["use_daemon"]
if not use_daemon:
error("Chain operations require daemon mode. Use --use-daemon flag.")
return
if not adapter.is_daemon_available():
error("Wallet daemon is not available")
return
try:
wallets = adapter.list_wallets_in_chain(chain_id)
output({
"chain_id": chain_id,
"wallets": wallets,
"count": len(wallets),
"mode": "daemon"
}, ctx.obj.get("output_format", "table"))
except Exception as e:
error(f"Failed to list wallets in chain {chain_id}: {str(e)}")
@chain.command()
@click.argument("chain_id")
@click.argument("wallet_name")
@click.pass_context
def info(ctx, chain_id: str, wallet_name: str):
"""Get wallet information from a specific chain"""
adapter = ctx.obj["wallet_adapter"]
use_daemon = ctx.obj["use_daemon"]
if not use_daemon:
error("Chain operations require daemon mode. Use --use-daemon flag.")
return
if not adapter.is_daemon_available():
error("Wallet daemon is not available")
return
try:
wallet_info = adapter.get_wallet_info_in_chain(chain_id, wallet_name)
if wallet_info:
output(wallet_info, ctx.obj.get("output_format", "table"))
else:
error(f"Wallet '{wallet_name}' not found in chain '{chain_id}'")
except Exception as e:
error(f"Failed to get wallet info: {str(e)}")
@chain.command()
@click.argument("chain_id")
@click.argument("wallet_name")
@click.pass_context
def balance(ctx, chain_id: str, wallet_name: str):
"""Get wallet balance in a specific chain"""
adapter = ctx.obj["wallet_adapter"]
use_daemon = ctx.obj["use_daemon"]
if not use_daemon:
error("Chain operations require daemon mode. Use --use-daemon flag.")
return
if not adapter.is_daemon_available():
error("Wallet daemon is not available")
return
try:
balance = adapter.get_wallet_balance_in_chain(chain_id, wallet_name)
if balance is not None:
output({
"chain_id": chain_id,
"wallet_name": wallet_name,
"balance": balance,
"mode": "daemon"
}, ctx.obj.get("output_format", "table"))
else:
error(f"Could not get balance for wallet '{wallet_name}' in chain '{chain_id}'")
except Exception as e:
error(f"Failed to get wallet balance: {str(e)}")
@chain.command()
@click.argument("source_chain_id")
@click.argument("target_chain_id")
@click.argument("wallet_name")
@click.option("--new-password", help="New password for target chain wallet")
@click.pass_context
def migrate(ctx, source_chain_id: str, target_chain_id: str, wallet_name: str, new_password: Optional[str]):
"""Migrate a wallet from one chain to another"""
adapter = ctx.obj["wallet_adapter"]
use_daemon = ctx.obj["use_daemon"]
if not use_daemon:
error("Chain operations require daemon mode. Use --use-daemon flag.")
return
if not adapter.is_daemon_available():
error("Wallet daemon is not available")
return
try:
# Get password
import getpass
password = getpass.getpass(f"Enter password for wallet '{wallet_name}': ")
result = adapter.migrate_wallet(source_chain_id, target_chain_id, wallet_name, password, new_password)
if result:
success(f"Migrated wallet '{wallet_name}' from '{source_chain_id}' to '{target_chain_id}'")
output(result, ctx.obj.get("output_format", "table"))
else:
error(f"Failed to migrate wallet '{wallet_name}'")
except Exception as e:
error(f"Failed to migrate wallet: {str(e)}")
@wallet.command()
@click.argument("chain_id")
@click.argument("wallet_name")
@click.option("--type", "wallet_type", default="hd", help="Wallet type (hd, simple)")
@click.option("--no-encrypt", is_flag=True, help="Skip wallet encryption (not recommended)")
@click.pass_context
def create_in_chain(ctx, chain_id: str, wallet_name: str, wallet_type: str, no_encrypt: bool):
"""Create a wallet in a specific chain"""
adapter = ctx.obj["wallet_adapter"]
use_daemon = ctx.obj["use_daemon"]
if not use_daemon:
error("Chain operations require daemon mode. Use --use-daemon flag.")
return
if not adapter.is_daemon_available():
error("Wallet daemon is not available")
return
try:
# Get password
import getpass
if not no_encrypt:
password = getpass.getpass(f"Enter password for wallet '{wallet_name}': ")
confirm_password = getpass.getpass(f"Confirm password for wallet '{wallet_name}': ")
if password != confirm_password:
error("Passwords do not match")
return
else:
password = "insecure" # Default password for unencrypted wallets
metadata = {
"wallet_type": wallet_type,
"encrypted": not no_encrypt,
"created_at": datetime.now().isoformat()
}
result = adapter.create_wallet_in_chain(chain_id, wallet_name, password, wallet_type, metadata)
if result:
success(f"Created wallet '{wallet_name}' in chain '{chain_id}'")
output(result, ctx.obj.get("output_format", "table"))
else:
error(f"Failed to create wallet '{wallet_name}' in chain '{chain_id}'")
except Exception as e:
error(f"Failed to create wallet in chain: {str(e)}")

View File

@@ -19,6 +19,26 @@ class Config:
blockchain_rpc_url: str = "http://127.0.0.1:8006"
wallet_url: str = "http://127.0.0.1:8002"
def _validate_localhost_urls(self):
"""Validate that all service URLs point to localhost"""
localhost_prefixes = ["http://localhost:", "http://127.0.0.1:", "https://localhost:", "https://127.0.0.1:"]
urls_to_check = [
("coordinator_url", self.coordinator_url),
("blockchain_rpc_url", self.blockchain_rpc_url),
("wallet_url", self.wallet_url)
]
for url_name, url in urls_to_check:
if not any(url.startswith(prefix) for prefix in localhost_prefixes):
# Force to localhost if not already
if url_name == "coordinator_url":
self.coordinator_url = "http://localhost:8000"
elif url_name == "blockchain_rpc_url":
self.blockchain_rpc_url = "http://localhost:8006"
elif url_name == "wallet_url":
self.wallet_url = "http://localhost:8002"
def __post_init__(self):
"""Initialize configuration"""
# Load environment variables
@@ -45,6 +65,9 @@ class Config:
self.blockchain_rpc_url = os.getenv("AITBC_BLOCKCHAIN_RPC_URL")
if os.getenv("AITBC_WALLET_URL"):
self.wallet_url = os.getenv("AITBC_WALLET_URL")
# Validate and enforce localhost URLs
self._validate_localhost_urls()
def load_from_file(self):
"""Load configuration from YAML file"""
@@ -60,6 +83,9 @@ class Config:
self.wallet_url = data.get('wallet_url', self.wallet_url)
except Exception as e:
print(f"Warning: Could not load config file: {e}")
# Validate and enforce localhost URLs after file loading
self._validate_localhost_urls()
def save_to_file(self):
"""Save configuration to YAML file"""

View File

@@ -0,0 +1,567 @@
"""Dual-Mode Wallet Adapter for AITBC CLI
This module provides an abstraction layer that supports both file-based
and daemon-based wallet operations, allowing seamless switching between modes.
"""
import json
import shutil
from pathlib import Path
from typing import Dict, Any, Optional, List, Union
from datetime import datetime
from .wallet_daemon_client import WalletDaemonClient, WalletInfo, WalletBalance, ChainInfo, WalletMigrationResult
from .config import Config
from .utils import error, success, output
class DualModeWalletAdapter:
"""Adapter supporting both file-based and daemon-based wallet operations"""
def __init__(self, config: Config, use_daemon: bool = False):
self.config = config
self.use_daemon = use_daemon
self.wallet_dir = Path.home() / ".aitbc" / "wallets"
self.wallet_dir.mkdir(parents=True, exist_ok=True)
if use_daemon:
self.daemon_client = WalletDaemonClient(config)
else:
self.daemon_client = None
def is_daemon_available(self) -> bool:
"""Check if daemon is available"""
if not self.daemon_client:
return False
return self.daemon_client.is_available()
def get_daemon_status(self) -> Dict[str, Any]:
"""Get daemon status"""
if not self.daemon_client:
return {"status": "disabled", "message": "Daemon mode not enabled"}
return self.daemon_client.get_status()
def create_wallet(self, wallet_name: str, password: str, wallet_type: str = "hd",
metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""Create a wallet using the appropriate mode"""
if self.use_daemon:
return self._create_wallet_daemon(wallet_name, password, metadata)
else:
return self._create_wallet_file(wallet_name, password, wallet_type)
def _create_wallet_daemon(self, wallet_name: str, password: str,
metadata: Optional[Dict[str, Any]]) -> Dict[str, Any]:
"""Create wallet using daemon"""
try:
if not self.is_daemon_available():
error("Wallet daemon is not available")
raise Exception("Daemon unavailable")
wallet_info = self.daemon_client.create_wallet(wallet_name, password, metadata)
success(f"Created daemon wallet: {wallet_name}")
return {
"mode": "daemon",
"wallet_name": wallet_name,
"wallet_id": wallet_info.wallet_id,
"public_key": wallet_info.public_key,
"address": wallet_info.address,
"created_at": wallet_info.created_at,
"metadata": wallet_info.metadata
}
except Exception as e:
error(f"Failed to create daemon wallet: {str(e)}")
raise
def _create_wallet_file(self, wallet_name: str, password: str, wallet_type: str) -> Dict[str, Any]:
"""Create wallet using file-based storage"""
from .commands.wallet import _save_wallet
wallet_path = self.wallet_dir / f"{wallet_name}.json"
if wallet_path.exists():
error(f"Wallet '{wallet_name}' already exists")
raise Exception("Wallet exists")
# Generate wallet data
if wallet_type == "simple":
# Simple wallet with deterministic key for testing
private_key = f"simple_key_{wallet_name}_{datetime.now().isoformat()}"
address = f"aitbc1{wallet_name}_simple"
else:
# HD wallet (placeholder for real implementation)
private_key = f"hd_key_{wallet_name}_{datetime.now().isoformat()}"
address = f"aitbc1{wallet_name}_hd"
wallet_data = {
"name": wallet_name,
"address": address,
"balance": 0.0,
"encrypted": bool(password),
"private_key": private_key,
"transactions": [],
"created_at": datetime.now().isoformat(),
"wallet_type": wallet_type
}
# Save wallet
save_password = password if password else None
_save_wallet(wallet_path, wallet_data, save_password)
success(f"Created file wallet: {wallet_name}")
return {
"mode": "file",
"wallet_name": wallet_name,
"address": address,
"balance": 0.0,
"wallet_type": wallet_type,
"created_at": wallet_data["created_at"]
}
def list_wallets(self) -> List[Dict[str, Any]]:
"""List wallets using the appropriate mode"""
if self.use_daemon:
return self._list_wallets_daemon()
else:
return self._list_wallets_file()
def _list_wallets_daemon(self) -> List[Dict[str, Any]]:
"""List wallets using daemon"""
try:
if not self.is_daemon_available():
error("Wallet daemon is not available")
return []
wallets = self.daemon_client.list_wallets()
return [
{
"mode": "daemon",
"wallet_name": w.wallet_id,
"wallet_id": w.wallet_id,
"public_key": w.public_key,
"address": w.address,
"created_at": w.created_at,
"metadata": w.metadata
}
for w in wallets
]
except Exception as e:
error(f"Failed to list daemon wallets: {str(e)}")
return []
def _list_wallets_file(self) -> List[Dict[str, Any]]:
"""List wallets using file-based storage"""
wallets = []
for wallet_file in self.wallet_dir.glob("*.json"):
try:
with open(wallet_file, 'r') as f:
wallet_data = json.load(f)
wallets.append({
"mode": "file",
"wallet_name": wallet_data.get("name") or wallet_data.get("wallet_id") or wallet_file.stem,
"address": wallet_data.get("address"),
"balance": wallet_data.get("balance", 0.0),
"wallet_type": wallet_data.get("wallet_type", "hd"),
"created_at": wallet_data.get("created_at"),
"encrypted": wallet_data.get("encrypted", False)
})
except Exception as e:
error(f"Error reading wallet file {wallet_file}: {str(e)}")
return wallets
def get_wallet_info(self, wallet_name: str) -> Optional[Dict[str, Any]]:
"""Get wallet information using the appropriate mode"""
if self.use_daemon:
return self._get_wallet_info_daemon(wallet_name)
else:
return self._get_wallet_info_file(wallet_name)
def _get_wallet_info_daemon(self, wallet_name: str) -> Optional[Dict[str, Any]]:
"""Get wallet info using daemon"""
try:
if not self.is_daemon_available():
return None
wallet_info = self.daemon_client.get_wallet_info(wallet_name)
if wallet_info:
return {
"mode": "daemon",
"wallet_name": wallet_name,
"wallet_id": wallet_info.wallet_id,
"public_key": wallet_info.public_key,
"address": wallet_info.address,
"created_at": wallet_info.created_at,
"metadata": wallet_info.metadata
}
return None
except Exception as e:
error(f"Failed to get daemon wallet info: {str(e)}")
return None
def _get_wallet_info_file(self, wallet_name: str) -> Optional[Dict[str, Any]]:
"""Get wallet info using file-based storage"""
from .commands.wallet import _load_wallet
wallet_path = self.wallet_dir / f"{wallet_name}.json"
if not wallet_path.exists():
return None
try:
with open(wallet_path, 'r') as f:
wallet_data = json.load(f)
return {
"mode": "file",
"wallet_name": wallet_data.get("name") or wallet_data.get("wallet_id") or wallet_name,
"address": wallet_data.get("address"),
"balance": wallet_data.get("balance", 0.0),
"wallet_type": wallet_data.get("wallet_type", "hd"),
"created_at": wallet_data.get("created_at"),
"encrypted": wallet_data.get("encrypted", False),
"transactions": wallet_data.get("transactions", [])
}
except Exception as e:
error(f"Failed to get file wallet info: {str(e)}")
return None
def get_wallet_balance(self, wallet_name: str) -> Optional[float]:
"""Get wallet balance using the appropriate mode"""
if self.use_daemon:
return self._get_wallet_balance_daemon(wallet_name)
else:
return self._get_wallet_balance_file(wallet_name)
def _get_wallet_balance_daemon(self, wallet_name: str) -> Optional[float]:
"""Get wallet balance using daemon"""
try:
if not self.is_daemon_available():
return None
balance_info = self.daemon_client.get_wallet_balance(wallet_name)
if balance_info:
return balance_info.balance
return None
except Exception as e:
error(f"Failed to get daemon wallet balance: {str(e)}")
return None
def _get_wallet_balance_file(self, wallet_name: str) -> Optional[float]:
"""Get wallet balance using file-based storage"""
wallet_info = self._get_wallet_info_file(wallet_name)
if wallet_info:
return wallet_info.get("balance", 0.0)
return None
def send_transaction(self, wallet_name: str, password: str, to_address: str,
amount: float, description: Optional[str] = None) -> Dict[str, Any]:
"""Send transaction using the appropriate mode"""
if self.use_daemon:
return self._send_transaction_daemon(wallet_name, password, to_address, amount, description)
else:
return self._send_transaction_file(wallet_name, password, to_address, amount, description)
def _send_transaction_daemon(self, wallet_name: str, password: str, to_address: str,
amount: float, description: Optional[str]) -> Dict[str, Any]:
"""Send transaction using daemon"""
try:
if not self.is_daemon_available():
error("Wallet daemon is not available")
raise Exception("Daemon unavailable")
result = self.daemon_client.send_transaction(wallet_name, password, to_address, amount, description)
success(f"Sent {amount} AITBC to {to_address} via daemon")
return {
"mode": "daemon",
"wallet_name": wallet_name,
"to_address": to_address,
"amount": amount,
"description": description,
"tx_hash": result.get("tx_hash"),
"timestamp": result.get("timestamp")
}
except Exception as e:
error(f"Failed to send daemon transaction: {str(e)}")
raise
def _send_transaction_file(self, wallet_name: str, password: str, to_address: str,
amount: float, description: Optional[str]) -> Dict[str, Any]:
"""Send transaction using file-based storage"""
from .commands.wallet import _load_wallet, _save_wallet
wallet_path = self.wallet_dir / f"{wallet_name}.json"
if not wallet_path.exists():
error(f"Wallet '{wallet_name}' not found")
raise Exception("Wallet not found")
wallet_data = _load_wallet(wallet_path, wallet_name)
balance = wallet_data.get("balance", 0)
if balance < amount:
error(f"Insufficient balance. Available: {balance}, Required: {amount}")
raise Exception("Insufficient balance")
# Add transaction
transaction = {
"type": "send",
"amount": -amount,
"to_address": to_address,
"description": description or "",
"timestamp": datetime.now().isoformat(),
}
wallet_data["transactions"].append(transaction)
wallet_data["balance"] = balance - amount
# Save wallet
save_password = password if wallet_data.get("encrypted") else None
_save_wallet(wallet_path, wallet_data, save_password)
success(f"Sent {amount} AITBC to {to_address}")
return {
"mode": "file",
"wallet_name": wallet_name,
"to_address": to_address,
"amount": amount,
"description": description,
"new_balance": wallet_data["balance"],
"timestamp": transaction["timestamp"]
}
def delete_wallet(self, wallet_name: str, password: str) -> bool:
"""Delete wallet using the appropriate mode"""
if self.use_daemon:
return self._delete_wallet_daemon(wallet_name, password)
else:
return self._delete_wallet_file(wallet_name, password)
def _delete_wallet_daemon(self, wallet_name: str, password: str) -> bool:
"""Delete wallet using daemon"""
try:
if not self.is_daemon_available():
return False
return self.daemon_client.delete_wallet(wallet_name, password)
except Exception as e:
error(f"Failed to delete daemon wallet: {str(e)}")
return False
def _delete_wallet_file(self, wallet_name: str, password: str) -> bool:
"""Delete wallet using file-based storage"""
wallet_path = self.wallet_dir / f"{wallet_name}.json"
if not wallet_path.exists():
error(f"Wallet '{wallet_name}' not found")
return False
try:
wallet_path.unlink()
success(f"Deleted wallet: {wallet_name}")
return True
except Exception as e:
error(f"Failed to delete wallet: {str(e)}")
return False
# Multi-Chain Methods
def list_chains(self) -> List[Dict[str, Any]]:
"""List all blockchain chains"""
if not self.use_daemon or not self.is_daemon_available():
error("Chain listing requires daemon mode")
return []
try:
chains = self.daemon_client.list_chains()
return [
{
"chain_id": chain.chain_id,
"name": chain.name,
"status": chain.status,
"coordinator_url": chain.coordinator_url,
"created_at": chain.created_at,
"updated_at": chain.updated_at,
"wallet_count": chain.wallet_count,
"recent_activity": chain.recent_activity
}
for chain in chains
]
except Exception as e:
error(f"Failed to list chains: {str(e)}")
return []
def create_chain(self, chain_id: str, name: str, coordinator_url: str,
coordinator_api_key: str, metadata: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
"""Create a new blockchain chain"""
if not self.use_daemon or not self.is_daemon_available():
error("Chain creation requires daemon mode")
return None
try:
chain = self.daemon_client.create_chain(chain_id, name, coordinator_url, coordinator_api_key, metadata)
return {
"chain_id": chain.chain_id,
"name": chain.name,
"status": chain.status,
"coordinator_url": chain.coordinator_url,
"created_at": chain.created_at,
"updated_at": chain.updated_at,
"wallet_count": chain.wallet_count,
"recent_activity": chain.recent_activity
}
except Exception as e:
error(f"Failed to create chain: {str(e)}")
return None
def create_wallet_in_chain(self, chain_id: str, wallet_name: str, password: str,
wallet_type: str = "hd", metadata: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
"""Create a wallet in a specific chain"""
if not self.use_daemon or not self.is_daemon_available():
error("Chain-specific wallet creation requires daemon mode")
return None
try:
wallet = self.daemon_client.create_wallet_in_chain(chain_id, wallet_name, password, metadata)
return {
"mode": "daemon",
"chain_id": chain_id,
"wallet_name": wallet.wallet_id,
"public_key": wallet.public_key,
"address": wallet.address,
"created_at": wallet.created_at,
"wallet_type": wallet_type,
"metadata": wallet.metadata or {}
}
except Exception as e:
error(f"Failed to create wallet in chain {chain_id}: {str(e)}")
return None
def list_wallets_in_chain(self, chain_id: str) -> List[Dict[str, Any]]:
"""List wallets in a specific chain"""
if not self.use_daemon or not self.is_daemon_available():
error("Chain-specific wallet listing requires daemon mode")
return []
try:
wallets = self.daemon_client.list_wallets_in_chain(chain_id)
return [
{
"mode": "daemon",
"chain_id": chain_id,
"wallet_name": wallet.wallet_id,
"public_key": wallet.public_key,
"address": wallet.address,
"created_at": wallet.created_at,
"metadata": wallet.metadata or {}
}
for wallet in wallets
]
except Exception as e:
error(f"Failed to list wallets in chain {chain_id}: {str(e)}")
return []
def get_wallet_info_in_chain(self, chain_id: str, wallet_name: str) -> Optional[Dict[str, Any]]:
"""Get wallet information from a specific chain"""
if not self.use_daemon or not self.is_daemon_available():
error("Chain-specific wallet info requires daemon mode")
return None
try:
wallet = self.daemon_client.get_wallet_info_in_chain(chain_id, wallet_name)
if wallet:
return {
"mode": "daemon",
"chain_id": chain_id,
"wallet_name": wallet.wallet_id,
"public_key": wallet.public_key,
"address": wallet.address,
"created_at": wallet.created_at,
"metadata": wallet.metadata or {}
}
return None
except Exception as e:
error(f"Failed to get wallet info from chain {chain_id}: {str(e)}")
return None
def get_wallet_balance_in_chain(self, chain_id: str, wallet_name: str) -> Optional[float]:
"""Get wallet balance in a specific chain"""
if not self.use_daemon or not self.is_daemon_available():
error("Chain-specific balance check requires daemon mode")
return None
try:
balance = self.daemon_client.get_wallet_balance_in_chain(chain_id, wallet_name)
return balance.balance if balance else None
except Exception as e:
error(f"Failed to get wallet balance in chain {chain_id}: {str(e)}")
return None
def unlock_wallet_in_chain(self, chain_id: str, wallet_name: str, password: str) -> bool:
"""Unlock a wallet in a specific chain"""
if not self.use_daemon or not self.is_daemon_available():
error("Chain-specific wallet unlock requires daemon mode")
return False
try:
return self.daemon_client.unlock_wallet_in_chain(chain_id, wallet_name, password)
except Exception as e:
error(f"Failed to unlock wallet in chain {chain_id}: {str(e)}")
return False
def sign_message_in_chain(self, chain_id: str, wallet_name: str, password: str, message: bytes) -> Optional[str]:
"""Sign a message with a wallet in a specific chain"""
if not self.use_daemon or not self.is_daemon_available():
error("Chain-specific message signing requires daemon mode")
return None
try:
return self.daemon_client.sign_message_in_chain(chain_id, wallet_name, password, message)
except Exception as e:
error(f"Failed to sign message in chain {chain_id}: {str(e)}")
return None
def migrate_wallet(self, source_chain_id: str, target_chain_id: str, wallet_name: str,
password: str, new_password: Optional[str] = None) -> Optional[Dict[str, Any]]:
"""Migrate a wallet from one chain to another"""
if not self.use_daemon or not self.is_daemon_available():
error("Wallet migration requires daemon mode")
return None
try:
result = self.daemon_client.migrate_wallet(source_chain_id, target_chain_id, wallet_name, password, new_password)
if result:
return {
"success": result.success,
"source_wallet": {
"chain_id": result.source_wallet.chain_id,
"wallet_name": result.source_wallet.wallet_id,
"public_key": result.source_wallet.public_key,
"address": result.source_wallet.address
},
"target_wallet": {
"chain_id": result.target_wallet.chain_id,
"wallet_name": result.target_wallet.wallet_id,
"public_key": result.target_wallet.public_key,
"address": result.target_wallet.address
},
"migration_timestamp": result.migration_timestamp
}
return None
except Exception as e:
error(f"Failed to migrate wallet: {str(e)}")
return None
def get_chain_status(self) -> Dict[str, Any]:
"""Get overall chain status and statistics"""
if not self.use_daemon or not self.is_daemon_available():
return {"status": "disabled", "message": "Chain status requires daemon mode"}
try:
return self.daemon_client.get_chain_status()
except Exception as e:
error(f"Failed to get chain status: {str(e)}")
return {"error": str(e)}

View File

@@ -46,6 +46,7 @@ from .commands.node import node
from .commands.analytics import analytics
from .commands.agent_comm import agent_comm
from .commands.deployment import deploy
from .commands.cross_chain import cross_chain
from .plugins import plugin, load_plugins
@@ -188,6 +189,7 @@ cli.add_command(node)
cli.add_command(analytics)
cli.add_command(agent_comm)
cli.add_command(deploy)
cli.add_command(cross_chain)
cli.add_command(plugin)
load_plugins(cli)

View File

@@ -0,0 +1,536 @@
"""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)}

View File

@@ -0,0 +1,317 @@
"""Wallet Migration Service for AITBC CLI
This module provides utilities for migrating wallets between
file-based storage and daemon-based storage.
"""
import json
import shutil
from pathlib import Path
from typing import Dict, Any, Optional, List
from datetime import datetime
from .wallet_daemon_client import WalletDaemonClient, WalletInfo
from .dual_mode_wallet_adapter import DualModeWalletAdapter
from .config import Config
from .utils import error, success, output
class WalletMigrationService:
"""Service for migrating wallets between file-based and daemon storage"""
def __init__(self, config: Config):
self.config = config
self.wallet_dir = Path.home() / ".aitbc" / "wallets"
self.wallet_dir.mkdir(parents=True, exist_ok=True)
# Create adapters for both modes
self.file_adapter = DualModeWalletAdapter(config, use_daemon=False)
self.daemon_adapter = DualModeWalletAdapter(config, use_daemon=True)
def is_daemon_available(self) -> bool:
"""Check if wallet daemon is available"""
return self.daemon_adapter.is_daemon_available()
def list_file_wallets(self) -> List[Dict[str, Any]]:
"""List all file-based wallets"""
return self.file_adapter.list_wallets()
def list_daemon_wallets(self) -> List[Dict[str, Any]]:
"""List all daemon-based wallets"""
if not self.is_daemon_available():
return []
return self.daemon_adapter.list_wallets()
def migrate_to_daemon(self, wallet_name: str, password: Optional[str] = None,
new_password: Optional[str] = None, force: bool = False) -> Dict[str, Any]:
"""Migrate a file-based wallet to daemon storage"""
try:
# Check if wallet exists in file storage
file_wallet = self.file_adapter.get_wallet_info(wallet_name)
if not file_wallet:
error(f"File wallet '{wallet_name}' not found")
raise Exception("Wallet not found")
# Check if wallet already exists in daemon
if self.is_daemon_available():
daemon_wallet = self.daemon_adapter.get_wallet_info(wallet_name)
if daemon_wallet and not force:
error(f"Wallet '{wallet_name}' already exists in daemon. Use --force to overwrite.")
raise Exception("Wallet exists in daemon")
# Get wallet data from file
wallet_path = self.wallet_dir / f"{wallet_name}.json"
with open(wallet_path, 'r') as f:
wallet_data = json.load(f)
# Prepare metadata for daemon
metadata = {
"migrated_from": "file",
"migration_date": datetime.now().isoformat(),
"original_wallet_type": wallet_data.get("wallet_type", "hd"),
"original_balance": wallet_data.get("balance", 0.0),
"transaction_count": len(wallet_data.get("transactions", [])),
"original_created_at": wallet_data.get("created_at")
}
# Use provided password or default
migration_password = new_password or password or "migrate_123"
# Create wallet in daemon
if self.is_daemon_available():
daemon_wallet_info = self.daemon_adapter.create_wallet(
wallet_name, migration_password, metadata=metadata
)
success(f"Migrated wallet '{wallet_name}' to daemon")
return {
"wallet_name": wallet_name,
"source_mode": "file",
"target_mode": "daemon",
"migrated_at": datetime.now().isoformat(),
"original_balance": wallet_data.get("balance", 0.0),
"transaction_count": len(wallet_data.get("transactions", [])),
"daemon_wallet_id": daemon_wallet_info.get("wallet_id"),
"backup_file": str(wallet_path)
}
else:
error("Wallet daemon is not available for migration")
raise Exception("Daemon unavailable")
except Exception as e:
error(f"Failed to migrate wallet to daemon: {str(e)}")
raise
def migrate_to_file(self, wallet_name: str, password: Optional[str] = None,
new_password: Optional[str] = None, force: bool = False) -> Dict[str, Any]:
"""Migrate a daemon-based wallet to file storage"""
try:
if not self.is_daemon_available():
error("Wallet daemon is not available")
raise Exception("Daemon unavailable")
# Check if wallet exists in daemon
daemon_wallet = self.daemon_adapter.get_wallet_info(wallet_name)
if not daemon_wallet:
error(f"Daemon wallet '{wallet_name}' not found")
raise Exception("Wallet not found")
# Check if wallet already exists in file storage
file_wallet = self.file_adapter.get_wallet_info(wallet_name)
if file_wallet and not force:
error(f"Wallet '{wallet_name}' already exists in file storage. Use --force to overwrite.")
raise Exception("Wallet exists in file storage")
# Get additional info from daemon
balance_info = self.daemon_adapter.get_wallet_balance(wallet_name)
# Create file wallet data
wallet_data = {
"name": wallet_name,
"address": daemon_wallet.get("address") or f"aitbc1{wallet_name}_migrated",
"balance": balance_info.balance if balance_info else 0.0,
"encrypted": bool(new_password or password),
"private_key": f"migrated_from_daemon_{wallet_name}_{datetime.now().isoformat()}",
"transactions": [],
"created_at": daemon_wallet.get("created_at") or datetime.now().isoformat(),
"wallet_type": "hd",
"migration_metadata": {
"migrated_from": "daemon",
"migration_date": datetime.now().isoformat(),
"original_wallet_id": daemon_wallet.get("wallet_id"),
"original_public_key": daemon_wallet.get("public_key"),
"daemon_metadata": daemon_wallet.get("metadata", {})
}
}
# Save to file
wallet_path = self.wallet_dir / f"{wallet_name}.json"
with open(wallet_path, 'w') as f:
json.dump(wallet_data, f, indent=2)
success(f"Migrated wallet '{wallet_name}' to file storage")
return {
"wallet_name": wallet_name,
"source_mode": "daemon",
"target_mode": "file",
"migrated_at": datetime.now().isoformat(),
"balance": wallet_data["balance"],
"wallet_file": str(wallet_path),
"original_wallet_id": daemon_wallet.get("wallet_id")
}
except Exception as e:
error(f"Failed to migrate wallet to file: {str(e)}")
raise
def sync_wallets(self, wallet_name: str, direction: str = "to_daemon") -> Dict[str, Any]:
"""Synchronize wallet data between file and daemon modes"""
try:
if direction == "to_daemon":
return self._sync_to_daemon(wallet_name)
elif direction == "to_file":
return self._sync_to_file(wallet_name)
else:
error("Invalid sync direction. Use 'to_daemon' or 'to_file'")
raise Exception("Invalid direction")
except Exception as e:
error(f"Failed to sync wallet: {str(e)}")
raise
def _sync_to_daemon(self, wallet_name: str) -> Dict[str, Any]:
"""Sync wallet data from file to daemon"""
file_wallet = self.file_adapter.get_wallet_info(wallet_name)
if not file_wallet:
error(f"File wallet '{wallet_name}' not found")
raise Exception("Wallet not found")
if not self.is_daemon_available():
error("Wallet daemon is not available")
raise Exception("Daemon unavailable")
daemon_wallet = self.daemon_adapter.get_wallet_info(wallet_name)
if not daemon_wallet:
error(f"Daemon wallet '{wallet_name}' not found")
raise Exception("Wallet not found")
# Compare and sync data
file_balance = file_wallet.get("balance", 0.0)
daemon_balance = self.daemon_adapter.get_wallet_balance(wallet_name) or 0.0
sync_info = {
"wallet_name": wallet_name,
"sync_direction": "file_to_daemon",
"sync_time": datetime.now().isoformat(),
"file_balance": file_balance,
"daemon_balance": daemon_balance,
"balance_difference": abs(file_balance - daemon_balance),
"sync_required": file_balance != daemon_balance
}
if sync_info["sync_required"]:
success(f"Wallet '{wallet_name}' sync required: balance difference {sync_info['balance_difference']}")
else:
success(f"Wallet '{wallet_name}' already in sync")
return sync_info
def _sync_to_file(self, wallet_name: str) -> Dict[str, Any]:
"""Sync wallet data from daemon to file"""
if not self.is_daemon_available():
error("Wallet daemon is not available")
raise Exception("Daemon unavailable")
daemon_wallet = self.daemon_adapter.get_wallet_info(wallet_name)
if not daemon_wallet:
error(f"Daemon wallet '{wallet_name}' not found")
raise Exception("Wallet not found")
file_wallet = self.file_adapter.get_wallet_info(wallet_name)
if not file_wallet:
error(f"File wallet '{wallet_name}' not found")
raise Exception("Wallet not found")
# Compare and sync data
file_balance = file_wallet.get("balance", 0.0)
daemon_balance = self.daemon_adapter.get_wallet_balance(wallet_name) or 0.0
sync_info = {
"wallet_name": wallet_name,
"sync_direction": "daemon_to_file",
"sync_time": datetime.now().isoformat(),
"file_balance": file_balance,
"daemon_balance": daemon_balance,
"balance_difference": abs(file_balance - daemon_balance),
"sync_required": file_balance != daemon_balance
}
if sync_info["sync_required"]:
success(f"Wallet '{wallet_name}' sync required: balance difference {sync_info['balance_difference']}")
else:
success(f"Wallet '{wallet_name}' already in sync")
return sync_info
def get_migration_status(self) -> Dict[str, Any]:
"""Get overall migration status"""
try:
file_wallets = self.list_file_wallets()
daemon_wallets = self.list_daemon_wallets() if self.is_daemon_available() else []
file_wallet_names = {w["wallet_name"] for w in file_wallets}
daemon_wallet_names = {w["wallet_name"] for w in daemon_wallets}
# Categorize wallets
file_only = file_wallet_names - daemon_wallet_names
daemon_only = daemon_wallet_names - file_wallet_names
both_modes = file_wallet_names & daemon_wallet_names
status = {
"daemon_available": self.is_daemon_available(),
"total_file_wallets": len(file_wallets),
"total_daemon_wallets": len(daemon_wallets),
"file_only_wallets": list(file_only),
"daemon_only_wallets": list(daemon_only),
"both_modes_wallets": list(both_modes),
"migration_candidates": list(file_only),
"sync_candidates": list(both_modes)
}
return status
except Exception as e:
error(f"Failed to get migration status: {str(e)}")
return {
"daemon_available": False,
"error": str(e)
}
def backup_wallet(self, wallet_name: str, backup_path: Optional[str] = None) -> str:
"""Create a backup of a wallet file"""
try:
wallet_path = self.wallet_dir / f"{wallet_name}.json"
if not wallet_path.exists():
error(f"Wallet '{wallet_name}' not found")
raise Exception("Wallet not found")
if not backup_path:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_filename = f"{wallet_name}_backup_{timestamp}.json"
backup_path = self.wallet_dir / "backups" / backup_filename
# Create backup directory
backup_path.parent.mkdir(parents=True, exist_ok=True)
# Copy wallet file
shutil.copy2(wallet_path, backup_path)
success(f"Wallet backup created: {backup_path}")
return str(backup_path)
except Exception as e:
error(f"Failed to backup wallet: {str(e)}")
raise