base-simulacrum 0.1.0

A headless CLI tool for locally testing EIP-5792 batch transactions against a simulated Base environment
Documentation
//! Base Simulacrum CLI - Local EIP-5792 batch transaction tester.

use base_simulacrum::{AnvilInstance, Eip5792Engine, SendCallsParams};
use clap::{Parser, Subcommand};
use std::path::PathBuf;

#[derive(Parser)]
#[command(name = "base-simulacrum")]
#[command(about = "Local EIP-5792 batch transaction tester for Base", long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Execute a batch of calls from a JSON file
    Run {
        /// Path to the JSON file containing SendCallsParams
        #[arg(short, long)]
        calls: PathBuf,

        /// Enable gas sponsorship simulation
        #[arg(short, long, default_value = "false")]
        sponsor: bool,

        /// Chain ID for the local network
        #[arg(long, default_value = "8453")]
        chain_id: u64,

        /// Port for the local Anvil instance
        #[arg(short, long, default_value = "8545")]
        port: u16,

        /// Optional fork URL (e.g., Base mainnet RPC)
        #[arg(short, long)]
        fork: Option<String>,
    },

    /// Check the status of a batch execution
    Status {
        /// Batch ID returned from a previous run
        #[arg(short, long)]
        batch_id: String,

        /// RPC URL of the running Anvil instance
        #[arg(short, long, default_value = "http://127.0.0.1:8545")]
        rpc: String,
    },
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let cli = Cli::parse();

    match cli.command {
        Commands::Run {
            calls,
            sponsor,
            chain_id,
            port,
            fork,
        } => {
            run_batch(calls, sponsor, chain_id, port, fork).await?;
        }
        Commands::Status { batch_id, rpc } => {
            check_status(batch_id, rpc).await?;
        }
    }

    Ok(())
}

async fn run_batch(
    calls_path: PathBuf,
    sponsor: bool,
    chain_id: u64,
    port: u16,
    fork: Option<String>,
) -> Result<(), Box<dyn std::error::Error>> {
    println!("🚀 Base Simulacrum: Local EIP-5792 Batch Tester");
    println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");

    let params: SendCallsParams = {
        let content = tokio::fs::read_to_string(&calls_path).await?;
        serde_json::from_str(&content)?
    };

    println!("📄 Loaded batch configuration:");
    println!("   Version: {}", params.version);
    println!("   Chain ID: {}", params.chain_id);
    println!("   From: {}", params.from);
    println!("   Calls: {}", params.calls.len());
    println!();

    let mut anvil = AnvilInstance::spawn(chain_id, port, fork).await?;
    println!();

    let engine = Eip5792Engine::new(anvil.rpc_url.clone(), sponsor);

    println!("  Executing batch transaction...");
    println!();

    let batch_id = match engine.wallet_send_calls(params).await {
        Ok(id) => {
            println!();
            println!("✓ Batch execution completed successfully");
            println!("  Batch ID: {}", id);
            id
        }
        Err(e) => {
            eprintln!();
            eprintln!("✗ Batch execution failed: {}", e);
            anvil.kill()?;
            return Err(e.into());
        }
    };

    println!();
    println!(" Fetching execution status...");

    let status = engine.wallet_get_calls_status(&batch_id).await?;

    println!("   Status: {:?}", status.status);
    println!("   Receipts: {}", status.receipts.len());

    for (idx, receipt) in status.receipts.iter().enumerate() {
        println!();
        println!("   Receipt {}:", idx + 1);
        println!("     Status: {}", receipt.status);
        println!("     Block: {}", receipt.block_number);
        println!("     Gas Used: {}", receipt.gas_used);
        println!("     Tx Hash: {}", receipt.transaction_hash);
    }

    println!();
    anvil.kill()?;

    println!();
    println!("✓ Test completed successfully");

    Ok(())
}

async fn check_status(batch_id: String, rpc: String) -> Result<(), Box<dyn std::error::Error>> {
    println!(" Checking batch status...");
    println!("   Batch ID: {}", batch_id);
    println!("   RPC: {}", rpc);
    println!();

    let engine = Eip5792Engine::new(rpc, false);
    let status = engine.wallet_get_calls_status(&batch_id).await?;

    println!("Status: {:?}", status.status);
    println!("Receipts: {}", status.receipts.len());

    for (idx, receipt) in status.receipts.iter().enumerate() {
        println!();
        println!("Receipt {}:", idx + 1);
        println!("  Status: {}", receipt.status);
        println!("  Block: {}", receipt.block_number);
        println!("  Gas Used: {}", receipt.gas_used);
        println!("  Tx Hash: {}", receipt.transaction_hash);
    }

    Ok(())
}