diff --git a/cli/aitbc_cli/commands/genesis.py b/cli/aitbc_cli/commands/genesis.py index 6b22d498..c13c8134 100644 --- a/cli/aitbc_cli/commands/genesis.py +++ b/cli/aitbc_cli/commands/genesis.py @@ -162,16 +162,27 @@ def verify(ctx, chain_id: str): @genesis.command() -@click.option("--chain-id", default=None, help="Chain ID to show info for (auto-detected from config if not provided)") +@click.option("--chain-id", default=None, help="Chain ID to show info for (auto-detected from blockchain node if not provided)") @click.option("--data-dir", default=None, help="Data directory path (default: /var/lib/aitbc/data)") +@click.option("--rpc-url", default=None, help="Blockchain RPC URL for chain ID auto-detection (default: http://localhost:8006)") @click.pass_context -def info(ctx, chain_id: str, data_dir: Optional[str]): +def info(ctx, chain_id: str, data_dir: Optional[str], rpc_url: Optional[str]): """Show genesis block information""" - # Auto-detect chain_id from config if not provided + # Auto-detect chain_id from blockchain node if not provided if not chain_id: from ..config import get_config config = get_config() - chain_id = getattr(config, 'chain_id', 'ait-mainnet') + + # Try to get chain_id from RPC health endpoint + if not rpc_url: + rpc_url = getattr(config, 'blockchain_rpc_url', 'http://localhost:8006') + + try: + from ..utils.chain_id import get_chain_id + chain_id = get_chain_id(rpc_url, override=None, timeout=5) + except Exception: + # Fallback to config or default + chain_id = getattr(config, 'chain_id', 'ait-mainnet') # Use provided data dir or default if not data_dir: @@ -184,6 +195,7 @@ def info(ctx, chain_id: str, data_dir: Optional[str]): if not genesis_path.exists(): error(f"Genesis config not found: {genesis_path}") error(f"Chain ID: {chain_id}, Data directory: {data_dir}") + error(f"RPC URL used for detection: {rpc_url}") error("Run 'aitbc genesis init' to create genesis block") return diff --git a/cli/aitbc_cli/commands/transactions.py b/cli/aitbc_cli/commands/transactions.py index 9d81be74..55f59164 100644 --- a/cli/aitbc_cli/commands/transactions.py +++ b/cli/aitbc_cli/commands/transactions.py @@ -134,25 +134,56 @@ def _send_transaction_impl(from_wallet: str, to_address: str, amount: float, fee @click.option('--rpc-url', help='Blockchain RPC URL') def send(from_wallet: str, to_address: str, amount: float, fee: float, password: Optional[str], password_file: Optional[str], rpc_url: Optional[str]): """Send transaction from one wallet to another""" - if password_file: + # Password resolution priority: + # 1. --password flag + # 2. --password-file flag + # 3. AITBC_WALLET_PASSWORD environment variable + # 4. Check if wallet is unencrypted (skip password) + # 5. Interactive getpass prompt (only if TTY) + + if password is not None: + # Password provided via flag (even if empty string) + pass + elif password_file: with open(password_file) as f: password = f.read().strip() - elif not password: - # Check if we're in a TTY environment - if not sys.stdin.isatty(): - # Non-interactive: try environment variable - password = os.environ.get("AITBC_WALLET_PASSWORD") - if not password: + elif "AITBC_WALLET_PASSWORD" in os.environ: + # Environment variable is set (even if empty) + password = os.environ["AITBC_WALLET_PASSWORD"] + else: + # Check if wallet is unencrypted + keystore_dir = DEFAULT_KEYSTORE_DIR + sender_keystore = keystore_dir / f"{from_wallet}.json" + if sender_keystore.exists(): + with open(sender_keystore) as f: + sender_data = json.load(f) + # If wallet has no encrypted_private_key, it's unencrypted + if not sender_data.get("encrypted_private_key"): + password = "" # Empty password for unencrypted wallets + else: + # Wallet is encrypted, need password + if not sys.stdin.isatty(): + error("No TTY available for password prompt. Use --password or --password-file, or set AITBC_WALLET_PASSWORD environment variable.") + raise click.Abort() + else: + import getpass + try: + password = getpass.getpass("Enter wallet password: ") + except Exception as e: + error(f"Password prompt failed: {e}") + raise click.Abort() + else: + # Wallet file doesn't exist, will fail later in _send_transaction_impl + if not sys.stdin.isatty(): error("No TTY available for password prompt. Use --password or --password-file, or set AITBC_WALLET_PASSWORD environment variable.") raise click.Abort() - else: - # Interactive: prompt for password - import getpass - try: - password = getpass.getpass("Enter wallet password: ") - except Exception as e: - error(f"Password prompt failed: {e}") - raise click.Abort() + else: + import getpass + try: + password = getpass.getpass("Enter wallet password: ") + except Exception as e: + error(f"Password prompt failed: {e}") + raise click.Abort() if not rpc_url: rpc_url = DEFAULT_RPC_URL @@ -169,25 +200,72 @@ def send(from_wallet: str, to_address: str, amount: float, fee: float, password: @click.option('--rpc-url', help='Blockchain RPC URL') def batch(transactions_file: str, password: Optional[str], password_file: Optional[str], rpc_url: Optional[str]): """Send batch transactions""" - if password_file: + # Password resolution priority: + # 1. --password flag + # 2. --password-file flag + # 3. AITBC_WALLET_PASSWORD environment variable + # 4. Check if wallet is unencrypted (skip password) + # 5. Interactive getpass prompt (only if TTY) + + if password is not None: + # Password provided via flag (even if empty string) + pass + elif password_file: with open(password_file) as f: password = f.read().strip() - elif not password: - # Check if we're in a TTY environment - if not sys.stdin.isatty(): - # Non-interactive: try environment variable - password = os.environ.get("AITBC_WALLET_PASSWORD") - if not password: + elif "AITBC_WALLET_PASSWORD" in os.environ: + # Environment variable is set (even if empty) + password = os.environ["AITBC_WALLET_PASSWORD"] + else: + # Check if first wallet is unencrypted + with open(transactions_file) as f: + transactions_data = json.load(f) + if transactions_data: + first_wallet = transactions_data[0].get('from_wallet') + keystore_dir = DEFAULT_KEYSTORE_DIR + sender_keystore = keystore_dir / f"{first_wallet}.json" + if sender_keystore.exists(): + with open(sender_keystore) as f: + sender_data = json.load(f) + # If wallet has no encrypted_private_key, it's unencrypted + if not sender_data.get("encrypted_private_key"): + password = "" # Empty password for unencrypted wallets + else: + # Wallet is encrypted, need password + if not sys.stdin.isatty(): + error("No TTY available for password prompt. Use --password or --password-file, or set AITBC_WALLET_PASSWORD environment variable.") + raise click.Abort() + else: + import getpass + try: + password = getpass.getpass("Enter wallet password: ") + except Exception as e: + error(f"Password prompt failed: {e}") + raise click.Abort() + else: + # Wallet file doesn't exist + if not sys.stdin.isatty(): + error("No TTY available for password prompt. Use --password or --password-file, or set AITBC_WALLET_PASSWORD environment variable.") + raise click.Abort() + else: + import getpass + try: + password = getpass.getpass("Enter wallet password: ") + except Exception as e: + error(f"Password prompt failed: {e}") + raise click.Abort() + else: + # Empty transactions file + if not sys.stdin.isatty(): error("No TTY available for password prompt. Use --password or --password-file, or set AITBC_WALLET_PASSWORD environment variable.") raise click.Abort() - else: - # Interactive: prompt for password - import getpass - try: - password = getpass.getpass("Enter wallet password: ") - except Exception as e: - error(f"Password prompt failed: {e}") - raise click.Abort() + else: + import getpass + try: + password = getpass.getpass("Enter wallet password: ") + except Exception as e: + error(f"Password prompt failed: {e}") + raise click.Abort() if not rpc_url: rpc_url = DEFAULT_RPC_URL