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)}")