base-simulacrum 0.1.0

A headless CLI tool for locally testing EIP-5792 batch transactions against a simulated Base environment
Documentation
//! Anvil process lifecycle management.
//!
//! This module provides functionality to spawn, manage, and terminate local Anvil
//! instances for testing purposes.

use std::process::{Child, Command, Stdio};
use std::time::Duration;
use thiserror::Error;
use tokio::time::sleep;

#[derive(Error, Debug)]
pub enum AnvilError {
    #[error("Failed to spawn anvil process: {0}")]
    SpawnFailed(String),
    #[error("Anvil process failed to start within timeout")]
    StartupTimeout,
    #[error("Failed to kill anvil process: {0}")]
    KillFailed(String),
}

pub struct AnvilInstance {
    process: Child,
    pub rpc_url: String,
    pub chain_id: u64,
}

impl AnvilInstance {
    pub async fn spawn(chain_id: u64, port: u16, fork_url: Option<String>) -> Result<Self, AnvilError> {
        let mut cmd = Command::new("anvil");
        
        cmd.arg("--port")
            .arg(port.to_string())
            .arg("--chain-id")
            .arg(chain_id.to_string())
            .arg("--block-time")
            .arg("1")
            .arg("--accounts")
            .arg("10")
            .arg("--balance")
            .arg("10000")
            .stdout(Stdio::null())
            .stderr(Stdio::null());

        if let Some(fork) = fork_url {
            cmd.arg("--fork-url").arg(fork);
        }

        let process = cmd
            .spawn()
            .map_err(|e| AnvilError::SpawnFailed(e.to_string()))?;

        let rpc_url = format!("http://127.0.0.1:{}", port);

        let instance = Self {
            process,
            rpc_url: rpc_url.clone(),
            chain_id,
        };

        instance.wait_for_ready().await?;

        println!("✓ Anvil spawned on {} (chain_id: {})", rpc_url, chain_id);

        Ok(instance)
    }

    async fn wait_for_ready(&self) -> Result<(), AnvilError> {
        let client = reqwest::Client::new();
        let max_attempts = 30;

        for _ in 0..max_attempts {
            let payload = serde_json::json!({
                "jsonrpc": "2.0",
                "method": "eth_chainId",
                "params": [],
                "id": 1
            });

            if let Ok(response) = client
                .post(&self.rpc_url)
                .json(&payload)
                .send()
                .await
            {
                if response.status().is_success() {
                    return Ok(());
                }
            }

            sleep(Duration::from_millis(100)).await;
        }

        Err(AnvilError::StartupTimeout)
    }

    pub fn kill(&mut self) -> Result<(), AnvilError> {
        self.process
            .kill()
            .map_err(|e| AnvilError::KillFailed(e.to_string()))?;
        
        self.process
            .wait()
            .map_err(|e| AnvilError::KillFailed(e.to_string()))?;

        println!("✓ Anvil process terminated");
        Ok(())
    }
}

impl Drop for AnvilInstance {
    fn drop(&mut self) {
        let _ = self.process.kill();
        let _ = self.process.wait();
    }
}