1use crate::{
2 AccountGenerator, CHAIN_ID, NodeConfig,
3 config::{DEFAULT_MNEMONIC, ForkChoice},
4 eth::{EthApi, backend::db::SerializableState, pool::transactions::TransactionOrder},
5};
6use alloy_genesis::Genesis;
7use alloy_primitives::{B256, U256, utils::Unit};
8use alloy_signer_local::coins_bip39::{English, Mnemonic};
9use anvil_server::ServerConfig;
10use clap::Parser;
11use core::fmt;
12use foundry_common::shell;
13use foundry_config::{Chain, Config, FigmentProviders};
14use foundry_evm::hardfork::{EthereumHardfork, OpHardfork};
15use foundry_evm_networks::NetworkConfigs;
16use futures::FutureExt;
17use rand_08::{SeedableRng, rngs::StdRng};
18use std::{
19 net::IpAddr,
20 path::{Path, PathBuf},
21 pin::Pin,
22 str::FromStr,
23 sync::{
24 Arc,
25 atomic::{AtomicUsize, Ordering},
26 },
27 task::{Context, Poll},
28 time::Duration,
29};
30use tokio::time::{Instant, Interval};
31
32#[derive(Clone, Debug, Parser)]
33pub struct NodeArgs {
34 #[arg(long, short, default_value = "8545", value_name = "NUM")]
36 pub port: u16,
37
38 #[arg(long, short, default_value = "10", value_name = "NUM")]
40 pub accounts: u64,
41
42 #[arg(long, default_value = "10000", value_name = "NUM")]
44 pub balance: u64,
45
46 #[arg(long, value_name = "NUM")]
48 pub timestamp: Option<u64>,
49
50 #[arg(long, value_name = "NUM")]
52 pub number: Option<u64>,
53
54 #[arg(long, short, conflicts_with_all = &["mnemonic_seed", "mnemonic_random"])]
57 pub mnemonic: Option<String>,
58
59 #[arg(long, conflicts_with_all = &["mnemonic", "mnemonic_seed"], default_missing_value = "12", num_args(0..=1))]
64 pub mnemonic_random: Option<usize>,
65
66 #[arg(long = "mnemonic-seed-unsafe", conflicts_with_all = &["mnemonic", "mnemonic_random"])]
72 pub mnemonic_seed: Option<u64>,
73
74 #[arg(long)]
78 pub derivation_path: Option<String>,
79
80 #[arg(long)]
85 pub hardfork: Option<String>,
86
87 #[arg(short, long, visible_alias = "blockTime", value_name = "SECONDS", value_parser = duration_from_secs_f64)]
89 pub block_time: Option<Duration>,
90
91 #[arg(long, value_name = "SLOTS_IN_AN_EPOCH", default_value_t = 32)]
93 pub slots_in_an_epoch: u64,
94
95 #[arg(long, value_name = "FILE", value_hint = clap::ValueHint::FilePath)]
97 pub config_out: Option<PathBuf>,
98
99 #[arg(long, visible_alias = "no-mine", conflicts_with = "block_time")]
101 pub no_mining: bool,
102
103 #[arg(long, requires = "block_time")]
104 pub mixed_mining: bool,
105
106 #[arg(
108 long,
109 value_name = "IP_ADDR",
110 env = "ANVIL_IP_ADDR",
111 default_value = "127.0.0.1",
112 help_heading = "Server options",
113 value_delimiter = ','
114 )]
115 pub host: Vec<IpAddr>,
116
117 #[arg(long, default_value = "fees")]
119 pub order: TransactionOrder,
120
121 #[arg(long, value_name = "PATH", value_parser= read_genesis_file)]
123 pub init: Option<Genesis>,
124
125 #[arg(
130 long,
131 value_name = "PATH",
132 value_parser = StateFile::parse,
133 conflicts_with_all = &[
134 "init",
135 "dump_state",
136 "load_state"
137 ]
138 )]
139 pub state: Option<StateFile>,
140
141 #[arg(short, long, value_name = "SECONDS")]
145 pub state_interval: Option<u64>,
146
147 #[arg(long, value_name = "PATH", conflicts_with = "init")]
151 pub dump_state: Option<PathBuf>,
152
153 #[arg(long, conflicts_with = "init", default_value = "false")]
160 pub preserve_historical_states: bool,
161
162 #[arg(
164 long,
165 value_name = "PATH",
166 value_parser = SerializableState::parse,
167 conflicts_with = "init"
168 )]
169 pub load_state: Option<SerializableState>,
170
171 #[arg(long, help = IPC_HELP, value_name = "PATH", visible_alias = "ipcpath")]
172 pub ipc: Option<Option<String>>,
173
174 #[arg(long)]
179 pub prune_history: Option<Option<usize>>,
180
181 #[arg(long, conflicts_with = "prune_history")]
185 pub max_persisted_states: Option<usize>,
186
187 #[arg(long)]
189 pub transaction_block_keeper: Option<usize>,
190
191 #[arg(long)]
193 pub max_transactions: Option<usize>,
194
195 #[command(flatten)]
196 pub evm: AnvilEvmArgs,
197
198 #[command(flatten)]
199 pub server_config: ServerConfig,
200
201 #[arg(long, value_name = "PATH")]
207 pub cache_path: Option<PathBuf>,
208}
209
210#[cfg(windows)]
211const IPC_HELP: &str =
212 "Launch an ipc server at the given path or default path = `\\.\\pipe\\anvil.ipc`";
213
214#[cfg(not(windows))]
216const IPC_HELP: &str = "Launch an ipc server at the given path or default path = `/tmp/anvil.ipc`";
217
218const DEFAULT_DUMP_INTERVAL: Duration = Duration::from_secs(60);
220
221impl NodeArgs {
222 pub fn into_node_config(self) -> eyre::Result<NodeConfig> {
223 let genesis_balance = Unit::ETHER.wei().saturating_mul(U256::from(self.balance));
224 let compute_units_per_second =
225 if self.evm.no_rate_limit { Some(u64::MAX) } else { self.evm.compute_units_per_second };
226
227 let hardfork = match &self.hardfork {
228 Some(hf) => {
229 if self.evm.networks.is_optimism() {
230 Some(OpHardfork::from_str(hf)?.into())
231 } else {
232 Some(EthereumHardfork::from_str(hf)?.into())
233 }
234 }
235 None => None,
236 };
237
238 Ok(NodeConfig::default()
239 .with_gas_limit(self.evm.gas_limit)
240 .disable_block_gas_limit(self.evm.disable_block_gas_limit)
241 .enable_tx_gas_limit(self.evm.enable_tx_gas_limit)
242 .with_gas_price(self.evm.gas_price)
243 .with_hardfork(hardfork)
244 .with_blocktime(self.block_time)
245 .with_no_mining(self.no_mining)
246 .with_mixed_mining(self.mixed_mining, self.block_time)
247 .with_account_generator(self.account_generator())?
248 .with_genesis_balance(genesis_balance)
249 .with_genesis_timestamp(self.timestamp)
250 .with_genesis_block_number(self.number)
251 .with_port(self.port)
252 .with_fork_choice(match (self.evm.fork_block_number, self.evm.fork_transaction_hash) {
253 (Some(block), None) => Some(ForkChoice::Block(block)),
254 (None, Some(hash)) => Some(ForkChoice::Transaction(hash)),
255 _ => self
256 .evm
257 .fork_url
258 .as_ref()
259 .and_then(|f| f.block)
260 .map(|num| ForkChoice::Block(num as i128)),
261 })
262 .with_fork_headers(self.evm.fork_headers)
263 .with_fork_chain_id(self.evm.fork_chain_id.map(u64::from).map(U256::from))
264 .fork_request_timeout(self.evm.fork_request_timeout.map(Duration::from_millis))
265 .fork_request_retries(self.evm.fork_request_retries)
266 .fork_retry_backoff(self.evm.fork_retry_backoff.map(Duration::from_millis))
267 .fork_compute_units_per_second(compute_units_per_second)
268 .with_eth_rpc_url(self.evm.fork_url.map(|fork| fork.url))
269 .with_base_fee(self.evm.block_base_fee_per_gas)
270 .disable_min_priority_fee(self.evm.disable_min_priority_fee)
271 .with_no_storage_caching(self.evm.no_storage_caching)
272 .with_server_config(self.server_config)
273 .with_host(self.host)
274 .set_silent(shell::is_quiet())
275 .set_config_out(self.config_out)
276 .with_chain_id(self.evm.chain_id)
277 .with_transaction_order(self.order)
278 .with_genesis(self.init)
279 .with_steps_tracing(self.evm.steps_tracing)
280 .with_print_logs(!self.evm.disable_console_log)
281 .with_print_traces(self.evm.print_traces)
282 .with_auto_impersonate(self.evm.auto_impersonate)
283 .with_ipc(self.ipc)
284 .with_code_size_limit(self.evm.code_size_limit)
285 .disable_code_size_limit(self.evm.disable_code_size_limit)
286 .set_pruned_history(self.prune_history)
287 .with_init_state(self.load_state.or_else(|| self.state.and_then(|s| s.state)))
288 .with_transaction_block_keeper(self.transaction_block_keeper)
289 .with_max_transactions(self.max_transactions)
290 .with_max_persisted_states(self.max_persisted_states)
291 .with_networks(self.evm.networks)
292 .with_disable_default_create2_deployer(self.evm.disable_default_create2_deployer)
293 .with_disable_pool_balance_checks(self.evm.disable_pool_balance_checks)
294 .with_slots_in_an_epoch(self.slots_in_an_epoch)
295 .with_memory_limit(self.evm.memory_limit)
296 .with_cache_path(self.cache_path))
297 }
298
299 fn account_generator(&self) -> AccountGenerator {
300 let mut generator = AccountGenerator::new(self.accounts as usize)
301 .phrase(DEFAULT_MNEMONIC)
302 .chain_id(self.evm.chain_id.unwrap_or(CHAIN_ID.into()));
303 if let Some(ref mnemonic) = self.mnemonic {
304 generator = generator.phrase(mnemonic);
305 } else if let Some(count) = self.mnemonic_random {
306 let mut rng = rand_08::thread_rng();
307 let mnemonic = match Mnemonic::<English>::new_with_count(&mut rng, count) {
308 Ok(mnemonic) => mnemonic.to_phrase(),
309 Err(err) => {
310 warn!(target: "node", ?count, %err, "failed to generate mnemonic, falling back to 12-word random mnemonic");
311 Mnemonic::<English>::new_with_count(&mut rng, 12)
314 .expect("valid default word count")
315 .to_phrase()
316 }
317 };
318 generator = generator.phrase(mnemonic);
319 } else if let Some(seed) = self.mnemonic_seed {
320 let mut seed = StdRng::seed_from_u64(seed);
321 let mnemonic = Mnemonic::<English>::new(&mut seed).to_phrase();
322 generator = generator.phrase(mnemonic);
323 }
324 if let Some(ref derivation) = self.derivation_path {
325 generator = generator.derivation_path(derivation);
326 }
327 generator
328 }
329
330 fn dump_state_path(&self) -> Option<PathBuf> {
332 self.dump_state.as_ref().or_else(|| self.state.as_ref().map(|s| &s.path)).cloned()
333 }
334
335 pub async fn run(self) -> eyre::Result<()> {
339 let dump_state = self.dump_state_path();
340 let dump_interval =
341 self.state_interval.map(Duration::from_secs).unwrap_or(DEFAULT_DUMP_INTERVAL);
342 let preserve_historical_states = self.preserve_historical_states;
343
344 let (api, mut handle) = crate::try_spawn(self.into_node_config()?).await?;
345
346 let mut fork = api.get_fork();
348 let running = Arc::new(AtomicUsize::new(0));
349
350 let mut signal = handle.shutdown_signal_mut().take();
353
354 let task_manager = handle.task_manager();
355 let mut on_shutdown = task_manager.on_shutdown();
356
357 let mut state_dumper =
358 PeriodicStateDumper::new(api, dump_state, dump_interval, preserve_historical_states);
359
360 task_manager.spawn(async move {
361 #[cfg(unix)]
363 let mut sigterm = Box::pin(async {
364 if let Ok(mut stream) =
365 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
366 {
367 stream.recv().await;
368 } else {
369 futures::future::pending::<()>().await;
370 }
371 });
372
373 #[cfg(not(unix))]
375 let mut sigterm = Box::pin(futures::future::pending::<()>());
376
377 tokio::select! {
379 _ = &mut sigterm => {
380 trace!("received sigterm signal, shutting down");
381 }
382 _ = &mut on_shutdown => {}
383 _ = &mut state_dumper => {}
384 }
385
386 state_dumper.dump().await;
388
389 if let Some(fork) = fork.take() {
392 trace!("flushing cache on shutdown");
393 fork.database
394 .read()
395 .await
396 .maybe_flush_cache()
397 .expect("Could not flush cache on fork DB");
398 }
401 std::process::exit(0);
402 });
403
404 ctrlc::set_handler(move || {
405 let prev = running.fetch_add(1, Ordering::SeqCst);
406 if prev == 0 {
407 trace!("received shutdown signal, shutting down");
408 let _ = signal.take();
409 }
410 })
411 .expect("Error setting Ctrl-C handler");
412
413 Ok(handle.await??)
414 }
415}
416
417#[derive(Clone, Debug, Parser)]
419#[command(next_help_heading = "EVM options")]
420pub struct AnvilEvmArgs {
421 #[arg(
425 long,
426 short,
427 visible_alias = "rpc-url",
428 value_name = "URL",
429 help_heading = "Fork config"
430 )]
431 pub fork_url: Option<ForkUrl>,
432
433 #[arg(
437 long = "fork-header",
438 value_name = "HEADERS",
439 help_heading = "Fork config",
440 requires = "fork_url"
441 )]
442 pub fork_headers: Vec<String>,
443
444 #[arg(id = "timeout", long = "timeout", help_heading = "Fork config", requires = "fork_url")]
448 pub fork_request_timeout: Option<u64>,
449
450 #[arg(id = "retries", long = "retries", help_heading = "Fork config", requires = "fork_url")]
454 pub fork_request_retries: Option<u32>,
455
456 #[arg(
462 long,
463 requires = "fork_url",
464 value_name = "BLOCK",
465 help_heading = "Fork config",
466 allow_hyphen_values = true
467 )]
468 pub fork_block_number: Option<i128>,
469
470 #[arg(
474 long,
475 requires = "fork_url",
476 value_name = "TRANSACTION",
477 help_heading = "Fork config",
478 conflicts_with = "fork_block_number"
479 )]
480 pub fork_transaction_hash: Option<B256>,
481
482 #[arg(long, requires = "fork_url", value_name = "BACKOFF", help_heading = "Fork config")]
486 pub fork_retry_backoff: Option<u64>,
487
488 #[arg(
494 long,
495 help_heading = "Fork config",
496 value_name = "CHAIN",
497 requires = "fork_block_number"
498 )]
499 pub fork_chain_id: Option<Chain>,
500
501 #[arg(
507 long,
508 requires = "fork_url",
509 alias = "cups",
510 value_name = "CUPS",
511 help_heading = "Fork config"
512 )]
513 pub compute_units_per_second: Option<u64>,
514
515 #[arg(
521 long,
522 requires = "fork_url",
523 value_name = "NO_RATE_LIMITS",
524 help_heading = "Fork config",
525 visible_alias = "no-rpc-rate-limit"
526 )]
527 pub no_rate_limit: bool,
528
529 #[arg(long, requires = "fork_url", help_heading = "Fork config")]
537 pub no_storage_caching: bool,
538
539 #[arg(long, alias = "block-gas-limit", help_heading = "Environment config")]
541 pub gas_limit: Option<u64>,
542
543 #[arg(
545 long,
546 value_name = "DISABLE_GAS_LIMIT",
547 help_heading = "Environment config",
548 alias = "disable-gas-limit",
549 conflicts_with = "gas_limit"
550 )]
551 pub disable_block_gas_limit: bool,
552
553 #[arg(long, visible_alias = "tx-gas-limit", help_heading = "Environment config")]
555 pub enable_tx_gas_limit: bool,
556
557 #[arg(long, value_name = "CODE_SIZE", help_heading = "Environment config")]
560 pub code_size_limit: Option<usize>,
561
562 #[arg(
564 long,
565 value_name = "DISABLE_CODE_SIZE_LIMIT",
566 conflicts_with = "code_size_limit",
567 help_heading = "Environment config"
568 )]
569 pub disable_code_size_limit: bool,
570
571 #[arg(long, help_heading = "Environment config")]
573 pub gas_price: Option<u128>,
574
575 #[arg(
577 long,
578 visible_alias = "base-fee",
579 value_name = "FEE",
580 help_heading = "Environment config"
581 )]
582 pub block_base_fee_per_gas: Option<u64>,
583
584 #[arg(long, visible_alias = "no-priority-fee", help_heading = "Environment config")]
586 pub disable_min_priority_fee: bool,
587
588 #[arg(long, alias = "chain", help_heading = "Environment config")]
590 pub chain_id: Option<Chain>,
591
592 #[arg(long, visible_alias = "tracing")]
594 pub steps_tracing: bool,
595
596 #[arg(long, visible_alias = "no-console-log")]
598 pub disable_console_log: bool,
599
600 #[arg(long, visible_alias = "enable-trace-printing")]
602 pub print_traces: bool,
603
604 #[arg(long, visible_alias = "auto-unlock")]
607 pub auto_impersonate: bool,
608
609 #[arg(long, visible_alias = "no-create2")]
611 pub disable_default_create2_deployer: bool,
612
613 #[arg(long)]
615 pub disable_pool_balance_checks: bool,
616
617 #[arg(long)]
619 pub memory_limit: Option<u64>,
620
621 #[command(flatten)]
622 pub networks: NetworkConfigs,
623}
624
625impl AnvilEvmArgs {
629 pub fn resolve_rpc_alias(&mut self) {
630 if let Some(fork_url) = &self.fork_url
631 && let Ok(config) = Config::load_with_providers(FigmentProviders::Anvil)
632 && let Some(Ok(url)) = config.get_rpc_url_with_alias(&fork_url.url)
633 {
634 self.fork_url = Some(ForkUrl { url: url.to_string(), block: fork_url.block });
635 }
636 }
637}
638
639struct PeriodicStateDumper {
641 in_progress_dump: Option<Pin<Box<dyn Future<Output = ()> + Send + Sync + 'static>>>,
642 api: EthApi,
643 dump_state: Option<PathBuf>,
644 preserve_historical_states: bool,
645 interval: Interval,
646}
647
648impl PeriodicStateDumper {
649 fn new(
650 api: EthApi,
651 dump_state: Option<PathBuf>,
652 interval: Duration,
653 preserve_historical_states: bool,
654 ) -> Self {
655 let dump_state = dump_state.map(|mut dump_state| {
656 if dump_state.is_dir() {
657 dump_state = dump_state.join("state.json");
658 }
659 dump_state
660 });
661
662 let interval = tokio::time::interval_at(Instant::now() + interval, interval);
664 Self { in_progress_dump: None, api, dump_state, preserve_historical_states, interval }
665 }
666
667 async fn dump(&self) {
668 if let Some(state) = self.dump_state.clone() {
669 Self::dump_state(self.api.clone(), state, self.preserve_historical_states).await
670 }
671 }
672
673 async fn dump_state(api: EthApi, dump_state: PathBuf, preserve_historical_states: bool) {
675 trace!(path=?dump_state, "Dumping state on shutdown");
676 match api.serialized_state(preserve_historical_states).await {
677 Ok(state) => {
678 if let Err(err) = foundry_common::fs::write_json_file(&dump_state, &state) {
679 error!(?err, "Failed to dump state");
680 } else {
681 trace!(path=?dump_state, "Dumped state on shutdown");
682 }
683 }
684 Err(err) => {
685 error!(?err, "Failed to extract state");
686 }
687 }
688 }
689}
690
691impl Future for PeriodicStateDumper {
693 type Output = ();
694
695 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
696 let this = self.get_mut();
697 if this.dump_state.is_none() {
698 return Poll::Pending;
699 }
700
701 loop {
702 if let Some(mut flush) = this.in_progress_dump.take() {
703 match flush.poll_unpin(cx) {
704 Poll::Ready(_) => {
705 this.interval.reset();
706 }
707 Poll::Pending => {
708 this.in_progress_dump = Some(flush);
709 return Poll::Pending;
710 }
711 }
712 }
713
714 if this.interval.poll_tick(cx).is_ready() {
715 let api = this.api.clone();
716 let path = this.dump_state.clone().expect("exists; see above");
717 this.in_progress_dump =
718 Some(Box::pin(Self::dump_state(api, path, this.preserve_historical_states)));
719 } else {
720 break;
721 }
722 }
723
724 Poll::Pending
725 }
726}
727
728#[derive(Clone, Debug)]
730pub struct StateFile {
731 pub path: PathBuf,
732 pub state: Option<SerializableState>,
733}
734
735impl StateFile {
736 fn parse(path: &str) -> Result<Self, String> {
739 Self::parse_path(path)
740 }
741
742 pub fn parse_path(path: impl AsRef<Path>) -> Result<Self, String> {
744 let mut path = path.as_ref().to_path_buf();
745 if path.is_dir() {
746 path = path.join("state.json");
747 }
748 let mut state = Self { path, state: None };
749 if !state.path.exists() {
750 return Ok(state);
751 }
752
753 state.state = Some(SerializableState::load(&state.path).map_err(|err| err.to_string())?);
754
755 Ok(state)
756 }
757}
758
759#[derive(Clone, Debug, PartialEq, Eq)]
762pub struct ForkUrl {
763 pub url: String,
765 pub block: Option<u64>,
767}
768
769impl fmt::Display for ForkUrl {
770 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
771 self.url.fmt(f)?;
772 if let Some(block) = self.block {
773 write!(f, "@{block}")?;
774 }
775 Ok(())
776 }
777}
778
779impl FromStr for ForkUrl {
780 type Err = String;
781
782 fn from_str(s: &str) -> Result<Self, Self::Err> {
783 if let Some((url, block)) = s.rsplit_once('@') {
784 if block == "latest" {
785 return Ok(Self { url: url.to_string(), block: None });
786 }
787 if !block.is_empty() && !block.contains(':') && !block.contains('.') {
789 let block: u64 = block
790 .parse()
791 .map_err(|_| format!("Failed to parse block number: `{block}`"))?;
792 return Ok(Self { url: url.to_string(), block: Some(block) });
793 }
794 }
795 Ok(Self { url: s.to_string(), block: None })
796 }
797}
798
799fn read_genesis_file(path: &str) -> Result<Genesis, String> {
801 foundry_common::fs::read_json_file(path.as_ref()).map_err(|err| err.to_string())
802}
803
804fn duration_from_secs_f64(s: &str) -> Result<Duration, String> {
805 let s = s.parse::<f64>().map_err(|e| e.to_string())?;
806 if s == 0.0 {
807 return Err("Duration must be greater than 0".to_string());
808 }
809 Duration::try_from_secs_f64(s).map_err(|e| e.to_string())
810}
811
812#[cfg(test)]
813mod tests {
814 use super::*;
815 use std::{env, net::Ipv4Addr};
816
817 #[test]
818 fn test_parse_fork_url() {
819 let fork: ForkUrl = "http://localhost:8545@1000000".parse().unwrap();
820 assert_eq!(
821 fork,
822 ForkUrl { url: "http://localhost:8545".to_string(), block: Some(1000000) }
823 );
824
825 let fork: ForkUrl = "http://localhost:8545".parse().unwrap();
826 assert_eq!(fork, ForkUrl { url: "http://localhost:8545".to_string(), block: None });
827
828 let fork: ForkUrl = "wss://user:password@example.com/".parse().unwrap();
829 assert_eq!(
830 fork,
831 ForkUrl { url: "wss://user:password@example.com/".to_string(), block: None }
832 );
833
834 let fork: ForkUrl = "wss://user:password@example.com/@latest".parse().unwrap();
835 assert_eq!(
836 fork,
837 ForkUrl { url: "wss://user:password@example.com/".to_string(), block: None }
838 );
839
840 let fork: ForkUrl = "wss://user:password@example.com/@100000".parse().unwrap();
841 assert_eq!(
842 fork,
843 ForkUrl { url: "wss://user:password@example.com/".to_string(), block: Some(100000) }
844 );
845 }
846
847 #[test]
848 fn can_parse_ethereum_hardfork() {
849 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--hardfork", "berlin"]);
850 let config = args.into_node_config().unwrap();
851 assert_eq!(config.hardfork, Some(EthereumHardfork::Berlin.into()));
852 }
853
854 #[test]
855 fn can_parse_optimism_hardfork() {
856 let args: NodeArgs =
857 NodeArgs::parse_from(["anvil", "--optimism", "--hardfork", "Regolith"]);
858 let config = args.into_node_config().unwrap();
859 assert_eq!(config.hardfork, Some(OpHardfork::Regolith.into()));
860 }
861
862 #[test]
863 fn cant_parse_invalid_hardfork() {
864 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--hardfork", "Regolith"]);
865 let config = args.into_node_config();
866 assert!(config.is_err());
867 }
868
869 #[test]
870 fn can_parse_fork_headers() {
871 let args: NodeArgs = NodeArgs::parse_from([
872 "anvil",
873 "--fork-url",
874 "http,://localhost:8545",
875 "--fork-header",
876 "User-Agent: test-agent",
877 "--fork-header",
878 "Referrer: example.com",
879 ]);
880 assert_eq!(args.evm.fork_headers, vec!["User-Agent: test-agent", "Referrer: example.com"]);
881 }
882
883 #[test]
884 fn can_parse_prune_config() {
885 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--prune-history"]);
886 assert!(args.prune_history.is_some());
887
888 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--prune-history", "100"]);
889 assert_eq!(args.prune_history, Some(Some(100)));
890 }
891
892 #[test]
893 fn can_parse_max_persisted_states_config() {
894 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--max-persisted-states", "500"]);
895 assert_eq!(args.max_persisted_states, (Some(500)));
896 }
897
898 #[test]
899 fn can_parse_disable_block_gas_limit() {
900 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--disable-block-gas-limit"]);
901 assert!(args.evm.disable_block_gas_limit);
902
903 let args =
904 NodeArgs::try_parse_from(["anvil", "--disable-block-gas-limit", "--gas-limit", "100"]);
905 assert!(args.is_err());
906 }
907
908 #[test]
909 fn can_parse_enable_tx_gas_limit() {
910 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--enable-tx-gas-limit"]);
911 assert!(args.evm.enable_tx_gas_limit);
912
913 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--tx-gas-limit"]);
915 assert!(args.evm.enable_tx_gas_limit);
916 }
917
918 #[test]
919 fn can_parse_disable_code_size_limit() {
920 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--disable-code-size-limit"]);
921 assert!(args.evm.disable_code_size_limit);
922
923 let args = NodeArgs::try_parse_from([
924 "anvil",
925 "--disable-code-size-limit",
926 "--code-size-limit",
927 "100",
928 ]);
929 assert!(args.is_err());
931 }
932
933 #[test]
934 fn can_parse_host() {
935 let args = NodeArgs::parse_from(["anvil"]);
936 assert_eq!(args.host, vec![IpAddr::V4(Ipv4Addr::LOCALHOST)]);
937
938 let args = NodeArgs::parse_from([
939 "anvil", "--host", "::1", "--host", "1.1.1.1", "--host", "2.2.2.2",
940 ]);
941 assert_eq!(
942 args.host,
943 ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::<IpAddr>().unwrap()).to_vec()
944 );
945
946 let args = NodeArgs::parse_from(["anvil", "--host", "::1,1.1.1.1,2.2.2.2"]);
947 assert_eq!(
948 args.host,
949 ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::<IpAddr>().unwrap()).to_vec()
950 );
951
952 unsafe { env::set_var("ANVIL_IP_ADDR", "1.1.1.1") };
953 let args = NodeArgs::parse_from(["anvil"]);
954 assert_eq!(args.host, vec!["1.1.1.1".parse::<IpAddr>().unwrap()]);
955
956 unsafe { env::set_var("ANVIL_IP_ADDR", "::1,1.1.1.1,2.2.2.2") };
957 let args = NodeArgs::parse_from(["anvil"]);
958 assert_eq!(
959 args.host,
960 ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::<IpAddr>().unwrap()).to_vec()
961 );
962 }
963}