use crate::{utils::GracefulShutdown, NodeError, NODE_STARTUP_TIMEOUT};
use alloy_hardforks::EthereumHardfork;
use alloy_network::EthereumWallet;
use alloy_primitives::{hex, Address, ChainId};
use alloy_signer::Signer;
use alloy_signer_local::LocalSigner;
use k256::{ecdsa::SigningKey, SecretKey as K256SecretKey};
use std::{
ffi::OsString,
io::{BufRead, BufReader},
net::SocketAddr,
path::PathBuf,
process::{Child, Command},
str::FromStr,
time::{Duration, Instant},
};
use url::Url;
pub const DEFAULT_IPC_ENDPOINT: &str =
if cfg!(unix) { "/tmp/anvil.ipc" } else { r"\\.\pipe\anvil.ipc" };
#[derive(Debug)]
pub struct AnvilInstance {
child: Child,
private_keys: Vec<K256SecretKey>,
addresses: Vec<Address>,
wallet: Option<EthereumWallet>,
ipc_path: Option<String>,
host: String,
port: u16,
chain_id: Option<ChainId>,
}
impl AnvilInstance {
pub const fn child(&self) -> &Child {
&self.child
}
pub const fn child_mut(&mut self) -> &mut Child {
&mut self.child
}
pub fn keys(&self) -> &[K256SecretKey] {
&self.private_keys
}
#[track_caller]
pub fn first_key(&self) -> &K256SecretKey {
self.private_keys.first().unwrap()
}
pub fn nth_key(&self, idx: usize) -> Option<&K256SecretKey> {
self.private_keys.get(idx)
}
pub fn addresses(&self) -> &[Address] {
&self.addresses
}
pub fn host(&self) -> &str {
&self.host
}
pub const fn port(&self) -> u16 {
self.port
}
pub fn chain_id(&self) -> ChainId {
const ANVIL_HARDHAT_CHAIN_ID: ChainId = 31_337;
self.chain_id.unwrap_or(ANVIL_HARDHAT_CHAIN_ID)
}
#[doc(alias = "http_endpoint")]
pub fn endpoint(&self) -> String {
format!("http://{}:{}", self.host, self.port)
}
pub fn ws_endpoint(&self) -> String {
format!("ws://{}:{}", self.host, self.port)
}
pub fn ipc_path(&self) -> &str {
self.ipc_path.as_deref().unwrap_or(DEFAULT_IPC_ENDPOINT)
}
#[doc(alias = "http_endpoint_url")]
pub fn endpoint_url(&self) -> Url {
Url::parse(&self.endpoint()).unwrap()
}
pub fn ws_endpoint_url(&self) -> Url {
Url::parse(&self.ws_endpoint()).unwrap()
}
pub fn wallet(&self) -> Option<EthereumWallet> {
self.wallet.clone()
}
}
impl Drop for AnvilInstance {
fn drop(&mut self) {
GracefulShutdown::shutdown(&mut self.child, 10, "anvil");
}
}
#[derive(Clone, Debug, Default)]
#[must_use = "This Builder struct does nothing unless it is `spawn`ed"]
pub struct Anvil {
program: Option<PathBuf>,
host: Option<String>,
port: Option<u16>,
block_time: Option<f64>,
chain_id: Option<ChainId>,
mnemonic: Option<String>,
ipc_path: Option<String>,
fork: Option<String>,
fork_block_number: Option<u64>,
args: Vec<OsString>,
envs: Vec<(OsString, OsString)>,
timeout: Option<u64>,
keep_stdout: bool,
}
impl Anvil {
pub fn new() -> Self {
Self::default()
}
pub fn at(path: impl Into<PathBuf>) -> Self {
Self::new().path(path)
}
pub fn path<T: Into<PathBuf>>(mut self, path: T) -> Self {
self.program = Some(path.into());
self
}
pub fn host<T: Into<String>>(mut self, host: T) -> Self {
self.host = Some(host.into());
self
}
pub fn port<T: Into<u16>>(mut self, port: T) -> Self {
self.port = Some(port.into());
self
}
pub fn ipc_path(mut self, path: impl Into<String>) -> Self {
self.ipc_path = Some(path.into());
self
}
pub const fn chain_id(mut self, chain_id: u64) -> Self {
self.chain_id = Some(chain_id);
self
}
pub fn mnemonic<T: Into<String>>(mut self, mnemonic: T) -> Self {
self.mnemonic = Some(mnemonic.into());
self
}
pub const fn block_time(mut self, block_time: u64) -> Self {
self.block_time = Some(block_time as f64);
self
}
pub const fn block_time_f64(mut self, block_time: f64) -> Self {
self.block_time = Some(block_time);
self
}
pub const fn fork_block_number(mut self, fork_block_number: u64) -> Self {
self.fork_block_number = Some(fork_block_number);
self
}
pub fn fork<T: Into<String>>(mut self, fork: T) -> Self {
self.fork = Some(fork.into());
self
}
pub fn hardfork(mut self, hardfork: EthereumHardfork) -> Self {
self = self.args(["--hardfork", hardfork.to_string().as_str()]);
self
}
pub fn paris(mut self) -> Self {
self = self.hardfork(EthereumHardfork::Paris);
self
}
pub fn cancun(mut self) -> Self {
self = self.hardfork(EthereumHardfork::Cancun);
self
}
pub fn shanghai(mut self) -> Self {
self = self.hardfork(EthereumHardfork::Shanghai);
self
}
pub fn prague(mut self) -> Self {
self = self.hardfork(EthereumHardfork::Prague);
self
}
pub fn odyssey(mut self) -> Self {
self = self.arg("--odyssey");
self
}
pub fn auto_impersonate(mut self) -> Self {
self = self.arg("--auto-impersonate");
self
}
pub fn push_arg<T: Into<OsString>>(&mut self, arg: T) {
self.args.push(arg.into());
}
pub fn extend_args<I, S>(&mut self, args: I)
where
I: IntoIterator<Item = S>,
S: Into<OsString>,
{
for arg in args {
self.push_arg(arg);
}
}
pub fn arg<T: Into<OsString>>(mut self, arg: T) -> Self {
self.args.push(arg.into());
self
}
pub fn args<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<OsString>,
{
for arg in args {
self = self.arg(arg);
}
self
}
pub fn env<K, V>(mut self, key: K, value: V) -> Self
where
K: Into<OsString>,
V: Into<OsString>,
{
self.envs.push((key.into(), value.into()));
self
}
pub fn envs<I, K, V>(mut self, envs: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<OsString>,
V: Into<OsString>,
{
for (key, value) in envs {
self = self.env(key, value);
}
self
}
pub const fn timeout(mut self, timeout: u64) -> Self {
self.timeout = Some(timeout);
self
}
pub const fn keep_stdout(mut self) -> Self {
self.keep_stdout = true;
self
}
#[track_caller]
pub fn spawn(self) -> AnvilInstance {
self.try_spawn().unwrap()
}
pub fn try_spawn(self) -> Result<AnvilInstance, NodeError> {
let mut cmd = self.program.as_ref().map_or_else(|| Command::new("anvil"), Command::new);
cmd.stdout(std::process::Stdio::piped()).stderr(std::process::Stdio::inherit());
cmd.env("FOUNDRY_DISABLE_NIGHTLY_WARNING", "")
.env("NO_COLOR", "1");
cmd.envs(self.envs);
if let Some(ref host) = self.host {
cmd.arg("--host").arg(host);
}
let mut port = self.port.unwrap_or_default();
cmd.arg("-p").arg(port.to_string());
if let Some(mnemonic) = self.mnemonic {
cmd.arg("-m").arg(mnemonic);
}
if let Some(chain_id) = self.chain_id {
cmd.arg("--chain-id").arg(chain_id.to_string());
}
if let Some(block_time) = self.block_time {
cmd.arg("-b").arg(block_time.to_string());
}
if let Some(fork) = self.fork {
cmd.arg("-f").arg(fork);
}
if let Some(fork_block_number) = self.fork_block_number {
cmd.arg("--fork-block-number").arg(fork_block_number.to_string());
}
if let Some(ipc_path) = &self.ipc_path {
cmd.arg("--ipc").arg(ipc_path);
}
cmd.args(self.args);
let mut child = cmd.spawn().map_err(NodeError::SpawnError)?;
let stdout = child.stdout.take().ok_or(NodeError::NoStdout)?;
let start = Instant::now();
let mut reader = BufReader::new(stdout);
let timeout = self.timeout.map(Duration::from_millis).unwrap_or(NODE_STARTUP_TIMEOUT);
let mut private_keys = Vec::new();
let mut addresses = Vec::new();
let mut is_private_key = false;
let mut chain_id = None;
let mut wallet = None;
loop {
if start + timeout <= Instant::now() {
let _ = child.kill();
return Err(NodeError::Timeout);
}
let mut line = String::new();
reader.read_line(&mut line).map_err(NodeError::ReadLineError)?;
trace!(target: "alloy::node::anvil", line);
if let Some(addr) = line.strip_prefix("Listening on") {
if let Ok(addr) = SocketAddr::from_str(addr.trim()) {
port = addr.port();
}
break;
}
if line.starts_with("Private Keys") {
is_private_key = true;
}
if is_private_key && line.starts_with('(') {
let key_str =
line.split("0x").last().ok_or(NodeError::ParsePrivateKeyError)?.trim();
let key_hex = hex::decode(key_str).map_err(NodeError::FromHexError)?;
let key = K256SecretKey::from_bytes((&key_hex[..]).into())
.map_err(|_| NodeError::DeserializePrivateKeyError)?;
addresses.push(Address::from_public_key(SigningKey::from(&key).verifying_key()));
private_keys.push(key);
}
if let Some(start_chain_id) = line.find("Chain ID:") {
let rest = &line[start_chain_id + "Chain ID:".len()..];
if let Ok(chain) = rest.split_whitespace().next().unwrap_or("").parse::<u64>() {
chain_id = Some(chain);
};
}
if !private_keys.is_empty() {
let mut private_keys = private_keys.iter().map(|key| {
let mut signer = LocalSigner::from(key.clone());
signer.set_chain_id(chain_id);
signer
});
let mut w = EthereumWallet::new(private_keys.next().unwrap());
for pk in private_keys {
w.register_signer(pk);
}
wallet = Some(w);
}
}
if self.keep_stdout {
child.stdout = Some(reader.into_inner());
}
Ok(AnvilInstance {
child,
private_keys,
addresses,
wallet,
ipc_path: self.ipc_path,
host: self.host.unwrap_or_else(|| "localhost".to_string()),
port,
chain_id: self.chain_id.or(chain_id),
})
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn assert_block_time_is_natural_number() {
let anvil = Anvil::new().block_time(12);
assert_eq!(anvil.block_time.unwrap().to_string(), "12");
}
#[test]
fn spawn_and_drop() {
let _ = Anvil::new().block_time(12).try_spawn().map(drop);
}
#[test]
fn can_set_host() {
let anvil = Anvil::new().host("0.0.0.0").block_time(12).try_spawn();
if let Ok(anvil) = anvil {
assert_eq!(anvil.host(), "0.0.0.0");
assert!(anvil.endpoint().starts_with("http://0.0.0.0:"));
assert!(anvil.ws_endpoint().starts_with("ws://0.0.0.0:"));
}
}
#[test]
fn default_host_is_localhost() {
let anvil = Anvil::new().block_time(12).try_spawn();
if let Ok(anvil) = anvil {
assert_eq!(anvil.host(), "localhost");
assert!(anvil.endpoint().starts_with("http://localhost:"));
assert!(anvil.ws_endpoint().starts_with("ws://localhost:"));
}
}
}