Merge gitea/main, preserving security fixes and current dependency versions

This commit is contained in:
AITBC System
2026-03-24 16:18:25 +01:00
252 changed files with 18525 additions and 1029 deletions

View File

@@ -917,18 +917,20 @@ def send(ctx, chain_id, from_addr, to, data, nonce):
config = ctx.obj['config']
try:
import httpx
import json
with httpx.Client() as client:
try:
payload_data = json.loads(data)
except json.JSONDecodeError:
payload_data = {"raw_data": data}
tx_payload = {
"type": "TRANSFER",
"chain_id": chain_id,
"from_address": from_addr,
"to_address": to,
"value": 0,
"gas_limit": 100000,
"gas_price": 1,
"sender": from_addr,
"nonce": nonce,
"data": data,
"signature": "mock_signature"
"fee": 0,
"payload": payload_data,
"sig": "mock_signature"
}
response = client.post(

View File

@@ -9,12 +9,57 @@ 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 .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')

View File

@@ -0,0 +1,67 @@
import click
import importlib.util
from pathlib import Path
def _load_keystore_script():
"""Dynamically load the top-level scripts/keystore.py module."""
root = Path(__file__).resolve().parents[3] # /opt/aitbc
ks_path = root / "scripts" / "keystore.py"
spec = importlib.util.spec_from_file_location("aitbc_scripts_keystore", ks_path)
if spec is None or spec.loader is None:
raise ImportError(f"Unable to load keystore script from {ks_path}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
@click.group()
def keystore():
"""Keystore operations (create wallets/keystores)."""
pass
@keystore.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",
default="/opt/aitbc/data/keystore",
show_default=True,
help="Directory to write keystore files",
)
@click.option(
"--force",
is_flag=True,
help="Overwrite existing keystore file if present",
)
@click.pass_context
def create(ctx, address: str, password_file: str, output: str, force: bool):
"""Create an encrypted keystore for the given address.
Examples:
aitbc keystore create --address aitbc1genesis
aitbc keystore create --address aitbc1treasury --password-file keystore/.password --output keystore
"""
pwd_path = Path(password_file)
with open(pwd_path, "r", encoding="utf-8") as f:
password = f.read().strip()
out_dir = Path(output) if output else Path("/opt/aitbc/data/keystore")
out_dir.mkdir(parents=True, exist_ok=True)
ks_module = _load_keystore_script()
ks_module.create_keystore(address=address, password=password, keystore_dir=out_dir, force=force)
click.echo(f"Created keystore for {address} at {out_dir}")
# Helper so other commands (genesis) can reuse the same logic
def create_keystore_via_script(address: str, password_file: str = "/opt/aitbc/data/keystore/.password", output_dir: str = "/opt/aitbc/data/keystore", force: bool = False):
pwd = Path(password_file).read_text(encoding="utf-8").strip()
out_dir = Path(output_dir)
out_dir.mkdir(parents=True, exist_ok=True)
ks_module = _load_keystore_script()
ks_module.create_keystore(address=address, password=pwd, keystore_dir=out_dir, force=force)

View File

@@ -290,8 +290,11 @@ class DualModeWalletAdapter:
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"""
"""Send transaction using file-based storage and blockchain RPC"""
from .commands.wallet import _load_wallet, _save_wallet
import httpx
from .utils import error, success
from datetime import datetime
wallet_path = self.wallet_dir / f"{wallet_name}.json"
@@ -300,23 +303,66 @@ class DualModeWalletAdapter:
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}")
# Fetch current balance and nonce from blockchain
from_address = wallet_data.get("address")
if not from_address:
error("Wallet does not have an address configured")
raise Exception("Invalid wallet")
rpc_url = self.config.blockchain_rpc_url
try:
resp = httpx.get(f"{rpc_url}/rpc/getBalance/{from_address}?chain_id=ait-mainnet", timeout=5)
if resp.status_code == 200:
data = resp.json()
chain_balance = data.get("balance", 0)
nonce = data.get("nonce", 0)
else:
error(f"Failed to get balance from chain: {resp.text}")
raise Exception("Chain error")
except Exception as e:
error(f"Failed to connect to blockchain RPC: {e}")
raise
if chain_balance < amount:
error(f"Insufficient blockchain balance. Available: {chain_balance}, Required: {amount}")
raise Exception("Insufficient balance")
# Construct and send transaction
tx_payload = {
"type": "TRANSFER",
"sender": from_address,
"nonce": nonce,
"fee": 0,
"payload": {"to": to_address, "value": amount},
"sig": "mock_signature" # Replace with real signature when implemented
}
# Add transaction
try:
resp = httpx.post(f"{rpc_url}/rpc/sendTx", json=tx_payload, timeout=5)
if resp.status_code not in (200, 201):
error(f"Failed to submit transaction to chain: {resp.text}")
raise Exception("Chain submission failed")
tx_hash = resp.json().get("tx_hash")
except Exception as e:
error(f"Failed to send transaction to RPC: {e}")
raise
# Add transaction to local history
transaction = {
"type": "send",
"amount": -amount,
"to_address": to_address,
"description": description or "",
"timestamp": datetime.now().isoformat(),
"tx_hash": tx_hash,
"status": "pending"
}
if "transactions" not in wallet_data:
wallet_data["transactions"] = []
wallet_data["transactions"].append(transaction)
wallet_data["balance"] = balance - amount
wallet_data["balance"] = chain_balance - amount
# Save wallet - CRITICAL SECURITY FIX: Always use password if wallet is encrypted
save_password = password if wallet_data.get("encrypted") else None
@@ -325,14 +371,14 @@ class DualModeWalletAdapter:
raise Exception("Password required for encrypted wallet")
_save_wallet(wallet_path, wallet_data, save_password)
success(f"Sent {amount} AITBC to {to_address}")
success(f"Submitted transaction {tx_hash} to send {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"],
"tx_hash": tx_hash,
"timestamp": transaction["timestamp"]
}

View File

@@ -46,6 +46,7 @@ from .commands.marketplace_advanced import advanced # Re-enabled after fixing r
from .commands.swarm import swarm
from .commands.chain import chain
from .commands.genesis import genesis
from .commands.keystore import keystore
from .commands.test_cli import test
from .commands.node import node
from .commands.analytics import analytics
@@ -257,6 +258,7 @@ cli.add_command(ai_group)
cli.add_command(swarm)
cli.add_command(chain)
cli.add_command(genesis)
cli.add_command(keystore)
cli.add_command(test)
cli.add_command(node)
cli.add_command(analytics)

View File

@@ -365,3 +365,4 @@ def create_http_client_with_retry(
transport=RetryTransport(),
timeout=timeout
)
from .subprocess import run_subprocess

View File

@@ -0,0 +1,31 @@
import subprocess
import sys
from typing import List, Optional, Union, Any
from . import error, output
def run_subprocess(cmd: List[str], check: bool = True, capture_output: bool = True, shell: bool = False, **kwargs: Any) -> Optional[Union[str, subprocess.CompletedProcess]]:
"""Run a subprocess command safely with logging"""
try:
if shell:
# When shell=True, cmd should be a string
cmd_str = " ".join(cmd) if isinstance(cmd, list) else cmd
result = subprocess.run(cmd_str, shell=True, check=check, capture_output=capture_output, text=True, **kwargs)
else:
result = subprocess.run(cmd, check=check, capture_output=capture_output, text=True, **kwargs)
if capture_output:
return result.stdout.strip()
return result
except subprocess.CalledProcessError as e:
error(f"Command failed with exit code {e.returncode}")
if capture_output and getattr(e, 'stderr', None):
print(e.stderr, file=sys.stderr)
if check:
sys.exit(e.returncode)
return getattr(e, 'stdout', None) if capture_output else None
except Exception as e:
error(f"Failed to execute command: {e}")
if check:
sys.exit(1)
return None