Files
aitbc/cli/commands/genesis.py
aitbc1 c0952c2525 refactor: flatten CLI directory structure - remove 'box in a box'
BEFORE:
/opt/aitbc/cli/
├── aitbc_cli/                    # Python package (box in a box)
│   ├── commands/
│   ├── main.py
│   └── ...
├── setup.py

AFTER:
/opt/aitbc/cli/                    # Flat structure
├── commands/                      # Direct access
├── main.py                        # Direct access
├── auth/
├── config/
├── core/
├── models/
├── utils/
├── plugins.py
└── setup.py

CHANGES MADE:
- Moved all files from aitbc_cli/ to cli/ root
- Fixed all relative imports (from . to absolute imports)
- Updated setup.py entry point: aitbc_cli.main → main
- Added CLI directory to Python path in entry script
- Simplified deployment.py to remove dependency on deleted core.deployment
- Fixed import paths in all command files
- Recreated virtual environment with new structure

BENEFITS:
- Eliminated 'box in a box' nesting
- Simpler directory structure
- Direct access to all modules
- Cleaner imports
- Easier maintenance and development
- CLI works with both 'python main.py' and 'aitbc' commands
2026-03-26 09:12:02 +01:00

476 lines
18 KiB
Python
Executable File

"""Genesis block generation commands for AITBC CLI"""
import click
import json
import yaml
from pathlib import Path
from datetime import datetime
from core.genesis_generator import GenesisGenerator, GenesisValidationError
from core.config import MultiChainConfig, load_multichain_config
from models.chain import GenesisConfig
from utils import output, error, success
from commands.keystore import create_keystore_via_script
import subprocess
import sys
@click.group()
def genesis():
"""Genesis block generation and management commands"""
pass
@genesis.command()
@click.option('--address', required=True, help='Wallet address (id) to create')
@click.option('--password-file', default='/opt/aitbc/data/keystore/.password', show_default=True, type=click.Path(exists=True, dir_okay=False), help='Path to password file')
@click.option('--output-dir', default='/opt/aitbc/data/keystore', show_default=True, help='Directory to write keystore file')
@click.option('--force', is_flag=True, help='Overwrite existing keystore file if present')
@click.pass_context
def create_keystore(ctx, address, password_file, output_dir, force):
"""Create an encrypted keystore for a genesis/treasury address."""
try:
create_keystore_via_script(address=address, password_file=password_file, output_dir=output_dir, force=force)
success(f"Created keystore for {address} at {output_dir}")
except Exception as e:
error(f"Error creating keystore: {e}")
raise click.Abort()
@genesis.command(name="init-production")
@click.option('--chain-id', default='ait-mainnet', show_default=True, help='Chain ID to initialize')
@click.option('--genesis-file', default='data/genesis_prod.yaml', show_default=True, help='Path to genesis YAML (copy to /opt/aitbc/genesis_prod.yaml if needed)')
@click.option('--db', default='/opt/aitbc/data/ait-mainnet/chain.db', show_default=True, help='SQLite DB path')
@click.option('--force', is_flag=True, help='Overwrite existing DB (removes file if present)')
@click.pass_context
def init_production(ctx, chain_id, genesis_file, db, force):
"""Initialize production chain DB using genesis allocations."""
db_path = Path(db)
if db_path.exists() and force:
db_path.unlink()
python_bin = Path(__file__).resolve().parents[3] / 'apps' / 'blockchain-node' / '.venv' / 'bin' / 'python3'
cmd = [
str(python_bin),
str(Path(__file__).resolve().parents[3] / 'scripts' / 'init_production_genesis.py'),
'--chain-id', chain_id,
'--db', db,
]
try:
subprocess.run(cmd, check=True)
success(f"Initialized production genesis for {chain_id} at {db}")
except subprocess.CalledProcessError as e:
error(f"Genesis init failed: {e}")
raise click.Abort()
@genesis.command()
@click.argument('config_file', type=click.Path(exists=True))
@click.option('--output', '-o', 'output_file', help='Output file path')
@click.option('--template', help='Use predefined template')
@click.option('--format', type=click.Choice(['json', 'yaml']), default='json', help='Output format')
@click.pass_context
def create(ctx, config_file, output_file, template, format):
"""Create genesis block from configuration"""
try:
config = load_multichain_config()
generator = GenesisGenerator(config)
if template:
# Create from template
genesis_block = generator.create_from_template(template, config_file)
else:
# Create from configuration file
with open(config_file, 'r') as f:
config_data = yaml.safe_load(f)
genesis_config = GenesisConfig(**config_data['genesis'])
genesis_block = generator.create_genesis(genesis_config)
# Determine output file
if output_file is None:
chain_id = genesis_block.chain_id
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_file = f"genesis_{chain_id}_{timestamp}.{format}"
# Save genesis block
output_path = Path(output_file)
output_path.parent.mkdir(parents=True, exist_ok=True)
if format == 'yaml':
with open(output_path, 'w') as f:
yaml.dump(genesis_block.dict(), f, default_flow_style=False, indent=2)
else:
with open(output_path, 'w') as f:
json.dump(genesis_block.dict(), f, indent=2)
success("Genesis block created successfully!")
result = {
"Chain ID": genesis_block.chain_id,
"Chain Type": genesis_block.chain_type.value,
"Purpose": genesis_block.purpose,
"Name": genesis_block.name,
"Genesis Hash": genesis_block.hash,
"Output File": output_file,
"Format": format
}
output(result, ctx.obj.get('output_format', 'table'))
if genesis_block.privacy.visibility == "private":
success("Private chain genesis created! Use access codes to invite participants.")
except GenesisValidationError as e:
error(f"Genesis validation error: {str(e)}")
raise click.Abort()
except Exception as e:
error(f"Error creating genesis block: {str(e)}")
raise click.Abort()
@genesis.command()
@click.argument('genesis_file', type=click.Path(exists=True))
@click.pass_context
def validate(ctx, genesis_file):
"""Validate genesis block integrity"""
try:
config = load_multichain_config()
generator = GenesisGenerator(config)
# Load genesis block
genesis_path = Path(genesis_file)
if genesis_path.suffix.lower() in ['.yaml', '.yml']:
with open(genesis_path, 'r') as f:
genesis_data = yaml.safe_load(f)
else:
with open(genesis_path, 'r') as f:
genesis_data = json.load(f)
from models.chain import GenesisBlock
genesis_block = GenesisBlock(**genesis_data)
# Validate genesis block
validation_result = generator.validate_genesis(genesis_block)
if validation_result.is_valid:
success("Genesis block is valid!")
# Show validation details
checks_data = [
{
"Check": check,
"Status": "✓ Pass" if passed else "✗ Fail"
}
for check, passed in validation_result.checks.items()
]
output(checks_data, ctx.obj.get('output_format', 'table'), title="Validation Results")
else:
error("Genesis block validation failed!")
# Show errors
errors_data = [
{
"Error": error_msg
}
for error_msg in validation_result.errors
]
output(errors_data, ctx.obj.get('output_format', 'table'), title="Validation Errors")
# Show failed checks
failed_checks = [
{
"Check": check,
"Status": "✗ Fail"
}
for check, passed in validation_result.checks.items()
if not passed
]
if failed_checks:
output(failed_checks, ctx.obj.get('output_format', 'table'), title="Failed Checks")
raise click.Abort()
except Exception as e:
error(f"Error validating genesis block: {str(e)}")
raise click.Abort()
@genesis.command()
@click.argument('genesis_file', type=click.Path(exists=True))
@click.pass_context
def info(ctx, genesis_file):
"""Show genesis block information"""
try:
config = load_multichain_config()
generator = GenesisGenerator(config)
genesis_info = generator.get_genesis_info(genesis_file)
# Basic information
basic_info = {
"Chain ID": genesis_info["chain_id"],
"Chain Type": genesis_info["chain_type"],
"Purpose": genesis_info["purpose"],
"Name": genesis_info["name"],
"Description": genesis_info.get("description", "No description"),
"Created": genesis_info["created"],
"Genesis Hash": genesis_info["genesis_hash"],
"State Root": genesis_info["state_root"]
}
output(basic_info, ctx.obj.get('output_format', 'table'), title="Genesis Block Information")
# Configuration details
config_info = {
"Consensus Algorithm": genesis_info["consensus_algorithm"],
"Block Time": f"{genesis_info['block_time']}s",
"Gas Limit": f"{genesis_info['gas_limit']:,}",
"Gas Price": f"{genesis_info['gas_price'] / 1e9:.1f} gwei",
"Accounts Count": genesis_info["accounts_count"],
"Contracts Count": genesis_info["contracts_count"]
}
output(config_info, ctx.obj.get('output_format', 'table'), title="Configuration Details")
# Privacy settings
privacy_info = {
"Visibility": genesis_info["privacy_visibility"],
"Access Control": genesis_info["access_control"]
}
output(privacy_info, ctx.obj.get('output_format', 'table'), title="Privacy Settings")
# File information
file_info = {
"File Size": f"{genesis_info['file_size']:,} bytes",
"File Format": genesis_info["file_format"]
}
output(file_info, ctx.obj.get('output_format', 'table'), title="File Information")
except Exception as e:
error(f"Error getting genesis info: {str(e)}")
raise click.Abort()
@genesis.command()
@click.argument('genesis_file', type=click.Path(exists=True))
@click.pass_context
def hash(ctx, genesis_file):
"""Calculate genesis hash"""
try:
config = load_multichain_config()
generator = GenesisGenerator(config)
genesis_hash = generator.calculate_genesis_hash(genesis_file)
result = {
"Genesis File": genesis_file,
"Genesis Hash": genesis_hash
}
output(result, ctx.obj.get('output_format', 'table'))
except Exception as e:
error(f"Error calculating genesis hash: {str(e)}")
raise click.Abort()
@genesis.command()
@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format')
@click.pass_context
def templates(ctx, format):
"""List available genesis templates"""
try:
config = load_multichain_config()
generator = GenesisGenerator(config)
templates = generator.list_templates()
if not templates:
output("No templates found", ctx.obj.get('output_format', 'table'))
return
if format == 'json':
output(templates, ctx.obj.get('output_format', 'table'))
else:
templates_data = [
{
"Template": template_name,
"Description": template_info["description"],
"Chain Type": template_info["chain_type"],
"Purpose": template_info["purpose"]
}
for template_name, template_info in templates.items()
]
output(templates_data, ctx.obj.get('output_format', 'table'), title="Available Templates")
except Exception as e:
error(f"Error listing templates: {str(e)}")
raise click.Abort()
@genesis.command()
@click.argument('template_name')
@click.option('--output', '-o', help='Output file path')
@click.pass_context
def template_info(ctx, template_name, output):
"""Show detailed information about a template"""
try:
config = load_multichain_config()
generator = GenesisGenerator(config)
templates = generator.list_templates()
if template_name not in templates:
error(f"Template {template_name} not found")
raise click.Abort()
template_info = templates[template_name]
info_data = {
"Template Name": template_name,
"Description": template_info["description"],
"Chain Type": template_info["chain_type"],
"Purpose": template_info["purpose"],
"File Path": template_info["file_path"]
}
output(info_data, ctx.obj.get('output_format', 'table'), title=f"Template Information: {template_name}")
# Show template content if requested
if output:
template_path = Path(template_info["file_path"])
if template_path.exists():
with open(template_path, 'r') as f:
template_content = f.read()
output_path = Path(output)
output_path.write_text(template_content)
success(f"Template content saved to {output}")
except Exception as e:
error(f"Error getting template info: {str(e)}")
raise click.Abort()
@genesis.command(name="init-production")
@click.option('--chain-id', default='ait-mainnet', show_default=True, help='Chain ID to initialize')
@click.option('--genesis-file', default='data/genesis_prod.yaml', show_default=True, help='Path to genesis YAML (copy to /opt/aitbc/genesis_prod.yaml if needed)')
@click.option('--force', is_flag=True, help='Overwrite existing DB (removes file if present)')
@click.pass_context
def init_production(ctx, chain_id, genesis_file, force):
"""Initialize production chain DB using genesis allocations."""
db_path = Path("/opt/aitbc/data") / chain_id / "chain.db"
if db_path.exists() and force:
db_path.unlink()
python_bin = Path(__file__).resolve().parents[3] / 'apps' / 'blockchain-node' / '.venv' / 'bin' / 'python3'
cmd = [
str(python_bin),
str(Path(__file__).resolve().parents[3] / 'scripts' / 'init_production_genesis.py'),
'--chain-id', chain_id,
]
try:
subprocess.run(cmd, check=True)
success(f"Initialized production genesis for {chain_id} at {db_path}")
except subprocess.CalledProcessError as e:
error(f"Genesis init failed: {e}")
raise click.Abort()
@genesis.command()
@click.argument('chain_id')
@click.option('--format', type=click.Choice(['json', 'yaml']), default='json', help='Export format')
@click.option('--output', '-o', help='Output file path')
@click.pass_context
def export(ctx, chain_id, format, output):
"""Export genesis block for a chain"""
try:
config = load_multichain_config()
generator = GenesisGenerator(config)
genesis_data = generator.export_genesis(chain_id, format)
if output:
output_path = Path(output)
output_path.parent.mkdir(parents=True, exist_ok=True)
if format == 'yaml':
# Parse JSON and convert to YAML
parsed_data = json.loads(genesis_data)
with open(output_path, 'w') as f:
yaml.dump(parsed_data, f, default_flow_style=False, indent=2)
else:
output_path.write_text(genesis_data)
success(f"Genesis block exported to {output}")
else:
# Print to stdout
if format == 'yaml':
parsed_data = json.loads(genesis_data)
output(yaml.dump(parsed_data, default_flow_style=False, indent=2),
ctx.obj.get('output_format', 'table'))
else:
output(genesis_data, ctx.obj.get('output_format', 'table'))
except Exception as e:
error(f"Error exporting genesis block: {str(e)}")
raise click.Abort()
@genesis.command()
@click.argument('template_name')
@click.argument('output_file')
@click.option('--format', type=click.Choice(['json', 'yaml']), default='yaml', help='Output format')
@click.pass_context
def create_template(ctx, template_name, output_file, format):
"""Create a new genesis template"""
try:
# Basic template structure
template_data = {
"description": f"Genesis template for {template_name}",
"genesis": {
"chain_type": "topic",
"purpose": template_name,
"name": f"{template_name.title()} Chain",
"description": f"A {template_name} chain for AITBC",
"consensus": {
"algorithm": "pos",
"block_time": 5,
"max_validators": 100,
"authorities": []
},
"privacy": {
"visibility": "public",
"access_control": "open",
"require_invitation": False
},
"parameters": {
"max_block_size": 1048576,
"max_gas_per_block": 10000000,
"min_gas_price": 1000000000,
"block_reward": "2000000000000000000"
},
"accounts": [],
"contracts": []
}
}
output_path = Path(output_file)
output_path.parent.mkdir(parents=True, exist_ok=True)
if format == 'yaml':
with open(output_path, 'w') as f:
yaml.dump(template_data, f, default_flow_style=False, indent=2)
else:
with open(output_path, 'w') as f:
json.dump(template_data, f, indent=2)
success(f"Template created: {output_file}")
result = {
"Template Name": template_name,
"Output File": output_file,
"Format": format,
"Chain Type": template_data["genesis"]["chain_type"],
"Purpose": template_data["genesis"]["purpose"]
}
output(result, ctx.obj.get('output_format', 'table'))
except Exception as e:
error(f"Error creating template: {str(e)}")
raise click.Abort()