Files
aitbc/cli/aitbc_cli/commands/simulate.py
oib 421191ccaf feat: add transaction hash search to blockchain explorer and cleanup settlement storage
Blockchain Explorer:
- Add transaction hash search support (64-char hex pattern validation)
- Fetch and display transaction details in modal (hash, type, from/to, amount, fee, block)
- Fix regex escape sequence in block height validation
- Update search placeholder text to mention both search types
- Add blank lines between function definitions for PEP 8 compliance

Settlement Storage:
- Add timedelta import for future
2026-02-17 14:34:12 +01:00

477 lines
14 KiB
Python

"""Simulation commands for AITBC CLI"""
import click
import json
import time
import random
from pathlib import Path
from typing import Optional, List, Dict, Any
from ..utils import output, error, success
@click.group()
def simulate():
"""Run simulations and manage test users"""
pass
@simulate.command()
@click.option(
"--distribute",
default="10000,1000",
help="Initial distribution: client_amount,miner_amount",
)
@click.option("--reset", is_flag=True, help="Reset existing simulation")
@click.pass_context
def init(ctx, distribute: str, reset: bool):
"""Initialize test economy"""
home_dir = Path("/home/oib/windsurf/aitbc/home")
if reset:
success("Resetting simulation...")
# Reset wallet files
for wallet_file in ["client_wallet.json", "miner_wallet.json"]:
wallet_path = home_dir / wallet_file
if wallet_path.exists():
wallet_path.unlink()
# Parse distribution
try:
client_amount, miner_amount = map(float, distribute.split(","))
except (ValueError, TypeError):
error("Invalid distribution format. Use: client_amount,miner_amount")
return
# Initialize genesis wallet
genesis_path = home_dir / "genesis_wallet.json"
if not genesis_path.exists():
genesis_wallet = {
"address": "aitbc1genesis",
"balance": 1000000,
"transactions": [],
}
with open(genesis_path, "w") as f:
json.dump(genesis_wallet, f, indent=2)
success("Genesis wallet created")
# Initialize client wallet
client_path = home_dir / "client_wallet.json"
if not client_path.exists():
client_wallet = {
"address": "aitbc1client",
"balance": client_amount,
"transactions": [
{
"type": "receive",
"amount": client_amount,
"from": "aitbc1genesis",
"timestamp": time.time(),
}
],
}
with open(client_path, "w") as f:
json.dump(client_wallet, f, indent=2)
success(f"Client wallet initialized with {client_amount} AITBC")
# Initialize miner wallet
miner_path = home_dir / "miner_wallet.json"
if not miner_path.exists():
miner_wallet = {
"address": "aitbc1miner",
"balance": miner_amount,
"transactions": [
{
"type": "receive",
"amount": miner_amount,
"from": "aitbc1genesis",
"timestamp": time.time(),
}
],
}
with open(miner_path, "w") as f:
json.dump(miner_wallet, f, indent=2)
success(f"Miner wallet initialized with {miner_amount} AITBC")
output(
{
"status": "initialized",
"distribution": {"client": client_amount, "miner": miner_amount},
"total_supply": client_amount + miner_amount,
},
ctx.obj["output_format"],
)
@simulate.group()
def user():
"""Manage test users"""
pass
@user.command()
@click.option("--type", type=click.Choice(["client", "miner"]), required=True)
@click.option("--name", required=True, help="User name")
@click.option("--balance", type=float, default=100, help="Initial balance")
@click.pass_context
def create(ctx, type: str, name: str, balance: float):
"""Create a test user"""
home_dir = Path("/home/oib/windsurf/aitbc/home")
user_id = f"{type}_{name}"
wallet_path = home_dir / f"{user_id}_wallet.json"
if wallet_path.exists():
error(f"User {name} already exists")
return
wallet = {
"address": f"aitbc1{user_id}",
"balance": balance,
"transactions": [
{
"type": "receive",
"amount": balance,
"from": "aitbc1genesis",
"timestamp": time.time(),
}
],
}
with open(wallet_path, "w") as f:
json.dump(wallet, f, indent=2)
success(f"Created {type} user: {name}")
output(
{"user_id": user_id, "address": wallet["address"], "balance": balance},
ctx.obj["output_format"],
)
@user.command()
@click.pass_context
def list(ctx):
"""List all test users"""
home_dir = Path("/home/oib/windsurf/aitbc/home")
users = []
for wallet_file in home_dir.glob("*_wallet.json"):
if wallet_file.name in ["genesis_wallet.json"]:
continue
with open(wallet_file) as f:
wallet = json.load(f)
user_type = "client" if "client" in wallet_file.name else "miner"
user_name = wallet_file.stem.replace("_wallet", "").replace(f"{user_type}_", "")
users.append(
{
"name": user_name,
"type": user_type,
"address": wallet["address"],
"balance": wallet["balance"],
}
)
output({"users": users}, ctx.obj["output_format"])
@user.command()
@click.argument("user")
@click.pass_context
def balance(ctx, user: str):
"""Check user balance"""
home_dir = Path("/home/oib/windsurf/aitbc/home")
wallet_path = home_dir / f"{user}_wallet.json"
if not wallet_path.exists():
error(f"User {user} not found")
return
with open(wallet_path) as f:
wallet = json.load(f)
output(
{"user": user, "address": wallet["address"], "balance": wallet["balance"]},
ctx.obj["output_format"],
)
@user.command()
@click.argument("user")
@click.argument("amount", type=float)
@click.pass_context
def fund(ctx, user: str, amount: float):
"""Fund a test user"""
home_dir = Path("/home/oib/windsurf/aitbc/home")
# Load genesis wallet
genesis_path = home_dir / "genesis_wallet.json"
with open(genesis_path) as f:
genesis = json.load(f)
if genesis["balance"] < amount:
error(f"Insufficient genesis balance: {genesis['balance']}")
return
# Load user wallet
wallet_path = home_dir / f"{user}_wallet.json"
if not wallet_path.exists():
error(f"User {user} not found")
return
with open(wallet_path) as f:
wallet = json.load(f)
# Transfer funds
genesis["balance"] -= amount
genesis["transactions"].append(
{
"type": "send",
"amount": -amount,
"to": wallet["address"],
"timestamp": time.time(),
}
)
wallet["balance"] += amount
wallet["transactions"].append(
{
"type": "receive",
"amount": amount,
"from": genesis["address"],
"timestamp": time.time(),
}
)
# Save wallets
with open(genesis_path, "w") as f:
json.dump(genesis, f, indent=2)
with open(wallet_path, "w") as f:
json.dump(wallet, f, indent=2)
success(f"Funded {user} with {amount} AITBC")
output(
{"user": user, "amount": amount, "new_balance": wallet["balance"]},
ctx.obj["output_format"],
)
@simulate.command()
@click.option("--jobs", type=int, default=5, help="Number of jobs to simulate")
@click.option("--rounds", type=int, default=3, help="Number of rounds")
@click.option(
"--delay", type=float, default=1.0, help="Delay between operations (seconds)"
)
@click.pass_context
def workflow(ctx, jobs: int, rounds: int, delay: float):
"""Simulate complete workflow"""
config = ctx.obj["config"]
success(f"Starting workflow simulation: {jobs} jobs x {rounds} rounds")
for round_num in range(1, rounds + 1):
click.echo(f"\n--- Round {round_num} ---")
# Submit jobs
submitted_jobs = []
for i in range(jobs):
prompt = f"Test job {i + 1} (round {round_num})"
# Simulate job submission
job_id = f"job_{round_num}_{i + 1}_{int(time.time())}"
submitted_jobs.append(job_id)
output(
{
"action": "submit_job",
"job_id": job_id,
"prompt": prompt,
"round": round_num,
},
ctx.obj["output_format"],
)
time.sleep(delay)
# Simulate job processing
for job_id in submitted_jobs:
# Simulate miner picking up job
output(
{
"action": "job_assigned",
"job_id": job_id,
"miner": f"miner_{random.randint(1, 3)}",
"status": "processing",
},
ctx.obj["output_format"],
)
time.sleep(delay * 0.5)
# Simulate job completion
earnings = random.uniform(1, 10)
output(
{
"action": "job_completed",
"job_id": job_id,
"earnings": earnings,
"status": "completed",
},
ctx.obj["output_format"],
)
time.sleep(delay * 0.5)
output(
{"status": "completed", "total_jobs": jobs * rounds, "rounds": rounds},
ctx.obj["output_format"],
)
@simulate.command()
@click.option("--clients", type=int, default=10, help="Number of clients")
@click.option("--miners", type=int, default=3, help="Number of miners")
@click.option("--duration", type=int, default=300, help="Test duration in seconds")
@click.option("--job-rate", type=float, default=1.0, help="Jobs per second")
@click.pass_context
def load_test(ctx, clients: int, miners: int, duration: int, job_rate: float):
"""Run load test"""
start_time = time.time()
end_time = start_time + duration
job_interval = 1.0 / job_rate
success(f"Starting load test: {clients} clients, {miners} miners, {duration}s")
stats = {
"jobs_submitted": 0,
"jobs_completed": 0,
"errors": 0,
"start_time": start_time,
}
while time.time() < end_time:
# Submit jobs
for client_id in range(clients):
if time.time() >= end_time:
break
job_id = f"load_test_{stats['jobs_submitted']}_{int(time.time())}"
stats["jobs_submitted"] += 1
# Simulate random job completion
if random.random() > 0.1: # 90% success rate
stats["jobs_completed"] += 1
else:
stats["errors"] += 1
time.sleep(job_interval)
# Show progress
elapsed = time.time() - start_time
if elapsed % 30 < 1: # Every 30 seconds
output(
{
"elapsed": elapsed,
"jobs_submitted": stats["jobs_submitted"],
"jobs_completed": stats["jobs_completed"],
"errors": stats["errors"],
"success_rate": stats["jobs_completed"]
/ max(1, stats["jobs_submitted"])
* 100,
},
ctx.obj["output_format"],
)
# Final stats
total_time = time.time() - start_time
output(
{
"status": "completed",
"duration": total_time,
"jobs_submitted": stats["jobs_submitted"],
"jobs_completed": stats["jobs_completed"],
"errors": stats["errors"],
"avg_jobs_per_second": stats["jobs_submitted"] / total_time,
"success_rate": stats["jobs_completed"]
/ max(1, stats["jobs_submitted"])
* 100,
},
ctx.obj["output_format"],
)
@simulate.command()
@click.option("--file", required=True, help="Scenario file path")
@click.pass_context
def scenario(ctx, file: str):
"""Run predefined scenario"""
scenario_path = Path(file)
if not scenario_path.exists():
error(f"Scenario file not found: {file}")
return
with open(scenario_path) as f:
scenario = json.load(f)
success(f"Running scenario: {scenario.get('name', 'Unknown')}")
# Execute scenario steps
for step in scenario.get("steps", []):
step_type = step.get("type")
step_name = step.get("name", "Unnamed step")
click.echo(f"\nExecuting: {step_name}")
if step_type == "submit_jobs":
count = step.get("count", 1)
for i in range(count):
output(
{
"action": "submit_job",
"step": step_name,
"job_num": i + 1,
"prompt": step.get("prompt", f"Scenario job {i + 1}"),
},
ctx.obj["output_format"],
)
elif step_type == "wait":
duration = step.get("duration", 1)
time.sleep(duration)
elif step_type == "check_balance":
user = step.get("user", "client")
# Would check actual balance
output({"action": "check_balance", "user": user}, ctx.obj["output_format"])
output(
{"status": "completed", "scenario": scenario.get("name", "Unknown")},
ctx.obj["output_format"],
)
@simulate.command()
@click.argument("simulation_id")
@click.pass_context
def results(ctx, simulation_id: str):
"""Show simulation results"""
# In a real implementation, this would query stored results
# For now, return mock data
output(
{
"simulation_id": simulation_id,
"status": "completed",
"start_time": time.time() - 3600,
"end_time": time.time(),
"duration": 3600,
"total_jobs": 50,
"successful_jobs": 48,
"failed_jobs": 2,
"success_rate": 96.0,
},
ctx.obj["output_format"],
)