fix: restructure password resolution and improve genesis chain_id detection
Some checks failed
CLI Tests / test-cli (push) Has been cancelled
Cross-Node Transaction Testing / transaction-test (push) Has been cancelled
Deploy to Testnet / deploy-testnet (push) Has been cancelled
Multi-Node Stress Testing / stress-test (push) Has been cancelled
Security Scanning / security-scan (push) Has been cancelled

- Restructured password resolution in transactions send/batch to check env var before TTY
- Changed from os.environ.get() to 'in os.environ' to detect empty string env vars
- Added unencrypted wallet detection to skip password for --no-encrypt wallets
- Updated genesis info to auto-detect chain_id from blockchain RPC health endpoint
- Added --rpc-url option to genesis info for custom RPC URL
- Improved error messages to show RPC URL used for detection
- Fixes termios.error when AITBC_WALLET_PASSWORD is set to empty string
This commit is contained in:
aitbc
2026-05-26 13:38:24 +02:00
parent 99ef26e464
commit 7128927707
2 changed files with 124 additions and 34 deletions

View File

@@ -162,16 +162,27 @@ def verify(ctx, chain_id: str):
@genesis.command() @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("--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 @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""" """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: if not chain_id:
from ..config import get_config from ..config import get_config
config = 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 # Use provided data dir or default
if not data_dir: if not data_dir:
@@ -184,6 +195,7 @@ def info(ctx, chain_id: str, data_dir: Optional[str]):
if not genesis_path.exists(): if not genesis_path.exists():
error(f"Genesis config not found: {genesis_path}") error(f"Genesis config not found: {genesis_path}")
error(f"Chain ID: {chain_id}, Data directory: {data_dir}") 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") error("Run 'aitbc genesis init' to create genesis block")
return return

View File

@@ -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') @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]): 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""" """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: with open(password_file) as f:
password = f.read().strip() password = f.read().strip()
elif not password: elif "AITBC_WALLET_PASSWORD" in os.environ:
# Check if we're in a TTY environment # Environment variable is set (even if empty)
if not sys.stdin.isatty(): password = os.environ["AITBC_WALLET_PASSWORD"]
# Non-interactive: try environment variable else:
password = os.environ.get("AITBC_WALLET_PASSWORD") # Check if wallet is unencrypted
if not password: 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.") error("No TTY available for password prompt. Use --password or --password-file, or set AITBC_WALLET_PASSWORD environment variable.")
raise click.Abort() raise click.Abort()
else: else:
# Interactive: prompt for password import getpass
import getpass try:
try: password = getpass.getpass("Enter wallet password: ")
password = getpass.getpass("Enter wallet password: ") except Exception as e:
except Exception as e: error(f"Password prompt failed: {e}")
error(f"Password prompt failed: {e}") raise click.Abort()
raise click.Abort()
if not rpc_url: if not rpc_url:
rpc_url = DEFAULT_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') @click.option('--rpc-url', help='Blockchain RPC URL')
def batch(transactions_file: str, password: Optional[str], password_file: Optional[str], rpc_url: Optional[str]): def batch(transactions_file: str, password: Optional[str], password_file: Optional[str], rpc_url: Optional[str]):
"""Send batch transactions""" """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: with open(password_file) as f:
password = f.read().strip() password = f.read().strip()
elif not password: elif "AITBC_WALLET_PASSWORD" in os.environ:
# Check if we're in a TTY environment # Environment variable is set (even if empty)
if not sys.stdin.isatty(): password = os.environ["AITBC_WALLET_PASSWORD"]
# Non-interactive: try environment variable else:
password = os.environ.get("AITBC_WALLET_PASSWORD") # Check if first wallet is unencrypted
if not password: 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.") error("No TTY available for password prompt. Use --password or --password-file, or set AITBC_WALLET_PASSWORD environment variable.")
raise click.Abort() raise click.Abort()
else: else:
# Interactive: prompt for password import getpass
import getpass try:
try: password = getpass.getpass("Enter wallet password: ")
password = getpass.getpass("Enter wallet password: ") except Exception as e:
except Exception as e: error(f"Password prompt failed: {e}")
error(f"Password prompt failed: {e}") raise click.Abort()
raise click.Abort()
if not rpc_url: if not rpc_url:
rpc_url = DEFAULT_RPC_URL rpc_url = DEFAULT_RPC_URL