alloy_node_bindings/nodes/
anvil.rs1use crate::{NodeError, NODE_STARTUP_TIMEOUT};
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
25#[derive(Debug)]
29pub struct AnvilInstance {
30 child: Child,
31 private_keys: Vec<K256SecretKey>,
32 addresses: Vec<Address>,
33 wallet: Option<EthereumWallet>,
34 ipc_path: Option<String>,
35 port: u16,
36 chain_id: Option<ChainId>,
37}
38
39impl AnvilInstance {
40 pub const fn child(&self) -> &Child {
42 &self.child
43 }
44
45 pub const fn child_mut(&mut self) -> &mut Child {
47 &mut self.child
48 }
49
50 pub fn keys(&self) -> &[K256SecretKey] {
52 &self.private_keys
53 }
54
55 #[track_caller]
61 pub fn first_key(&self) -> &K256SecretKey {
62 self.private_keys.first().unwrap()
63 }
64
65 pub fn nth_key(&self, idx: usize) -> Option<&K256SecretKey> {
67 self.private_keys.get(idx)
68 }
69
70 pub fn addresses(&self) -> &[Address] {
72 &self.addresses
73 }
74
75 pub const fn port(&self) -> u16 {
77 self.port
78 }
79
80 pub fn chain_id(&self) -> ChainId {
82 const ANVIL_HARDHAT_CHAIN_ID: ChainId = 31_337;
83 self.chain_id.unwrap_or(ANVIL_HARDHAT_CHAIN_ID)
84 }
85
86 #[doc(alias = "http_endpoint")]
88 pub fn endpoint(&self) -> String {
89 format!("http://localhost:{}", self.port)
90 }
91
92 pub fn ws_endpoint(&self) -> String {
94 format!("ws://localhost:{}", self.port)
95 }
96
97 pub fn ipc_path(&self) -> &str {
99 self.ipc_path.as_deref().unwrap_or(DEFAULT_IPC_ENDPOINT)
100 }
101
102 #[doc(alias = "http_endpoint_url")]
104 pub fn endpoint_url(&self) -> Url {
105 Url::parse(&self.endpoint()).unwrap()
106 }
107
108 pub fn ws_endpoint_url(&self) -> Url {
110 Url::parse(&self.ws_endpoint()).unwrap()
111 }
112
113 pub fn wallet(&self) -> Option<EthereumWallet> {
115 self.wallet.clone()
116 }
117}
118
119impl Drop for AnvilInstance {
120 fn drop(&mut self) {
121 #[cfg(unix)]
122 {
123 if let Ok(out) =
126 Command::new("kill").arg("-SIGTERM").arg(self.child.id().to_string()).output()
127 {
128 if out.status.success() {
129 return;
130 }
131 }
132 }
133 if let Err(err) = self.child.kill() {
134 eprintln!("alloy-node-bindings: failed to kill anvil process: {}", err);
135 }
136 }
137}
138
139#[derive(Clone, Debug, Default)]
161#[must_use = "This Builder struct does nothing unless it is `spawn`ed"]
162pub struct Anvil {
163 program: Option<PathBuf>,
164 port: Option<u16>,
165 block_time: Option<f64>,
168 chain_id: Option<ChainId>,
169 mnemonic: Option<String>,
170 ipc_path: Option<String>,
171 fork: Option<String>,
172 fork_block_number: Option<u64>,
173 args: Vec<OsString>,
174 envs: Vec<(OsString, OsString)>,
175 timeout: Option<u64>,
176 keep_stdout: bool,
177}
178
179impl Anvil {
180 pub fn new() -> Self {
194 Self::default()
195 }
196
197 pub fn at(path: impl Into<PathBuf>) -> Self {
210 Self::new().path(path)
211 }
212
213 pub fn path<T: Into<PathBuf>>(mut self, path: T) -> Self {
218 self.program = Some(path.into());
219 self
220 }
221
222 pub fn port<T: Into<u16>>(mut self, port: T) -> Self {
224 self.port = Some(port.into());
225 self
226 }
227
228 pub fn ipc_path(mut self, path: impl Into<String>) -> Self {
230 self.ipc_path = Some(path.into());
231 self
232 }
233
234 pub const fn chain_id(mut self, chain_id: u64) -> Self {
238 self.chain_id = Some(chain_id);
239 self
240 }
241
242 pub fn mnemonic<T: Into<String>>(mut self, mnemonic: T) -> Self {
244 self.mnemonic = Some(mnemonic.into());
245 self
246 }
247
248 pub const fn block_time(mut self, block_time: u64) -> Self {
250 self.block_time = Some(block_time as f64);
251 self
252 }
253
254 pub const fn block_time_f64(mut self, block_time: f64) -> Self {
257 self.block_time = Some(block_time);
258 self
259 }
260
261 pub const fn fork_block_number(mut self, fork_block_number: u64) -> Self {
265 self.fork_block_number = Some(fork_block_number);
266 self
267 }
268
269 pub fn fork<T: Into<String>>(mut self, fork: T) -> Self {
274 self.fork = Some(fork.into());
275 self
276 }
277
278 pub fn hardfork(mut self, hardfork: EthereumHardfork) -> Self {
280 self = self.args(["--hardfork", hardfork.to_string().as_str()]);
281 self
282 }
283
284 pub fn paris(mut self) -> Self {
286 self = self.hardfork(EthereumHardfork::Paris);
287 self
288 }
289
290 pub fn cancun(mut self) -> Self {
292 self = self.hardfork(EthereumHardfork::Cancun);
293 self
294 }
295
296 pub fn shanghai(mut self) -> Self {
298 self = self.hardfork(EthereumHardfork::Shanghai);
299 self
300 }
301
302 pub fn prague(mut self) -> Self {
304 self = self.hardfork(EthereumHardfork::Prague);
305 self
306 }
307
308 pub fn odyssey(mut self) -> Self {
310 self = self.arg("--odyssey");
311 self
312 }
313
314 pub fn auto_impersonate(mut self) -> Self {
316 self = self.arg("--auto-impersonate");
317 self
318 }
319
320 pub fn push_arg<T: Into<OsString>>(&mut self, arg: T) {
322 self.args.push(arg.into());
323 }
324
325 pub fn extend_args<I, S>(&mut self, args: I)
327 where
328 I: IntoIterator<Item = S>,
329 S: Into<OsString>,
330 {
331 for arg in args {
332 self.push_arg(arg);
333 }
334 }
335
336 pub fn arg<T: Into<OsString>>(mut self, arg: T) -> Self {
338 self.args.push(arg.into());
339 self
340 }
341
342 pub fn args<I, S>(mut self, args: I) -> Self
344 where
345 I: IntoIterator<Item = S>,
346 S: Into<OsString>,
347 {
348 for arg in args {
349 self = self.arg(arg);
350 }
351 self
352 }
353
354 pub fn env<K, V>(mut self, key: K, value: V) -> Self
356 where
357 K: Into<OsString>,
358 V: Into<OsString>,
359 {
360 self.envs.push((key.into(), value.into()));
361 self
362 }
363
364 pub fn envs<I, K, V>(mut self, envs: I) -> Self
366 where
367 I: IntoIterator<Item = (K, V)>,
368 K: Into<OsString>,
369 V: Into<OsString>,
370 {
371 for (key, value) in envs {
372 self = self.env(key, value);
373 }
374 self
375 }
376
377 pub const fn timeout(mut self, timeout: u64) -> Self {
380 self.timeout = Some(timeout);
381 self
382 }
383
384 pub const fn keep_stdout(mut self) -> Self {
388 self.keep_stdout = true;
389 self
390 }
391
392 #[track_caller]
398 pub fn spawn(self) -> AnvilInstance {
399 self.try_spawn().unwrap()
400 }
401
402 pub fn try_spawn(self) -> Result<AnvilInstance, NodeError> {
404 let mut cmd = self.program.as_ref().map_or_else(|| Command::new("anvil"), Command::new);
405 cmd.stdout(std::process::Stdio::piped()).stderr(std::process::Stdio::inherit());
406
407 cmd.env("FOUNDRY_DISABLE_NIGHTLY_WARNING", "")
409 .env("NO_COLOR", "1");
411
412 cmd.envs(self.envs);
414
415 let mut port = self.port.unwrap_or_default();
416 cmd.arg("-p").arg(port.to_string());
417
418 if let Some(mnemonic) = self.mnemonic {
419 cmd.arg("-m").arg(mnemonic);
420 }
421
422 if let Some(chain_id) = self.chain_id {
423 cmd.arg("--chain-id").arg(chain_id.to_string());
424 }
425
426 if let Some(block_time) = self.block_time {
427 cmd.arg("-b").arg(block_time.to_string());
428 }
429
430 if let Some(fork) = self.fork {
431 cmd.arg("-f").arg(fork);
432 }
433
434 if let Some(fork_block_number) = self.fork_block_number {
435 cmd.arg("--fork-block-number").arg(fork_block_number.to_string());
436 }
437
438 if let Some(ipc_path) = &self.ipc_path {
439 cmd.arg("--ipc").arg(ipc_path);
440 }
441
442 cmd.args(self.args);
443
444 let mut child = cmd.spawn().map_err(NodeError::SpawnError)?;
445
446 let stdout = child.stdout.take().ok_or(NodeError::NoStdout)?;
447
448 let start = Instant::now();
449 let mut reader = BufReader::new(stdout);
450 let timeout = self.timeout.map(Duration::from_millis).unwrap_or(NODE_STARTUP_TIMEOUT);
451
452 let mut private_keys = Vec::new();
453 let mut addresses = Vec::new();
454 let mut is_private_key = false;
455 let mut chain_id = None;
456 let mut wallet = None;
457 loop {
458 if start + timeout <= Instant::now() {
459 return Err(NodeError::Timeout);
460 }
461
462 let mut line = String::new();
463 reader.read_line(&mut line).map_err(NodeError::ReadLineError)?;
464 trace!(target: "alloy::node::anvil", line);
465 if let Some(addr) = line.strip_prefix("Listening on") {
466 if let Ok(addr) = SocketAddr::from_str(addr.trim()) {
469 port = addr.port();
470 }
471 break;
472 }
473
474 if line.starts_with("Private Keys") {
475 is_private_key = true;
476 }
477
478 if is_private_key && line.starts_with('(') {
479 let key_str =
480 line.split("0x").last().ok_or(NodeError::ParsePrivateKeyError)?.trim();
481 let key_hex = hex::decode(key_str).map_err(NodeError::FromHexError)?;
482 let key = K256SecretKey::from_bytes((&key_hex[..]).into())
483 .map_err(|_| NodeError::DeserializePrivateKeyError)?;
484 addresses.push(Address::from_public_key(SigningKey::from(&key).verifying_key()));
485 private_keys.push(key);
486 }
487
488 if let Some(start_chain_id) = line.find("Chain ID:") {
489 let rest = &line[start_chain_id + "Chain ID:".len()..];
490 if let Ok(chain) = rest.split_whitespace().next().unwrap_or("").parse::<u64>() {
491 chain_id = Some(chain);
492 };
493 }
494
495 if !private_keys.is_empty() {
496 let mut private_keys = private_keys.iter().map(|key| {
497 let mut signer = LocalSigner::from(key.clone());
498 signer.set_chain_id(chain_id);
499 signer
500 });
501 let mut w = EthereumWallet::new(private_keys.next().unwrap());
502 for pk in private_keys {
503 w.register_signer(pk);
504 }
505 wallet = Some(w);
506 }
507 }
508
509 if self.keep_stdout {
510 child.stdout = Some(reader.into_inner());
512 }
513
514 Ok(AnvilInstance {
515 child,
516 private_keys,
517 addresses,
518 wallet,
519 ipc_path: self.ipc_path,
520 port,
521 chain_id: self.chain_id.or(chain_id),
522 })
523 }
524}
525
526#[cfg(test)]
527mod test {
528 use super::*;
529
530 #[test]
531 fn assert_block_time_is_natural_number() {
532 let anvil = Anvil::new().block_time(12);
535 assert_eq!(anvil.block_time.unwrap().to_string(), "12");
536 }
537
538 #[test]
539 fn spawn_and_drop() {
540 let _ = Anvil::new().block_time(12).try_spawn().map(drop);
541 }
542}