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();
}
}