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 host: String,
36 port: u16,
37 chain_id: Option<ChainId>,
38}
39
40impl AnvilInstance {
41 pub const fn child(&self) -> &Child {
43 &self.child
44 }
45
46 pub const fn child_mut(&mut self) -> &mut Child {
48 &mut self.child
49 }
50
51 pub fn keys(&self) -> &[K256SecretKey] {
53 &self.private_keys
54 }
55
56 #[track_caller]
62 pub fn first_key(&self) -> &K256SecretKey {
63 self.private_keys.first().unwrap()
64 }
65
66 pub fn nth_key(&self, idx: usize) -> Option<&K256SecretKey> {
68 self.private_keys.get(idx)
69 }
70
71 pub fn addresses(&self) -> &[Address] {
73 &self.addresses
74 }
75
76 pub fn host(&self) -> &str {
78 &self.host
79 }
80
81 pub const fn port(&self) -> u16 {
83 self.port
84 }
85
86 pub fn chain_id(&self) -> ChainId {
88 const ANVIL_HARDHAT_CHAIN_ID: ChainId = 31_337;
89 self.chain_id.unwrap_or(ANVIL_HARDHAT_CHAIN_ID)
90 }
91
92 #[doc(alias = "http_endpoint")]
94 pub fn endpoint(&self) -> String {
95 format!("http://{}:{}", self.host, self.port)
96 }
97
98 pub fn ws_endpoint(&self) -> String {
100 format!("ws://{}:{}", self.host, self.port)
101 }
102
103 pub fn ipc_path(&self) -> &str {
105 self.ipc_path.as_deref().unwrap_or(DEFAULT_IPC_ENDPOINT)
106 }
107
108 #[doc(alias = "http_endpoint_url")]
110 pub fn endpoint_url(&self) -> Url {
111 Url::parse(&self.endpoint()).unwrap()
112 }
113
114 pub fn ws_endpoint_url(&self) -> Url {
116 Url::parse(&self.ws_endpoint()).unwrap()
117 }
118
119 pub fn wallet(&self) -> Option<EthereumWallet> {
121 self.wallet.clone()
122 }
123}
124
125impl Drop for AnvilInstance {
126 fn drop(&mut self) {
127 #[cfg(unix)]
128 {
129 if let Ok(out) =
132 Command::new("kill").arg("-SIGTERM").arg(self.child.id().to_string()).output()
133 {
134 if out.status.success() {
135 return;
136 }
137 }
138 }
139 if let Err(err) = self.child.kill() {
140 eprintln!("alloy-node-bindings: failed to kill anvil process: {}", err);
141 }
142 }
143}
144
145#[derive(Clone, Debug, Default)]
167#[must_use = "This Builder struct does nothing unless it is `spawn`ed"]
168pub struct Anvil {
169 program: Option<PathBuf>,
170 host: Option<String>,
171 port: Option<u16>,
172 block_time: Option<f64>,
175 chain_id: Option<ChainId>,
176 mnemonic: Option<String>,
177 ipc_path: Option<String>,
178 fork: Option<String>,
179 fork_block_number: Option<u64>,
180 args: Vec<OsString>,
181 envs: Vec<(OsString, OsString)>,
182 timeout: Option<u64>,
183 keep_stdout: bool,
184}
185
186impl Anvil {
187 pub fn new() -> Self {
201 Self::default()
202 }
203
204 pub fn at(path: impl Into<PathBuf>) -> Self {
217 Self::new().path(path)
218 }
219
220 pub fn path<T: Into<PathBuf>>(mut self, path: T) -> Self {
225 self.program = Some(path.into());
226 self
227 }
228
229 pub fn host<T: Into<String>>(mut self, host: T) -> Self {
231 self.host = Some(host.into());
232 self
233 }
234
235 pub fn port<T: Into<u16>>(mut self, port: T) -> Self {
237 self.port = Some(port.into());
238 self
239 }
240
241 pub fn ipc_path(mut self, path: impl Into<String>) -> Self {
243 self.ipc_path = Some(path.into());
244 self
245 }
246
247 pub const fn chain_id(mut self, chain_id: u64) -> Self {
251 self.chain_id = Some(chain_id);
252 self
253 }
254
255 pub fn mnemonic<T: Into<String>>(mut self, mnemonic: T) -> Self {
257 self.mnemonic = Some(mnemonic.into());
258 self
259 }
260
261 pub const fn block_time(mut self, block_time: u64) -> Self {
263 self.block_time = Some(block_time as f64);
264 self
265 }
266
267 pub const fn block_time_f64(mut self, block_time: f64) -> Self {
270 self.block_time = Some(block_time);
271 self
272 }
273
274 pub const fn fork_block_number(mut self, fork_block_number: u64) -> Self {
278 self.fork_block_number = Some(fork_block_number);
279 self
280 }
281
282 pub fn fork<T: Into<String>>(mut self, fork: T) -> Self {
287 self.fork = Some(fork.into());
288 self
289 }
290
291 pub fn hardfork(mut self, hardfork: EthereumHardfork) -> Self {
293 self = self.args(["--hardfork", hardfork.to_string().as_str()]);
294 self
295 }
296
297 pub fn paris(mut self) -> Self {
299 self = self.hardfork(EthereumHardfork::Paris);
300 self
301 }
302
303 pub fn cancun(mut self) -> Self {
305 self = self.hardfork(EthereumHardfork::Cancun);
306 self
307 }
308
309 pub fn shanghai(mut self) -> Self {
311 self = self.hardfork(EthereumHardfork::Shanghai);
312 self
313 }
314
315 pub fn prague(mut self) -> Self {
317 self = self.hardfork(EthereumHardfork::Prague);
318 self
319 }
320
321 pub fn odyssey(mut self) -> Self {
323 self = self.arg("--odyssey");
324 self
325 }
326
327 pub fn auto_impersonate(mut self) -> Self {
329 self = self.arg("--auto-impersonate");
330 self
331 }
332
333 pub fn push_arg<T: Into<OsString>>(&mut self, arg: T) {
335 self.args.push(arg.into());
336 }
337
338 pub fn extend_args<I, S>(&mut self, args: I)
340 where
341 I: IntoIterator<Item = S>,
342 S: Into<OsString>,
343 {
344 for arg in args {
345 self.push_arg(arg);
346 }
347 }
348
349 pub fn arg<T: Into<OsString>>(mut self, arg: T) -> Self {
351 self.args.push(arg.into());
352 self
353 }
354
355 pub fn args<I, S>(mut self, args: I) -> Self
357 where
358 I: IntoIterator<Item = S>,
359 S: Into<OsString>,
360 {
361 for arg in args {
362 self = self.arg(arg);
363 }
364 self
365 }
366
367 pub fn env<K, V>(mut self, key: K, value: V) -> Self
369 where
370 K: Into<OsString>,
371 V: Into<OsString>,
372 {
373 self.envs.push((key.into(), value.into()));
374 self
375 }
376
377 pub fn envs<I, K, V>(mut self, envs: I) -> Self
379 where
380 I: IntoIterator<Item = (K, V)>,
381 K: Into<OsString>,
382 V: Into<OsString>,
383 {
384 for (key, value) in envs {
385 self = self.env(key, value);
386 }
387 self
388 }
389
390 pub const fn timeout(mut self, timeout: u64) -> Self {
393 self.timeout = Some(timeout);
394 self
395 }
396
397 pub const fn keep_stdout(mut self) -> Self {
401 self.keep_stdout = true;
402 self
403 }
404
405 #[track_caller]
411 pub fn spawn(self) -> AnvilInstance {
412 self.try_spawn().unwrap()
413 }
414
415 pub fn try_spawn(self) -> Result<AnvilInstance, NodeError> {
417 let mut cmd = self.program.as_ref().map_or_else(|| Command::new("anvil"), Command::new);
418 cmd.stdout(std::process::Stdio::piped()).stderr(std::process::Stdio::inherit());
419
420 cmd.env("FOUNDRY_DISABLE_NIGHTLY_WARNING", "")
422 .env("NO_COLOR", "1");
424
425 cmd.envs(self.envs);
427
428 if let Some(ref host) = self.host {
429 cmd.arg("--host").arg(host);
430 }
431
432 let mut port = self.port.unwrap_or_default();
433 cmd.arg("-p").arg(port.to_string());
434
435 if let Some(mnemonic) = self.mnemonic {
436 cmd.arg("-m").arg(mnemonic);
437 }
438
439 if let Some(chain_id) = self.chain_id {
440 cmd.arg("--chain-id").arg(chain_id.to_string());
441 }
442
443 if let Some(block_time) = self.block_time {
444 cmd.arg("-b").arg(block_time.to_string());
445 }
446
447 if let Some(fork) = self.fork {
448 cmd.arg("-f").arg(fork);
449 }
450
451 if let Some(fork_block_number) = self.fork_block_number {
452 cmd.arg("--fork-block-number").arg(fork_block_number.to_string());
453 }
454
455 if let Some(ipc_path) = &self.ipc_path {
456 cmd.arg("--ipc").arg(ipc_path);
457 }
458
459 cmd.args(self.args);
460
461 let mut child = cmd.spawn().map_err(NodeError::SpawnError)?;
462
463 let stdout = child.stdout.take().ok_or(NodeError::NoStdout)?;
464
465 let start = Instant::now();
466 let mut reader = BufReader::new(stdout);
467 let timeout = self.timeout.map(Duration::from_millis).unwrap_or(NODE_STARTUP_TIMEOUT);
468
469 let mut private_keys = Vec::new();
470 let mut addresses = Vec::new();
471 let mut is_private_key = false;
472 let mut chain_id = None;
473 let mut wallet = None;
474 loop {
475 if start + timeout <= Instant::now() {
476 return Err(NodeError::Timeout);
477 }
478
479 let mut line = String::new();
480 reader.read_line(&mut line).map_err(NodeError::ReadLineError)?;
481 trace!(target: "alloy::node::anvil", line);
482 if let Some(addr) = line.strip_prefix("Listening on") {
483 if let Ok(addr) = SocketAddr::from_str(addr.trim()) {
486 port = addr.port();
487 }
488 break;
489 }
490
491 if line.starts_with("Private Keys") {
492 is_private_key = true;
493 }
494
495 if is_private_key && line.starts_with('(') {
496 let key_str =
497 line.split("0x").last().ok_or(NodeError::ParsePrivateKeyError)?.trim();
498 let key_hex = hex::decode(key_str).map_err(NodeError::FromHexError)?;
499 let key = K256SecretKey::from_bytes((&key_hex[..]).into())
500 .map_err(|_| NodeError::DeserializePrivateKeyError)?;
501 addresses.push(Address::from_public_key(SigningKey::from(&key).verifying_key()));
502 private_keys.push(key);
503 }
504
505 if let Some(start_chain_id) = line.find("Chain ID:") {
506 let rest = &line[start_chain_id + "Chain ID:".len()..];
507 if let Ok(chain) = rest.split_whitespace().next().unwrap_or("").parse::<u64>() {
508 chain_id = Some(chain);
509 };
510 }
511
512 if !private_keys.is_empty() {
513 let mut private_keys = private_keys.iter().map(|key| {
514 let mut signer = LocalSigner::from(key.clone());
515 signer.set_chain_id(chain_id);
516 signer
517 });
518 let mut w = EthereumWallet::new(private_keys.next().unwrap());
519 for pk in private_keys {
520 w.register_signer(pk);
521 }
522 wallet = Some(w);
523 }
524 }
525
526 if self.keep_stdout {
527 child.stdout = Some(reader.into_inner());
529 }
530
531 Ok(AnvilInstance {
532 child,
533 private_keys,
534 addresses,
535 wallet,
536 ipc_path: self.ipc_path,
537 host: self.host.unwrap_or_else(|| "localhost".to_string()),
538 port,
539 chain_id: self.chain_id.or(chain_id),
540 })
541 }
542}
543
544#[cfg(test)]
545mod test {
546 use super::*;
547
548 #[test]
549 fn assert_block_time_is_natural_number() {
550 let anvil = Anvil::new().block_time(12);
553 assert_eq!(anvil.block_time.unwrap().to_string(), "12");
554 }
555
556 #[test]
557 fn spawn_and_drop() {
558 let _ = Anvil::new().block_time(12).try_spawn().map(drop);
559 }
560
561 #[test]
562 fn can_set_host() {
563 let anvil = Anvil::new().host("0.0.0.0").block_time(12).try_spawn();
564 if let Ok(anvil) = anvil {
565 assert_eq!(anvil.host(), "0.0.0.0");
566 assert!(anvil.endpoint().starts_with("http://0.0.0.0:"));
567 assert!(anvil.ws_endpoint().starts_with("ws://0.0.0.0:"));
568 }
569 }
570
571 #[test]
572 fn default_host_is_localhost() {
573 let anvil = Anvil::new().block_time(12).try_spawn();
574 if let Ok(anvil) = anvil {
575 assert_eq!(anvil.host(), "localhost");
576 assert!(anvil.endpoint().starts_with("http://localhost:"));
577 assert!(anvil.ws_endpoint().starts_with("ws://localhost:"));
578 }
579 }
580}