alloy_node_bindings/nodes/
anvil.rs1use crate::NodeError;
4use alloy_hardforks::EthereumHardfork;
5use alloy_network::EthereumWallet;
6use alloy_primitives::{hex, Address, ChainId};
7use alloy_signer::Signer;
8use alloy_signer_local::LocalSigner;
9use k256::{ecdsa::SigningKey, SecretKey as K256SecretKey};
10use std::{
11 ffi::OsString,
12 io::{BufRead, BufReader},
13 net::SocketAddr,
14 path::PathBuf,
15 process::{Child, Command},
16 str::FromStr,
17 time::{Duration, Instant},
18};
19use url::Url;
20
21pub const DEFAULT_IPC_ENDPOINT: &str =
23 if cfg!(unix) { "/tmp/anvil.ipc" } else { r"\\.\pipe\anvil.ipc" };
24
25const ANVIL_STARTUP_TIMEOUT_MILLIS: u64 = 10_000;
27
28#[derive(Debug)]
32pub struct AnvilInstance {
33 child: Child,
34 private_keys: Vec<K256SecretKey>,
35 addresses: Vec<Address>,
36 wallet: Option<EthereumWallet>,
37 ipc_path: Option<String>,
38 port: u16,
39 chain_id: Option<ChainId>,
40}
41
42impl AnvilInstance {
43 pub const fn child(&self) -> &Child {
45 &self.child
46 }
47
48 pub const fn child_mut(&mut self) -> &mut Child {
50 &mut self.child
51 }
52
53 pub fn keys(&self) -> &[K256SecretKey] {
55 &self.private_keys
56 }
57
58 #[track_caller]
64 pub fn first_key(&self) -> &K256SecretKey {
65 self.private_keys.first().unwrap()
66 }
67
68 pub fn nth_key(&self, idx: usize) -> Option<&K256SecretKey> {
70 self.private_keys.get(idx)
71 }
72
73 pub fn addresses(&self) -> &[Address] {
75 &self.addresses
76 }
77
78 pub const fn port(&self) -> u16 {
80 self.port
81 }
82
83 pub fn chain_id(&self) -> ChainId {
85 const ANVIL_HARDHAT_CHAIN_ID: ChainId = 31_337;
86 self.chain_id.unwrap_or(ANVIL_HARDHAT_CHAIN_ID)
87 }
88
89 #[doc(alias = "http_endpoint")]
91 pub fn endpoint(&self) -> String {
92 format!("http://localhost:{}", self.port)
93 }
94
95 pub fn ws_endpoint(&self) -> String {
97 format!("ws://localhost:{}", self.port)
98 }
99
100 pub fn ipc_path(&self) -> &str {
102 self.ipc_path.as_deref().unwrap_or(DEFAULT_IPC_ENDPOINT)
103 }
104
105 #[doc(alias = "http_endpoint_url")]
107 pub fn endpoint_url(&self) -> Url {
108 Url::parse(&self.endpoint()).unwrap()
109 }
110
111 pub fn ws_endpoint_url(&self) -> Url {
113 Url::parse(&self.ws_endpoint()).unwrap()
114 }
115
116 pub fn wallet(&self) -> Option<EthereumWallet> {
118 self.wallet.clone()
119 }
120}
121
122impl Drop for AnvilInstance {
123 fn drop(&mut self) {
124 #[cfg(unix)]
125 {
126 if let Ok(out) =
129 Command::new("kill").arg("-SIGTERM").arg(self.child.id().to_string()).output()
130 {
131 if out.status.success() {
132 return;
133 }
134 }
135 }
136 if let Err(err) = self.child.kill() {
137 eprintln!("alloy-node-bindings: failed to kill anvil process: {}", err);
138 }
139 }
140}
141
142#[derive(Clone, Debug, Default)]
164#[must_use = "This Builder struct does nothing unless it is `spawn`ed"]
165pub struct Anvil {
166 program: Option<PathBuf>,
167 port: Option<u16>,
168 block_time: Option<f64>,
171 chain_id: Option<ChainId>,
172 mnemonic: Option<String>,
173 ipc_path: Option<String>,
174 fork: Option<String>,
175 fork_block_number: Option<u64>,
176 args: Vec<OsString>,
177 envs: Vec<(OsString, OsString)>,
178 timeout: Option<u64>,
179 keep_stdout: bool,
180}
181
182impl Anvil {
183 pub fn new() -> Self {
197 Self::default()
198 }
199
200 pub fn at(path: impl Into<PathBuf>) -> Self {
213 Self::new().path(path)
214 }
215
216 pub fn path<T: Into<PathBuf>>(mut self, path: T) -> Self {
221 self.program = Some(path.into());
222 self
223 }
224
225 pub fn port<T: Into<u16>>(mut self, port: T) -> Self {
227 self.port = Some(port.into());
228 self
229 }
230
231 pub fn ipc_path(mut self, path: impl Into<String>) -> Self {
233 self.ipc_path = Some(path.into());
234 self
235 }
236
237 pub const fn chain_id(mut self, chain_id: u64) -> Self {
241 self.chain_id = Some(chain_id);
242 self
243 }
244
245 pub fn mnemonic<T: Into<String>>(mut self, mnemonic: T) -> Self {
247 self.mnemonic = Some(mnemonic.into());
248 self
249 }
250
251 pub const fn block_time(mut self, block_time: u64) -> Self {
253 self.block_time = Some(block_time as f64);
254 self
255 }
256
257 pub const fn block_time_f64(mut self, block_time: f64) -> Self {
260 self.block_time = Some(block_time);
261 self
262 }
263
264 pub const fn fork_block_number(mut self, fork_block_number: u64) -> Self {
268 self.fork_block_number = Some(fork_block_number);
269 self
270 }
271
272 pub fn fork<T: Into<String>>(mut self, fork: T) -> Self {
277 self.fork = Some(fork.into());
278 self
279 }
280
281 pub fn hardfork(mut self, hardfork: EthereumHardfork) -> Self {
283 self = self.args(["--hardfork", hardfork.to_string().as_str()]);
284 self
285 }
286
287 pub fn paris(mut self) -> Self {
289 self = self.hardfork(EthereumHardfork::Paris);
290 self
291 }
292
293 pub fn cancun(mut self) -> Self {
295 self = self.hardfork(EthereumHardfork::Cancun);
296 self
297 }
298
299 pub fn shanghai(mut self) -> Self {
301 self = self.hardfork(EthereumHardfork::Shanghai);
302 self
303 }
304
305 pub fn prague(mut self) -> Self {
307 self = self.hardfork(EthereumHardfork::Prague);
308 self
309 }
310
311 pub fn odyssey(mut self) -> Self {
313 self = self.arg("--odyssey");
314 self
315 }
316
317 pub fn auto_impersonate(mut self) -> Self {
319 self = self.arg("--auto-impersonate");
320 self
321 }
322
323 pub fn push_arg<T: Into<OsString>>(&mut self, arg: T) {
325 self.args.push(arg.into());
326 }
327
328 pub fn extend_args<I, S>(&mut self, args: I)
330 where
331 I: IntoIterator<Item = S>,
332 S: Into<OsString>,
333 {
334 for arg in args {
335 self.push_arg(arg);
336 }
337 }
338
339 pub fn arg<T: Into<OsString>>(mut self, arg: T) -> Self {
341 self.args.push(arg.into());
342 self
343 }
344
345 pub fn args<I, S>(mut self, args: I) -> Self
347 where
348 I: IntoIterator<Item = S>,
349 S: Into<OsString>,
350 {
351 for arg in args {
352 self = self.arg(arg);
353 }
354 self
355 }
356
357 pub fn env<K, V>(mut self, key: K, value: V) -> Self
359 where
360 K: Into<OsString>,
361 V: Into<OsString>,
362 {
363 self.envs.push((key.into(), value.into()));
364 self
365 }
366
367 pub fn envs<I, K, V>(mut self, envs: I) -> Self
369 where
370 I: IntoIterator<Item = (K, V)>,
371 K: Into<OsString>,
372 V: Into<OsString>,
373 {
374 for (key, value) in envs {
375 self = self.env(key, value);
376 }
377 self
378 }
379
380 pub const fn timeout(mut self, timeout: u64) -> Self {
382 self.timeout = Some(timeout);
383 self
384 }
385
386 pub const fn keep_stdout(mut self) -> Self {
390 self.keep_stdout = true;
391 self
392 }
393
394 #[track_caller]
400 pub fn spawn(self) -> AnvilInstance {
401 self.try_spawn().unwrap()
402 }
403
404 pub fn try_spawn(self) -> Result<AnvilInstance, NodeError> {
406 let mut cmd = self.program.as_ref().map_or_else(|| Command::new("anvil"), Command::new);
407 cmd.stdout(std::process::Stdio::piped()).stderr(std::process::Stdio::inherit());
408
409 cmd.env("FOUNDRY_DISABLE_NIGHTLY_WARNING", "")
411 .env("NO_COLOR", "1");
413
414 cmd.envs(self.envs);
416
417 let mut port = self.port.unwrap_or_default();
418 cmd.arg("-p").arg(port.to_string());
419
420 if let Some(mnemonic) = self.mnemonic {
421 cmd.arg("-m").arg(mnemonic);
422 }
423
424 if let Some(chain_id) = self.chain_id {
425 cmd.arg("--chain-id").arg(chain_id.to_string());
426 }
427
428 if let Some(block_time) = self.block_time {
429 cmd.arg("-b").arg(block_time.to_string());
430 }
431
432 if let Some(fork) = self.fork {
433 cmd.arg("-f").arg(fork);
434 }
435
436 if let Some(fork_block_number) = self.fork_block_number {
437 cmd.arg("--fork-block-number").arg(fork_block_number.to_string());
438 }
439
440 if let Some(ipc_path) = &self.ipc_path {
441 cmd.arg("--ipc").arg(ipc_path);
442 }
443
444 cmd.args(self.args);
445
446 let mut child = cmd.spawn().map_err(NodeError::SpawnError)?;
447
448 let stdout = child.stdout.take().ok_or(NodeError::NoStdout)?;
449
450 let start = Instant::now();
451 let mut reader = BufReader::new(stdout);
452
453 let mut private_keys = Vec::new();
454 let mut addresses = Vec::new();
455 let mut is_private_key = false;
456 let mut chain_id = None;
457 let mut wallet = None;
458 loop {
459 if start + Duration::from_millis(self.timeout.unwrap_or(ANVIL_STARTUP_TIMEOUT_MILLIS))
460 <= Instant::now()
461 {
462 return Err(NodeError::Timeout);
463 }
464
465 let mut line = String::new();
466 reader.read_line(&mut line).map_err(NodeError::ReadLineError)?;
467 trace!(target: "alloy::node::anvil", line);
468 if let Some(addr) = line.strip_prefix("Listening on") {
469 if let Ok(addr) = SocketAddr::from_str(addr.trim()) {
472 port = addr.port();
473 }
474 break;
475 }
476
477 if line.starts_with("Private Keys") {
478 is_private_key = true;
479 }
480
481 if is_private_key && line.starts_with('(') {
482 let key_str =
483 line.split("0x").last().ok_or(NodeError::ParsePrivateKeyError)?.trim();
484 let key_hex = hex::decode(key_str).map_err(NodeError::FromHexError)?;
485 let key = K256SecretKey::from_bytes((&key_hex[..]).into())
486 .map_err(|_| NodeError::DeserializePrivateKeyError)?;
487 addresses.push(Address::from_public_key(SigningKey::from(&key).verifying_key()));
488 private_keys.push(key);
489 }
490
491 if let Some(start_chain_id) = line.find("Chain ID:") {
492 let rest = &line[start_chain_id + "Chain ID:".len()..];
493 if let Ok(chain) = rest.split_whitespace().next().unwrap_or("").parse::<u64>() {
494 chain_id = Some(chain);
495 };
496 }
497
498 if !private_keys.is_empty() {
499 let mut private_keys = private_keys.iter().map(|key| {
500 let mut signer = LocalSigner::from(key.clone());
501 signer.set_chain_id(chain_id);
502 signer
503 });
504 let mut w = EthereumWallet::new(private_keys.next().unwrap());
505 for pk in private_keys {
506 w.register_signer(pk);
507 }
508 wallet = Some(w);
509 }
510 }
511
512 if self.keep_stdout {
513 child.stdout = Some(reader.into_inner());
515 }
516
517 Ok(AnvilInstance {
518 child,
519 private_keys,
520 addresses,
521 wallet,
522 ipc_path: self.ipc_path,
523 port,
524 chain_id: self.chain_id.or(chain_id),
525 })
526 }
527}
528
529#[cfg(test)]
530mod test {
531 use super::*;
532
533 #[test]
534 fn assert_block_time_is_natural_number() {
535 let anvil = Anvil::new().block_time(12);
538 assert_eq!(anvil.block_time.unwrap().to_string(), "12");
539 }
540
541 #[test]
542 fn spawn_and_drop() {
543 let _ = Anvil::new().block_time(12).try_spawn().map(drop);
544 }
545}