Skip to main content

base_simulacrum/
anvil.rs

1//! Anvil process lifecycle management.
2//!
3//! This module provides functionality to spawn, manage, and terminate local Anvil
4//! instances for testing purposes.
5
6use std::process::{Child, Command, Stdio};
7use std::time::Duration;
8use thiserror::Error;
9use tokio::time::sleep;
10
11#[derive(Error, Debug)]
12pub enum AnvilError {
13    #[error("Failed to spawn anvil process: {0}")]
14    SpawnFailed(String),
15    #[error("Anvil process failed to start within timeout")]
16    StartupTimeout,
17    #[error("Failed to kill anvil process: {0}")]
18    KillFailed(String),
19}
20
21pub struct AnvilInstance {
22    process: Child,
23    pub rpc_url: String,
24    pub chain_id: u64,
25}
26
27impl AnvilInstance {
28    pub async fn spawn(chain_id: u64, port: u16, fork_url: Option<String>) -> Result<Self, AnvilError> {
29        let mut cmd = Command::new("anvil");
30        
31        cmd.arg("--port")
32            .arg(port.to_string())
33            .arg("--chain-id")
34            .arg(chain_id.to_string())
35            .arg("--block-time")
36            .arg("1")
37            .arg("--accounts")
38            .arg("10")
39            .arg("--balance")
40            .arg("10000")
41            .stdout(Stdio::null())
42            .stderr(Stdio::null());
43
44        if let Some(fork) = fork_url {
45            cmd.arg("--fork-url").arg(fork);
46        }
47
48        let process = cmd
49            .spawn()
50            .map_err(|e| AnvilError::SpawnFailed(e.to_string()))?;
51
52        let rpc_url = format!("http://127.0.0.1:{}", port);
53
54        let instance = Self {
55            process,
56            rpc_url: rpc_url.clone(),
57            chain_id,
58        };
59
60        instance.wait_for_ready().await?;
61
62        println!("✓ Anvil spawned on {} (chain_id: {})", rpc_url, chain_id);
63
64        Ok(instance)
65    }
66
67    async fn wait_for_ready(&self) -> Result<(), AnvilError> {
68        let client = reqwest::Client::new();
69        let max_attempts = 30;
70
71        for _ in 0..max_attempts {
72            let payload = serde_json::json!({
73                "jsonrpc": "2.0",
74                "method": "eth_chainId",
75                "params": [],
76                "id": 1
77            });
78
79            if let Ok(response) = client
80                .post(&self.rpc_url)
81                .json(&payload)
82                .send()
83                .await
84            {
85                if response.status().is_success() {
86                    return Ok(());
87                }
88            }
89
90            sleep(Duration::from_millis(100)).await;
91        }
92
93        Err(AnvilError::StartupTimeout)
94    }
95
96    pub fn kill(&mut self) -> Result<(), AnvilError> {
97        self.process
98            .kill()
99            .map_err(|e| AnvilError::KillFailed(e.to_string()))?;
100        
101        self.process
102            .wait()
103            .map_err(|e| AnvilError::KillFailed(e.to_string()))?;
104
105        println!("✓ Anvil process terminated");
106        Ok(())
107    }
108}
109
110impl Drop for AnvilInstance {
111    fn drop(&mut self) {
112        let _ = self.process.kill();
113        let _ = self.process.wait();
114    }
115}