1use crate::{
2 AccountGenerator, CHAIN_ID, NodeConfig,
3 config::{DEFAULT_MNEMONIC, DEFAULT_SLOTS_IN_AN_EPOCH, ForkChoice},
4 eth::{EthApi, backend::db::SerializableState, pool::transactions::TransactionOrder},
5};
6use alloy_genesis::Genesis;
7use alloy_network::Network;
8use alloy_primitives::{Address, B256, U256, map::HashMap, utils::Unit};
9use alloy_signer_local::coins_bip39::{English, Mnemonic};
10use anvil_server::ServerConfig;
11use clap::Parser;
12use core::fmt;
13use foundry_common::shell;
14use foundry_config::{Chain, Config, FigmentProviders};
15use foundry_evm::hardfork::FoundryHardfork;
16use foundry_evm_networks::NetworkConfigs;
17use foundry_primitives::FoundryReceiptEnvelope;
18use futures::FutureExt;
19use rand_08::{SeedableRng, rngs::StdRng};
20use std::{
21 net::IpAddr,
22 path::{Path, PathBuf},
23 pin::Pin,
24 str::FromStr,
25 sync::{
26 Arc,
27 atomic::{AtomicUsize, Ordering},
28 },
29 task::{Context, Poll},
30 time::Duration,
31};
32use tokio::time::{Instant, Interval};
33
34#[derive(Clone, Debug, Parser)]
35pub struct NodeArgs {
36 #[arg(long, short, default_value = "8545", value_name = "NUM")]
38 pub port: u16,
39
40 #[arg(long, short, default_value = "10", value_name = "NUM")]
42 pub accounts: u64,
43
44 #[arg(long, default_value = "10000", value_name = "NUM")]
46 pub balance: u64,
47
48 #[arg(long, value_name = "NUM")]
50 pub timestamp: Option<u64>,
51
52 #[arg(long, value_name = "NUM")]
54 pub number: Option<u64>,
55
56 #[arg(long, short, conflicts_with_all = &["mnemonic_seed", "mnemonic_random"])]
59 pub mnemonic: Option<String>,
60
61 #[arg(long, conflicts_with_all = &["mnemonic", "mnemonic_seed"], default_missing_value = "12", num_args(0..=1))]
66 pub mnemonic_random: Option<usize>,
67
68 #[arg(long = "mnemonic-seed-unsafe", conflicts_with_all = &["mnemonic", "mnemonic_random"])]
74 pub mnemonic_seed: Option<u64>,
75
76 #[arg(long)]
80 pub derivation_path: Option<String>,
81
82 #[arg(long = "tempo.fee-payer", value_name = "ADDRESS")]
88 pub tempo_fee_payer: Option<Address>,
89
90 #[arg(long)]
95 pub hardfork: Option<String>,
96
97 #[cfg(feature = "base")]
99 #[arg(long, value_name = "ADDRESS")]
100 pub base_activation_admin: Option<Address>,
101
102 #[arg(short, long, visible_alias = "blockTime", value_name = "SECONDS", value_parser = duration_from_secs_f64)]
104 pub block_time: Option<Duration>,
105
106 #[arg(long, value_name = "MILLISECONDS", default_value = "5")]
109 pub transaction_coalescing_window: u64,
110
111 #[arg(long, value_name = "SLOTS_IN_AN_EPOCH", default_value_t = DEFAULT_SLOTS_IN_AN_EPOCH)]
113 pub slots_in_an_epoch: u64,
114
115 #[arg(long, value_name = "FILE", value_hint = clap::ValueHint::FilePath)]
117 pub config_out: Option<PathBuf>,
118
119 #[arg(long, visible_alias = "no-mine", conflicts_with = "block_time")]
121 pub no_mining: bool,
122
123 #[arg(long, requires = "block_time")]
126 pub mixed_mining: bool,
127
128 #[arg(
130 long,
131 value_name = "IP_ADDR",
132 env = "ANVIL_IP_ADDR",
133 default_value = "127.0.0.1",
134 help_heading = "Server options",
135 value_delimiter = ','
136 )]
137 pub host: Vec<IpAddr>,
138
139 #[arg(long, default_value = "fees")]
141 pub order: TransactionOrder,
142
143 #[arg(long, value_name = "PATH", value_parser= read_genesis_file)]
145 pub init: Option<Genesis>,
146
147 #[arg(
152 long,
153 value_name = "PATH",
154 value_parser = StateFile::parse,
155 conflicts_with_all = &[
156 "init",
157 "dump_state",
158 "load_state"
159 ]
160 )]
161 pub state: Option<StateFile>,
162
163 #[arg(short, long, value_name = "SECONDS")]
167 pub state_interval: Option<u64>,
168
169 #[arg(long, value_name = "PATH", conflicts_with = "init")]
173 pub dump_state: Option<PathBuf>,
174
175 #[arg(long, conflicts_with = "init", default_value = "false")]
182 pub preserve_historical_states: bool,
183
184 #[arg(
186 long,
187 value_name = "PATH",
188 value_parser = SerializableState::parse,
189 conflicts_with = "init"
190 )]
191 pub load_state: Option<SerializableState>,
192
193 #[arg(long, value_name = "ADDRESS:AMOUNT", value_delimiter = ' ', num_args = 1..)]
198 pub fund_accounts: Vec<String>,
199
200 #[arg(long, help = IPC_HELP, value_name = "PATH", visible_alias = "ipcpath")]
201 pub ipc: Option<Option<String>>,
202
203 #[arg(long)]
208 pub prune_history: Option<Option<usize>>,
209
210 #[arg(long, conflicts_with = "prune_history")]
214 pub max_persisted_states: Option<usize>,
215
216 #[arg(long)]
218 pub transaction_block_keeper: Option<usize>,
219
220 #[arg(long)]
222 pub max_transactions: Option<usize>,
223
224 #[command(flatten)]
225 pub evm: AnvilEvmArgs,
226
227 #[command(flatten)]
228 pub server_config: ServerConfig,
229
230 #[arg(long, value_name = "PATH")]
236 pub cache_path: Option<PathBuf>,
237}
238
239#[cfg(windows)]
240const IPC_HELP: &str =
241 "Launch an ipc server at the given path or default path = `\\.\\pipe\\anvil.ipc`";
242
243#[cfg(not(windows))]
245const IPC_HELP: &str = "Launch an ipc server at the given path or default path = `/tmp/anvil.ipc`";
246
247const DEFAULT_DUMP_INTERVAL: Duration = Duration::from_secs(60);
249
250impl NodeArgs {
251 pub fn into_node_config(self) -> eyre::Result<NodeConfig> {
252 let genesis_balance = Unit::ETHER.wei().saturating_mul(U256::from(self.balance));
253 let compute_units_per_second =
254 if self.evm.no_rate_limit { Some(u64::MAX) } else { self.evm.compute_units_per_second };
255
256 if self.evm.fork_url.len() > 1 {
258 for fork in &self.evm.fork_url[1..] {
259 if fork.block.is_some() {
260 eyre::bail!(
261 "Block number suffixes (@block) on secondary --fork-url values are not supported. \
262 Use --fork-block-number to set the fork block for all endpoints."
263 );
264 }
265 }
266 }
267
268 let funded_accounts = self.parse_funded_accounts()?;
269 #[cfg(feature = "base")]
270 let base_activation_admin = self.base_activation_admin;
271
272 let local_chain_id = self
273 .evm
274 .chain_id
275 .map(u64::from)
276 .or_else(|| self.init.as_ref().map(|genesis| genesis.config.chain_id));
277 let inferred_network_chain_id = self
278 .evm
279 .fork_chain_id
280 .map(u64::from)
281 .or(if self.evm.fork_url.is_empty() { local_chain_id } else { None });
282 let networks = if let Some(chain_id) = inferred_network_chain_id {
283 self.evm.networks.try_with_chain_id(chain_id).map_err(eyre::Report::msg)?
284 } else {
285 self.evm.networks
286 };
287
288 let hardfork = match &self.hardfork {
289 Some(hf) => Some(parse_hardfork(hf, &networks)?),
290 None => None,
291 };
292 let networks = if let Some(hardfork) = hardfork {
293 networks.normalize_for_hardfork(hardfork).map_err(eyre::Report::msg)?
294 } else {
295 networks
296 };
297
298 let config = NodeConfig::default()
299 .with_gas_limit(self.evm.gas_limit)
300 .disable_block_gas_limit(self.evm.disable_block_gas_limit)
301 .enable_tx_gas_limit(self.evm.enable_tx_gas_limit)
302 .with_gas_price(self.evm.gas_price)
303 .with_hardfork(hardfork)
304 .with_blocktime(self.block_time)
305 .with_transaction_coalescing_window(Duration::from_millis(
306 self.transaction_coalescing_window,
307 ))
308 .with_no_mining(self.no_mining)
309 .with_mixed_mining(self.mixed_mining, self.block_time)
310 .with_account_generator(self.account_generator())?
311 .with_genesis_balance(genesis_balance)
312 .with_genesis_timestamp(self.timestamp)
313 .with_genesis_block_number(self.number)
314 .with_port(self.port)
315 .with_fork_choice(match (self.evm.fork_block_number, self.evm.fork_transaction_hash) {
316 (Some(block), None) => Some(ForkChoice::Block(block)),
317 (None, Some(hash)) => Some(ForkChoice::Transaction(hash)),
318 _ => self
319 .evm
320 .fork_url
321 .first()
322 .and_then(|f| f.block)
323 .map(|num| ForkChoice::Block(num as i128)),
324 })
325 .with_fork_headers(self.evm.fork_headers)
326 .with_fork_chain_id(self.evm.fork_chain_id.map(u64::from).map(U256::from))
327 .with_no_fork_node_info(self.evm.no_fork_node_info)
328 .with_no_bal(self.evm.no_bal)
329 .with_fork_state_by_number(self.evm.fork_state_by_number)
330 .fork_request_timeout(self.evm.fork_request_timeout.map(Duration::from_millis))
331 .fork_request_retries(self.evm.fork_request_retries)
332 .fork_retry_backoff(self.evm.fork_retry_backoff.map(Duration::from_millis))
333 .fork_compute_units_per_second(compute_units_per_second)
334 .with_fork_urls(self.evm.fork_url.into_iter().map(|f| f.url).collect())
335 .with_base_fee(self.evm.block_base_fee_per_gas)
336 .disable_min_priority_fee(self.evm.disable_min_priority_fee)
337 .with_no_storage_caching(self.evm.no_storage_caching)
338 .with_server_config(self.server_config)
339 .with_host(self.host)
340 .set_silent(shell::is_quiet())
341 .set_config_out(self.config_out)
342 .with_transaction_order(self.order)
343 .with_genesis(self.init)
344 .with_steps_tracing(self.evm.steps_tracing)
345 .with_print_logs(!self.evm.disable_console_log)
346 .with_print_traces(self.evm.print_traces)
347 .with_auto_impersonate(self.evm.auto_impersonate)
348 .with_ipc(self.ipc)
349 .with_code_size_limit(self.evm.code_size_limit)
350 .disable_code_size_limit(self.evm.disable_code_size_limit)
351 .set_pruned_history(self.prune_history)
352 .with_init_state(self.load_state.or_else(|| self.state.and_then(|s| s.state)))
353 .with_transaction_block_keeper(self.transaction_block_keeper)
354 .with_max_transactions(self.max_transactions)
355 .with_max_persisted_states(self.max_persisted_states)
356 .with_chain_id(self.evm.chain_id)
357 .with_networks(networks)
360 .with_tempo_fee_payer(self.tempo_fee_payer)
361 .with_disable_default_create2_deployer(self.evm.disable_default_create2_deployer)
362 .with_disable_pool_balance_checks(self.evm.disable_pool_balance_checks)
363 .with_slots_in_an_epoch(self.slots_in_an_epoch)
364 .with_memory_limit(self.evm.memory_limit)
365 .with_cache_path(self.cache_path)
366 .with_funded_accounts(funded_accounts);
367 #[cfg(feature = "base")]
368 let config = config.with_base_activation_admin(base_activation_admin);
369 Ok(config)
370 }
371
372 fn parse_funded_accounts(&self) -> eyre::Result<HashMap<Address, U256>> {
373 let mut accounts = HashMap::default();
374 for entry in &self.fund_accounts {
375 let parts: Vec<&str> = entry.split(':').collect();
376 if parts.len() != 2 {
377 eyre::bail!(
378 "Invalid fund-accounts entry '{}'. Expected format: ADDRESS:AMOUNT",
379 entry
380 );
381 }
382 let address = parts[0]
383 .parse::<Address>()
384 .map_err(|e| eyre::eyre!("Invalid address '{}': {}", parts[0], e))?;
385 let amount: u64 = parts[1]
386 .parse()
387 .map_err(|e| eyre::eyre!("Invalid amount '{}': {}", parts[1], e))?;
388 let balance = Unit::ETHER.wei().saturating_mul(U256::from(amount));
389 accounts.insert(address, balance);
390 }
391 Ok(accounts)
392 }
393
394 fn account_generator(&self) -> AccountGenerator {
395 let mut generator = AccountGenerator::new(self.accounts as usize)
396 .phrase(DEFAULT_MNEMONIC)
397 .chain_id(self.evm.chain_id.unwrap_or(CHAIN_ID.into()));
398 if let Some(ref mnemonic) = self.mnemonic {
399 generator = generator.phrase(mnemonic);
400 } else if let Some(count) = self.mnemonic_random {
401 let mut rng = rand_08::thread_rng();
402 let mnemonic = match Mnemonic::<English>::new_with_count(&mut rng, count) {
403 Ok(mnemonic) => mnemonic.to_phrase(),
404 Err(err) => {
405 warn!(target: "node", ?count, %err, "failed to generate mnemonic, falling back to 12-word random mnemonic");
406 Mnemonic::<English>::new_with_count(&mut rng, 12)
409 .expect("valid default word count")
410 .to_phrase()
411 }
412 };
413 generator = generator.phrase(mnemonic);
414 } else if let Some(seed) = self.mnemonic_seed {
415 let mut seed = StdRng::seed_from_u64(seed);
416 let mnemonic = Mnemonic::<English>::new(&mut seed).to_phrase();
417 generator = generator.phrase(mnemonic);
418 }
419 if let Some(ref derivation) = self.derivation_path {
420 generator = generator.derivation_path(derivation);
421 }
422 generator
423 }
424
425 pub async fn run(self) -> eyre::Result<()> {
429 let dump_state =
430 self.dump_state.as_ref().or_else(|| self.state.as_ref().map(|s| &s.path)).cloned();
431 let dump_interval =
432 self.state_interval.map(Duration::from_secs).unwrap_or(DEFAULT_DUMP_INTERVAL);
433 let preserve_historical_states = self.preserve_historical_states;
434
435 let (api, mut handle) = crate::try_spawn(self.into_node_config()?).await?;
436
437 let mut fork = api.get_fork();
439 let running = Arc::new(AtomicUsize::new(0));
440
441 let mut signal = handle.shutdown_signal_mut().take();
444
445 let task_manager = handle.task_manager();
446 let mut on_shutdown = task_manager.on_shutdown();
447
448 let mut state_dumper =
449 PeriodicStateDumper::new(api, dump_state, dump_interval, preserve_historical_states);
450
451 task_manager.spawn(async move {
452 #[cfg(unix)]
454 let mut sigterm = Box::pin(async {
455 if let Ok(mut stream) =
456 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
457 {
458 stream.recv().await;
459 } else {
460 futures::future::pending::<()>().await;
461 }
462 });
463
464 #[cfg(not(unix))]
466 let mut sigterm = Box::pin(futures::future::pending::<()>());
467
468 tokio::select! {
470 _ = &mut sigterm => {
471 trace!("received sigterm signal, shutting down");
472 }
473 _ = &mut on_shutdown => {}
474 _ = &mut state_dumper => {}
475 }
476
477 state_dumper.dump().await;
479
480 if let Some(fork) = fork.take() {
483 trace!("flushing cache on shutdown");
484 fork.database
485 .read()
486 .await
487 .maybe_flush_cache()
488 .expect("Could not flush cache on fork DB");
489 }
492 std::process::exit(0);
493 });
494
495 ctrlc::set_handler(move || {
496 let prev = running.fetch_add(1, Ordering::SeqCst);
497 if prev == 0 {
498 trace!("received shutdown signal, shutting down");
499 let _ = signal.take();
500 }
501 })
502 .expect("Error setting Ctrl-C handler");
503
504 Ok(handle.await??)
505 }
506}
507
508#[derive(Clone, Debug, Parser)]
510#[command(next_help_heading = "EVM options")]
511pub struct AnvilEvmArgs {
512 #[arg(
520 long,
521 short,
522 visible_alias = "rpc-url",
523 value_name = "URL",
524 help_heading = "Fork config"
525 )]
526 pub fork_url: Vec<ForkUrl>,
527
528 #[arg(
532 long = "fork-header",
533 value_name = "HEADERS",
534 help_heading = "Fork config",
535 requires = "fork_url"
536 )]
537 pub fork_headers: Vec<String>,
538
539 #[arg(id = "timeout", long = "timeout", help_heading = "Fork config", requires = "fork_url")]
543 pub fork_request_timeout: Option<u64>,
544
545 #[arg(id = "retries", long = "retries", help_heading = "Fork config", requires = "fork_url")]
549 pub fork_request_retries: Option<u32>,
550
551 #[arg(
557 long,
558 requires = "fork_url",
559 value_name = "BLOCK",
560 help_heading = "Fork config",
561 allow_hyphen_values = true
562 )]
563 pub fork_block_number: Option<i128>,
564
565 #[arg(
569 long,
570 requires = "fork_url",
571 value_name = "TRANSACTION",
572 help_heading = "Fork config",
573 conflicts_with = "fork_block_number"
574 )]
575 pub fork_transaction_hash: Option<B256>,
576
577 #[arg(long, requires = "fork_url", value_name = "BACKOFF", help_heading = "Fork config")]
581 pub fork_retry_backoff: Option<u64>,
582
583 #[arg(
589 long,
590 help_heading = "Fork config",
591 value_name = "CHAIN",
592 requires = "fork_block_number"
593 )]
594 pub fork_chain_id: Option<Chain>,
595
596 #[arg(long, requires = "fork_url", help_heading = "Fork config")]
602 pub no_fork_node_info: bool,
603
604 #[arg(long, help_heading = "Fork config")]
610 pub no_bal: bool,
611
612 #[arg(long, requires = "fork_url", help_heading = "Fork config")]
620 pub fork_state_by_number: bool,
621
622 #[arg(
628 long,
629 requires = "fork_url",
630 alias = "cups",
631 value_name = "CUPS",
632 help_heading = "Fork config"
633 )]
634 pub compute_units_per_second: Option<u64>,
635
636 #[arg(
642 long,
643 requires = "fork_url",
644 value_name = "NO_RATE_LIMITS",
645 help_heading = "Fork config",
646 visible_alias = "no-rpc-rate-limit"
647 )]
648 pub no_rate_limit: bool,
649
650 #[arg(long, requires = "fork_url", help_heading = "Fork config")]
658 pub no_storage_caching: bool,
659
660 #[arg(long, alias = "block-gas-limit", help_heading = "Environment config")]
662 pub gas_limit: Option<u64>,
663
664 #[arg(
666 long,
667 value_name = "DISABLE_GAS_LIMIT",
668 help_heading = "Environment config",
669 alias = "disable-gas-limit",
670 conflicts_with = "gas_limit"
671 )]
672 pub disable_block_gas_limit: bool,
673
674 #[arg(long, visible_alias = "tx-gas-limit", help_heading = "Environment config")]
676 pub enable_tx_gas_limit: bool,
677
678 #[arg(long, value_name = "CODE_SIZE", help_heading = "Environment config")]
681 pub code_size_limit: Option<usize>,
682
683 #[arg(
685 long,
686 value_name = "DISABLE_CODE_SIZE_LIMIT",
687 conflicts_with = "code_size_limit",
688 help_heading = "Environment config"
689 )]
690 pub disable_code_size_limit: bool,
691
692 #[arg(long, help_heading = "Environment config")]
694 pub gas_price: Option<u128>,
695
696 #[arg(
698 long,
699 visible_alias = "base-fee",
700 value_name = "FEE",
701 help_heading = "Environment config"
702 )]
703 pub block_base_fee_per_gas: Option<u64>,
704
705 #[arg(long, visible_alias = "no-priority-fee", help_heading = "Environment config")]
707 pub disable_min_priority_fee: bool,
708
709 #[arg(long, alias = "chain", help_heading = "Environment config")]
711 pub chain_id: Option<Chain>,
712
713 #[arg(long, visible_alias = "tracing")]
719 pub steps_tracing: bool,
720
721 #[arg(long, visible_alias = "no-console-log")]
723 pub disable_console_log: bool,
724
725 #[arg(long, visible_alias = "enable-trace-printing")]
727 pub print_traces: bool,
728
729 #[arg(long, visible_alias = "auto-unlock")]
732 pub auto_impersonate: bool,
733
734 #[arg(long, visible_alias = "no-create2")]
736 pub disable_default_create2_deployer: bool,
737
738 #[arg(long)]
740 pub disable_pool_balance_checks: bool,
741
742 #[arg(long)]
744 pub memory_limit: Option<u64>,
745
746 #[command(flatten)]
747 pub networks: NetworkConfigs,
748}
749
750impl AnvilEvmArgs {
757 pub fn resolve_rpc_alias(&mut self) {
758 if let Ok(config) = Config::load_with_providers(FigmentProviders::Anvil) {
759 let mut resolved_urls = Vec::new();
760 for fork_url in &self.fork_url {
761 let mut endpoints = config.rpc_endpoints.clone().resolved();
762 if let Some(endpoint) = endpoints.remove(&fork_url.url) {
763 match endpoint.all_urls() {
765 Ok(urls) => {
766 for (i, url) in urls.into_iter().enumerate() {
767 resolved_urls.push(ForkUrl {
768 url,
769 block: if i == 0 { fork_url.block } else { None },
771 });
772 }
773 }
774 Err(e) => {
775 warn!(target: "node", alias=%fork_url.url, %e, "could not resolve all endpoints, using primary endpoint only");
776 if let Ok(url) = endpoint.url() {
777 resolved_urls.push(ForkUrl { url, block: fork_url.block });
778 } else {
779 resolved_urls.push(fork_url.clone());
780 }
781 }
782 }
783 } else if let Some(Ok(url)) = config.get_rpc_url_with_alias(&fork_url.url) {
784 resolved_urls.push(ForkUrl { url: url.to_string(), block: fork_url.block });
786 } else {
787 resolved_urls.push(fork_url.clone());
789 }
790 }
791 self.fork_url = resolved_urls;
792 }
793 }
794}
795
796struct PeriodicStateDumper<N: Network> {
798 in_progress_dump: Option<Pin<Box<dyn Future<Output = ()> + Send + Sync + 'static>>>,
799 api: EthApi<N>,
800 dump_state: Option<PathBuf>,
801 preserve_historical_states: bool,
802 interval: Interval,
803}
804
805impl<N: Network<ReceiptEnvelope = FoundryReceiptEnvelope>> PeriodicStateDumper<N> {
806 fn new(
807 api: EthApi<N>,
808 dump_state: Option<PathBuf>,
809 interval: Duration,
810 preserve_historical_states: bool,
811 ) -> Self {
812 let dump_state = dump_state.map(|mut dump_state| {
813 if dump_state.is_dir() {
814 dump_state = dump_state.join("state.json");
815 }
816 dump_state
817 });
818
819 let interval = tokio::time::interval_at(Instant::now() + interval, interval);
821 Self { in_progress_dump: None, api, dump_state, preserve_historical_states, interval }
822 }
823
824 async fn dump(mut self) {
825 self.in_progress_dump = None;
827 if let Some(state) = self.dump_state {
828 Self::dump_state(self.api, state, self.preserve_historical_states).await
829 }
830 }
831
832 async fn dump_state(api: EthApi<N>, dump_state: PathBuf, preserve_historical_states: bool) {
834 trace!(path=?dump_state, "Dumping state on shutdown");
835 match api.serialized_state(preserve_historical_states).await {
836 Ok(state) => {
837 if let Err(err) = foundry_common::fs::write_json_file(&dump_state, &state) {
838 error!(?err, "Failed to dump state");
839 } else {
840 trace!(path=?dump_state, "Dumped state on shutdown");
841 }
842 }
843 Err(err) => {
844 error!(?err, "Failed to extract state");
845 }
846 }
847 }
848}
849
850impl<N: Network<ReceiptEnvelope = FoundryReceiptEnvelope>> Future for PeriodicStateDumper<N> {
852 type Output = ();
853
854 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
855 let this = self.get_mut();
856 if this.dump_state.is_none() {
857 return Poll::Pending;
858 }
859
860 loop {
861 if let Some(mut flush) = this.in_progress_dump.take() {
862 match flush.poll_unpin(cx) {
863 Poll::Ready(_) => {
864 this.interval.reset();
865 }
866 Poll::Pending => {
867 this.in_progress_dump = Some(flush);
868 return Poll::Pending;
869 }
870 }
871 }
872
873 if this.interval.poll_tick(cx).is_ready() {
874 let api = this.api.clone();
875 let path = this.dump_state.clone().expect("exists; see above");
876 this.in_progress_dump =
877 Some(Box::pin(Self::dump_state(api, path, this.preserve_historical_states)));
878 } else {
879 break;
880 }
881 }
882
883 Poll::Pending
884 }
885}
886
887#[derive(Clone, Debug)]
889pub struct StateFile {
890 pub path: PathBuf,
891 pub state: Option<SerializableState>,
892}
893
894impl StateFile {
895 fn parse(path: &str) -> Result<Self, String> {
898 Self::parse_path(path)
899 }
900
901 pub fn parse_path(path: impl AsRef<Path>) -> Result<Self, String> {
903 let mut path = path.as_ref().to_path_buf();
904 if path.is_dir() {
905 path = path.join("state.json");
906 }
907 let mut state = Self { path, state: None };
908 if !state.path.exists() {
909 return Ok(state);
910 }
911
912 state.state = Some(SerializableState::load(&state.path).map_err(|err| err.to_string())?);
913
914 Ok(state)
915 }
916}
917
918#[derive(Clone, Debug, PartialEq, Eq)]
921pub struct ForkUrl {
922 pub url: String,
924 pub block: Option<u64>,
926}
927
928impl fmt::Display for ForkUrl {
929 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
930 self.url.fmt(f)?;
931 if let Some(block) = self.block {
932 write!(f, "@{block}")?;
933 }
934 Ok(())
935 }
936}
937
938impl FromStr for ForkUrl {
939 type Err = String;
940
941 fn from_str(s: &str) -> Result<Self, Self::Err> {
942 if let Some((url, block)) = s.rsplit_once('@') {
943 if block == "latest" {
944 return Ok(Self { url: url.to_string(), block: None });
945 }
946 if !block.is_empty() && !block.contains(':') && !block.contains('.') {
948 let block: u64 = block
949 .parse()
950 .map_err(|_| format!("Failed to parse block number: `{block}`"))?;
951 return Ok(Self { url: url.to_string(), block: Some(block) });
952 }
953 }
954 Ok(Self { url: s.to_string(), block: None })
955 }
956}
957
958fn parse_hardfork(hf: &str, networks: &NetworkConfigs) -> eyre::Result<FoundryHardfork> {
960 if let Ok(hardfork) = FoundryHardfork::from_str(hf) {
961 networks.normalize_for_hardfork(hardfork).map_err(eyre::Report::msg)?;
962 return Ok(hardfork);
963 }
964
965 networks.execution_network().parse_hardfork(hf).map_err(eyre::Report::msg)
966}
967
968fn read_genesis_file(path: &str) -> Result<Genesis, String> {
970 foundry_common::fs::read_json_file(path.as_ref()).map_err(|err| err.to_string())
971}
972
973fn duration_from_secs_f64(s: &str) -> Result<Duration, String> {
974 let s = s.parse::<f64>().map_err(|e| e.to_string())?;
975 if s == 0.0 {
976 return Err("Duration must be greater than 0".to_string());
977 }
978 Duration::try_from_secs_f64(s).map_err(|e| e.to_string())
979}
980
981#[cfg(test)]
982mod tests {
983 use super::*;
984 use foundry_evm::hardfork::EthereumHardfork;
985 use std::{env, net::Ipv4Addr};
986 use tempo_hardfork::TempoHardfork;
987
988 #[cfg(feature = "base")]
989 use foundry_evm::hardforks::BaseUpgrade;
990
991 #[cfg(feature = "optimism")]
992 use foundry_evm::hardfork::OpHardfork;
993
994 #[tokio::test]
995 async fn final_dump_cancels_suspended_periodic_dump() {
996 for queued_for_mining in [false, true] {
997 let (api, _handle) = crate::spawn(NodeConfig::test().with_no_mining(true)).await;
998 let dir = tempfile::tempdir().unwrap();
999 let path = dir.path().join("state.json");
1000 let mining =
1001 if queued_for_mining { Some(api.backend.lock_mining().await) } else { None };
1002 let db = api.backend.get_db().write().await;
1003 let mut dumper = PeriodicStateDumper::new(
1004 api.clone(),
1005 Some(path.clone()),
1006 Duration::from_secs(60),
1007 false,
1008 );
1009 dumper.in_progress_dump =
1010 Some(Box::pin(PeriodicStateDumper::dump_state(api.clone(), path.clone(), false)));
1011 assert!(futures::poll!(&mut dumper).is_pending());
1012
1013 drop(db);
1015 drop(mining);
1016 tokio::time::timeout(Duration::from_secs(2), dumper.dump())
1017 .await
1018 .expect("final dump waited for a suspended periodic dump");
1019 let state = foundry_common::fs::read_json_file::<SerializableState>(&path).unwrap();
1020 assert_eq!(state.best_block_number, Some(0));
1021 }
1022 }
1023
1024 #[cfg(feature = "base")]
1025 #[test]
1026 fn base_chain_ids_select_native_base_unless_overridden() {
1027 for chain_id in ["8453", "84532"] {
1028 let config =
1029 NodeArgs::parse_from(["anvil", "--chain-id", chain_id]).into_node_config().unwrap();
1030 assert!(config.networks.is_base());
1031
1032 let config =
1033 NodeArgs::parse_from(["anvil", "--chain-id", chain_id, "--network", "ethereum"])
1034 .into_node_config()
1035 .unwrap();
1036 assert!(config.networks.execution_network().is_ethereum());
1037 }
1038 }
1039
1040 #[test]
1041 fn fork_bal_can_be_disabled() {
1042 let args =
1043 NodeArgs::try_parse_from(["anvil", "--fork-url", "http://localhost:8545", "--no-bal"]);
1044 let config = args.unwrap().into_node_config().unwrap();
1045 assert!(config.no_bal);
1046 assert!(!NodeArgs::parse_from(["anvil"]).into_node_config().unwrap().no_bal);
1047 assert!(NodeArgs::parse_from(["anvil", "--no-bal"]).into_node_config().unwrap().no_bal);
1048 }
1049
1050 #[test]
1051 fn test_parse_fork_url() {
1052 let fork: ForkUrl = "http://localhost:8545@1000000".parse().unwrap();
1053 assert_eq!(
1054 fork,
1055 ForkUrl { url: "http://localhost:8545".to_string(), block: Some(1000000) }
1056 );
1057
1058 let fork: ForkUrl = "http://localhost:8545".parse().unwrap();
1059 assert_eq!(fork, ForkUrl { url: "http://localhost:8545".to_string(), block: None });
1060
1061 let fork: ForkUrl = "wss://user:password@example.com/".parse().unwrap();
1062 assert_eq!(
1063 fork,
1064 ForkUrl { url: "wss://user:password@example.com/".to_string(), block: None }
1065 );
1066
1067 let fork: ForkUrl = "wss://user:password@example.com/@latest".parse().unwrap();
1068 assert_eq!(
1069 fork,
1070 ForkUrl { url: "wss://user:password@example.com/".to_string(), block: None }
1071 );
1072
1073 let fork: ForkUrl = "wss://user:password@example.com/@100000".parse().unwrap();
1074 assert_eq!(
1075 fork,
1076 ForkUrl { url: "wss://user:password@example.com/".to_string(), block: Some(100000) }
1077 );
1078 }
1079
1080 #[test]
1081 fn can_parse_ethereum_hardfork() {
1082 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--hardfork", "berlin"]);
1083 let config = args.into_node_config().unwrap();
1084 assert_eq!(config.hardfork, Some(EthereumHardfork::Berlin.into()));
1085 }
1086
1087 #[cfg(feature = "optimism")]
1088 #[test]
1089 fn can_parse_optimism_hardfork() {
1090 let args: NodeArgs =
1091 NodeArgs::parse_from(["anvil", "--optimism", "--hardfork", "Regolith"]);
1092 let config = args.into_node_config().unwrap();
1093 assert_eq!(config.hardfork, Some(OpHardfork::Regolith.into()));
1094 }
1095
1096 #[test]
1097 fn can_parse_tempo_hardfork_from_network() {
1098 let args: NodeArgs =
1099 NodeArgs::parse_from(["anvil", "--network", "tempo", "--hardfork", "T5"]);
1100 let config = args.into_node_config().unwrap();
1101
1102 assert!(config.networks.is_tempo());
1103 assert_eq!(config.hardfork, Some(TempoHardfork::T5.into()));
1104 }
1105
1106 #[test]
1107 fn can_parse_namespaced_tempo_hardfork() {
1108 let args = NodeArgs::parse_from(["anvil", "--hardfork", "tempo:T5"]);
1109 let config = args.into_node_config().unwrap();
1110
1111 assert!(config.networks.is_tempo());
1112 assert_eq!(config.hardfork, Some(TempoHardfork::T5.into()));
1113 }
1114
1115 #[cfg(feature = "base")]
1116 #[test]
1117 fn can_parse_base_hardfork_from_network() {
1118 let args: NodeArgs =
1119 NodeArgs::parse_from(["anvil", "--network", "base", "--hardfork", "Beryl"]);
1120 let config = args.into_node_config().unwrap();
1121
1122 assert!(config.networks.is_base());
1123 assert_eq!(config.hardfork, Some(BaseUpgrade::Beryl.into()));
1124 }
1125
1126 #[cfg(feature = "base")]
1127 #[test]
1128 fn can_parse_namespaced_base_hardfork() {
1129 let args = NodeArgs::parse_from(["anvil", "--hardfork", "base:Beryl"]);
1130 let config = args.into_node_config().unwrap();
1131
1132 assert!(config.networks.is_base());
1133 assert_eq!(config.hardfork, Some(BaseUpgrade::Beryl.into()));
1134 }
1135
1136 #[cfg(feature = "base")]
1137 #[test]
1138 fn can_parse_base_activation_admin() {
1139 let admin = Address::repeat_byte(0xaa);
1140 let admin_arg = admin.to_string();
1141 let args = NodeArgs::parse_from([
1142 "anvil",
1143 "--network",
1144 "base",
1145 "--hardfork",
1146 "base:Beryl",
1147 "--base-activation-admin",
1148 admin_arg.as_str(),
1149 ]);
1150 let config = args.into_node_config().unwrap();
1151
1152 assert!(config.networks.is_base());
1153 assert_eq!(config.base_activation_admin, Some(admin));
1154 }
1155
1156 #[cfg(feature = "optimism")]
1157 #[test]
1158 fn chain_id_infers_optimism_network_in_node_config() {
1159 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--chain-id", "10"]);
1160 let config = args.into_node_config().unwrap();
1161
1162 assert!(config.networks.is_optimism());
1163 }
1164
1165 #[test]
1166 #[cfg(not(feature = "optimism"))]
1167 fn chain_id_rejects_disabled_optimism_network() {
1168 let args = NodeArgs::parse_from(["anvil", "--chain-id", "10"]);
1169 let error = args.into_node_config().unwrap_err();
1170
1171 assert_eq!(
1172 error.to_string(),
1173 "cannot infer execution network from chain ID 10: network family `optimism` is not \
1174 enabled in this build"
1175 );
1176 }
1177
1178 #[test]
1179 #[cfg(feature = "base")]
1180 fn base_fork_chain_id_precedes_execution_chain_id_for_network() {
1181 let args = NodeArgs::parse_from([
1182 "anvil",
1183 "--fork-url",
1184 "http://localhost:8545",
1185 "--fork-block-number",
1186 "1",
1187 "--fork-chain-id",
1188 "8453",
1189 "--chain-id",
1190 "1",
1191 ]);
1192 let config = args.into_node_config().unwrap();
1193
1194 assert!(config.networks.is_base());
1195 assert_eq!(config.get_chain_id(), 1);
1196 }
1197
1198 #[test]
1201 #[cfg(all(not(feature = "base"), feature = "optimism"))]
1202 fn chain_id_without_base_still_resolves_to_optimism() {
1203 for chain_id in ["8453", "84532"] {
1204 let args = NodeArgs::parse_from(["anvil", "--chain-id", chain_id]);
1205 let config = args
1206 .into_node_config()
1207 .unwrap_or_else(|error| panic!("chain ID {chain_id} must still resolve: {error}"));
1208 assert!(config.networks.is_optimism(), "chain ID {chain_id} must resolve to Optimism");
1209 }
1210 }
1211
1212 #[test]
1213 #[cfg(not(feature = "monad"))]
1214 fn chain_id_rejects_disabled_monad_network() {
1215 for chain_id in ["143", "10143"] {
1216 let args = NodeArgs::parse_from(["anvil", "--chain-id", chain_id]);
1217 let error = args.into_node_config().unwrap_err();
1218
1219 assert_eq!(
1220 error.to_string(),
1221 format!(
1222 "cannot infer execution network from chain ID {chain_id}: network family \
1223 `monad` is not enabled in this build"
1224 )
1225 );
1226 }
1227 }
1228
1229 #[test]
1230 fn explicit_ethereum_allows_base_chain_id() {
1231 let args = NodeArgs::parse_from(["anvil", "--network", "ethereum", "--chain-id", "8453"]);
1232 let config = args.into_node_config().unwrap();
1233
1234 assert_eq!(config.networks, NetworkConfigs::with_ethereum());
1235 assert_eq!(config.get_chain_id(), 8453);
1236 }
1237
1238 #[test]
1239 #[cfg(not(feature = "monad"))]
1240 fn explicit_ethereum_allows_monad_chain_id() {
1241 let args = NodeArgs::parse_from(["anvil", "--network", "ethereum", "--chain-id", "143"]);
1242 let config = args.into_node_config().unwrap();
1243
1244 assert_eq!(config.networks, NetworkConfigs::with_ethereum());
1245 assert_eq!(config.get_chain_id(), 143);
1246 }
1247
1248 #[test]
1249 #[cfg(not(feature = "monad"))]
1250 fn genesis_chain_id_rejects_disabled_monad_network() {
1251 let mut args = NodeArgs::parse_from(["anvil"]);
1252 let mut genesis = Genesis::default();
1253 genesis.config.chain_id = 143;
1254 args.init = Some(genesis);
1255
1256 let error = args.into_node_config().unwrap_err();
1257
1258 assert_eq!(
1259 error.to_string(),
1260 "cannot infer execution network from chain ID 143: network family `monad` is not \
1261 enabled in this build"
1262 );
1263 }
1264
1265 #[test]
1266 #[cfg(feature = "base")]
1267 fn genesis_chain_id_infers_base_network() {
1268 let mut args = NodeArgs::parse_from(["anvil"]);
1269 let mut genesis = Genesis::default();
1270 genesis.config.chain_id = 8453;
1271 args.init = Some(genesis);
1272
1273 let config = args.into_node_config().unwrap();
1274 assert!(config.networks.is_base());
1275 }
1276
1277 #[test]
1280 #[cfg(all(not(feature = "base"), feature = "optimism"))]
1281 fn genesis_chain_id_without_base_still_infers_optimism() {
1282 let mut args = NodeArgs::parse_from(["anvil"]);
1283 let mut genesis = Genesis::default();
1284 genesis.config.chain_id = 8453;
1285 args.init = Some(genesis);
1286
1287 let config = args.into_node_config().unwrap();
1288 assert!(config.networks.is_optimism());
1289 }
1290
1291 #[test]
1292 #[cfg(not(feature = "monad"))]
1293 fn explicit_network_overrides_genesis_chain_id_inference() {
1294 let mut args = NodeArgs::parse_from(["anvil", "--network", "ethereum"]);
1295 let mut genesis = Genesis::default();
1296 genesis.config.chain_id = 143;
1297 args.init = Some(genesis);
1298
1299 let config = args.into_node_config().unwrap();
1300
1301 assert_eq!(config.networks, NetworkConfigs::with_ethereum());
1302 assert_eq!(config.get_chain_id(), 143);
1303 }
1304
1305 #[test]
1306 #[cfg(not(feature = "monad"))]
1307 fn explicit_chain_id_precedes_genesis_network_inference() {
1308 let mut args = NodeArgs::parse_from(["anvil", "--chain-id", "1"]);
1309 let mut genesis = Genesis::default();
1310 genesis.config.chain_id = 143;
1311 args.init = Some(genesis);
1312
1313 let config = args.into_node_config().unwrap();
1314
1315 assert!(!config.networks.has_network_selection());
1316 assert_eq!(config.get_chain_id(), 1);
1317 }
1318
1319 #[test]
1320 fn explicit_network_overrides_base_genesis_chain_id_inference() {
1321 let mut args = NodeArgs::parse_from(["anvil", "--network", "ethereum"]);
1322 let mut genesis = Genesis::default();
1323 genesis.config.chain_id = 8453;
1324 args.init = Some(genesis);
1325
1326 let config = args.into_node_config().unwrap();
1327 assert_eq!(config.networks, NetworkConfigs::with_ethereum());
1328 assert_eq!(config.get_chain_id(), 8453);
1329 }
1330
1331 #[test]
1332 fn explicit_chain_id_precedes_base_genesis_network_inference() {
1333 let mut args = NodeArgs::parse_from(["anvil", "--chain-id", "1"]);
1334 let mut genesis = Genesis::default();
1335 genesis.config.chain_id = 8453;
1336 args.init = Some(genesis);
1337
1338 let config = args.into_node_config().unwrap();
1339 assert!(!config.networks.has_network_selection());
1340 assert_eq!(config.get_chain_id(), 1);
1341 }
1342
1343 #[test]
1344 #[cfg(not(feature = "monad"))]
1345 fn disabled_fork_chain_id_precedes_execution_chain_id() {
1346 let args = NodeArgs::parse_from([
1347 "anvil",
1348 "--fork-url",
1349 "http://localhost:8545",
1350 "--fork-block-number",
1351 "1",
1352 "--fork-chain-id",
1353 "10143",
1354 "--chain-id",
1355 "1",
1356 ]);
1357 let error = args.into_node_config().unwrap_err();
1358
1359 assert!(error.to_string().contains(
1360 "cannot infer execution network from chain ID 10143: network family `monad` is not \
1361 enabled in this build"
1362 ));
1363 }
1364
1365 #[test]
1366 fn fork_state_by_number_flag_sets_config() {
1367 let args = NodeArgs::parse_from([
1368 "anvil",
1369 "--fork-url",
1370 "http://localhost:8545",
1371 "--fork-state-by-number",
1372 ]);
1373 let config = args.into_node_config().unwrap();
1374
1375 assert!(config.fork_state_by_number);
1376 }
1377
1378 #[test]
1379 fn unknown_chain_id_preserves_ethereum_fallback() {
1380 let args = NodeArgs::parse_from(["anvil", "--chain-id", "98765432"]);
1381 let config = args.into_node_config().unwrap();
1382
1383 assert!(!config.networks.has_network_selection());
1384 assert_eq!(config.get_chain_id(), 98_765_432);
1385 }
1386
1387 #[test]
1388 fn chain_id_infers_tempo_network_for_hardfork() {
1389 let args = NodeArgs::parse_from(["anvil", "--chain-id", "4217", "--hardfork", "T5"]);
1390 let config = args.into_node_config().unwrap();
1391
1392 assert!(config.networks.is_tempo());
1393 assert_eq!(config.hardfork, Some(TempoHardfork::T5.into()));
1394 }
1395
1396 #[test]
1397 fn fork_chain_id_infers_tempo_network_for_hardfork() {
1398 let args = NodeArgs::parse_from([
1399 "anvil",
1400 "--fork-url",
1401 "http://localhost:8545",
1402 "--fork-block-number",
1403 "1",
1404 "--fork-chain-id",
1405 "4217",
1406 "--hardfork",
1407 "T5",
1408 ]);
1409 let config = args.into_node_config().unwrap();
1410
1411 assert!(config.networks.is_tempo());
1412 assert_eq!(config.hardfork, Some(TempoHardfork::T5.into()));
1413 }
1414
1415 #[test]
1416 #[cfg(feature = "monad")]
1417 fn chain_id_infers_monad_network_in_node_config() {
1418 let args = NodeArgs::parse_from(["anvil", "--chain-id", "143"]);
1419 let config = args.into_node_config().unwrap();
1420
1421 assert!(config.networks.is_monad());
1422 }
1423
1424 #[test]
1425 #[cfg(feature = "monad")]
1426 fn fork_chain_id_precedes_execution_chain_id_for_network() {
1427 let args = NodeArgs::parse_from([
1428 "anvil",
1429 "--fork-url",
1430 "http://localhost:8545",
1431 "--fork-block-number",
1432 "1",
1433 "--fork-chain-id",
1434 "10143",
1435 "--chain-id",
1436 "1",
1437 ]);
1438 let config = args.into_node_config().unwrap();
1439
1440 assert!(config.networks.is_monad());
1441 assert_eq!(config.get_chain_id(), 1);
1442 }
1443
1444 #[test]
1445 fn fork_execution_chain_id_does_not_infer_source_network() {
1446 let args = NodeArgs::parse_from([
1447 "anvil",
1448 "--fork-url",
1449 "http://localhost:8545",
1450 "--chain-id",
1451 "4217",
1452 ]);
1453 let config = args.into_node_config().unwrap();
1454
1455 assert!(config.networks.resolved_network().is_none());
1456 assert_eq!(config.get_chain_id(), 4217);
1457 }
1458
1459 #[test]
1460 #[cfg(feature = "monad")]
1461 fn can_parse_monad_hardfork() {
1462 let args: NodeArgs =
1463 NodeArgs::parse_from(["anvil", "--network", "monad", "--hardfork", "MonadNine"]);
1464 let config = args.into_node_config().unwrap();
1465 assert_eq!(config.hardfork, Some(foundry_evm::hardfork::MonadHardfork::MonadNine.into()));
1466 assert!(config.networks.is_monad());
1467 }
1468
1469 #[test]
1470 #[cfg(feature = "monad")]
1471 fn monad_uses_monad_default_hardfork() {
1472 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--network", "monad"]);
1473 let config = args.into_node_config().unwrap();
1474 assert_eq!(config.hardfork, None);
1475 assert_eq!(config.get_hardfork(), foundry_evm::hardfork::MonadHardfork::default().into());
1476 assert!(config.networks.is_monad());
1477 }
1478
1479 #[test]
1480 fn cant_parse_invalid_hardfork() {
1481 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--hardfork", "Regolith"]);
1482 let config = args.into_node_config();
1483 assert!(config.is_err());
1484 }
1485
1486 #[test]
1487 fn can_parse_fork_headers() {
1488 let args: NodeArgs = NodeArgs::parse_from([
1489 "anvil",
1490 "--fork-url",
1491 "http,://localhost:8545",
1492 "--fork-header",
1493 "User-Agent: test-agent",
1494 "--fork-header",
1495 "Referrer: example.com",
1496 ]);
1497 assert_eq!(args.evm.fork_headers, vec!["User-Agent: test-agent", "Referrer: example.com"]);
1498 }
1499
1500 #[test]
1501 fn can_parse_prune_config() {
1502 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--prune-history"]);
1503 assert!(args.prune_history.is_some());
1504
1505 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--prune-history", "100"]);
1506 assert_eq!(args.prune_history, Some(Some(100)));
1507 }
1508
1509 #[test]
1510 fn can_parse_max_persisted_states_config() {
1511 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--max-persisted-states", "500"]);
1512 assert_eq!(args.max_persisted_states, (Some(500)));
1513 }
1514
1515 #[test]
1516 fn can_parse_disable_block_gas_limit() {
1517 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--disable-block-gas-limit"]);
1518 assert!(args.evm.disable_block_gas_limit);
1519
1520 let args =
1521 NodeArgs::try_parse_from(["anvil", "--disable-block-gas-limit", "--gas-limit", "100"]);
1522 assert!(args.is_err());
1523 }
1524
1525 #[test]
1526 fn can_parse_enable_tx_gas_limit() {
1527 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--enable-tx-gas-limit"]);
1528 assert!(args.evm.enable_tx_gas_limit);
1529
1530 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--tx-gas-limit"]);
1532 assert!(args.evm.enable_tx_gas_limit);
1533 }
1534
1535 #[test]
1536 fn can_parse_disable_code_size_limit() {
1537 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--disable-code-size-limit"]);
1538 assert!(args.evm.disable_code_size_limit);
1539
1540 let args = NodeArgs::try_parse_from([
1541 "anvil",
1542 "--disable-code-size-limit",
1543 "--code-size-limit",
1544 "100",
1545 ]);
1546 assert!(args.is_err());
1548 }
1549
1550 #[test]
1551 fn can_parse_host() {
1552 let args = NodeArgs::parse_from(["anvil"]);
1553 assert_eq!(args.host, vec![IpAddr::V4(Ipv4Addr::LOCALHOST)]);
1554
1555 let args = NodeArgs::parse_from([
1556 "anvil", "--host", "::1", "--host", "1.1.1.1", "--host", "2.2.2.2",
1557 ]);
1558 assert_eq!(
1559 args.host,
1560 ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::<IpAddr>().unwrap()).to_vec()
1561 );
1562
1563 let args = NodeArgs::parse_from(["anvil", "--host", "::1,1.1.1.1,2.2.2.2"]);
1564 assert_eq!(
1565 args.host,
1566 ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::<IpAddr>().unwrap()).to_vec()
1567 );
1568
1569 unsafe { env::set_var("ANVIL_IP_ADDR", "1.1.1.1") };
1570 let args = NodeArgs::parse_from(["anvil"]);
1571 assert_eq!(args.host, vec!["1.1.1.1".parse::<IpAddr>().unwrap()]);
1572
1573 unsafe { env::set_var("ANVIL_IP_ADDR", "::1,1.1.1.1,2.2.2.2") };
1574 let args = NodeArgs::parse_from(["anvil"]);
1575 assert_eq!(
1576 args.host,
1577 ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::<IpAddr>().unwrap()).to_vec()
1578 );
1579 }
1580
1581 #[test]
1582 fn can_parse_no_fork_node_info() {
1583 let args = NodeArgs::parse_from([
1584 "anvil",
1585 "--fork-url",
1586 "http://localhost:8545",
1587 "--no-fork-node-info",
1588 ]);
1589 assert!(args.evm.no_fork_node_info);
1590 let config = args.into_node_config().unwrap();
1591 assert!(config.no_fork_node_info);
1592 }
1593
1594 #[test]
1595 fn can_parse_multiple_fork_urls() {
1596 let args: NodeArgs = NodeArgs::parse_from([
1597 "anvil",
1598 "--fork-url",
1599 "http://localhost:8545",
1600 "--fork-url",
1601 "http://localhost:8546",
1602 "--fork-url",
1603 "http://localhost:8547",
1604 ]);
1605 assert_eq!(args.evm.fork_url.len(), 3);
1606 assert_eq!(args.evm.fork_url[0].url, "http://localhost:8545");
1607 assert_eq!(args.evm.fork_url[1].url, "http://localhost:8546");
1608 assert_eq!(args.evm.fork_url[2].url, "http://localhost:8547");
1609
1610 let args: NodeArgs = NodeArgs::parse_from([
1612 "anvil",
1613 "--fork-url",
1614 "http://localhost:8545@1000000",
1615 "--fork-url",
1616 "http://localhost:8546",
1617 ]);
1618 assert_eq!(args.evm.fork_url[0].block, Some(1000000));
1619 assert_eq!(args.evm.fork_url[1].block, None);
1620 }
1621
1622 #[test]
1623 fn rejects_block_suffix_on_secondary_fork_urls() {
1624 let args: NodeArgs = NodeArgs::parse_from([
1625 "anvil",
1626 "--fork-url",
1627 "http://localhost:8545@1000000",
1628 "--fork-url",
1629 "http://localhost:8546@2000000",
1630 ]);
1631 let result = args.into_node_config();
1632 assert!(result.is_err());
1633 assert!(
1634 result.unwrap_err().to_string().contains("Block number suffixes"),
1635 "should reject block suffix on secondary fork URL"
1636 );
1637 }
1638
1639 #[test]
1640 fn fork_dependent_args_require_fork_url() {
1641 let cases = [
1643 vec!["anvil", "--fork-header", "X-Api-Key: test"],
1644 vec!["anvil", "--timeout", "5000"],
1645 vec!["anvil", "--retries", "3"],
1646 vec!["anvil", "--fork-block-number", "100"],
1647 vec!["anvil", "--fork-retry-backoff", "500"],
1648 vec!["anvil", "--no-fork-node-info"],
1649 ];
1650 for args in &cases {
1651 let result = NodeArgs::try_parse_from(args);
1652 assert!(result.is_err(), "expected error when using {:?} without --fork-url", args[1]);
1653 }
1654 }
1655
1656 #[test]
1657 fn transaction_coalescing_window_cli() {
1658 for (args, expected) in [
1659 (vec!["anvil"], 5),
1660 (vec!["anvil", "--transaction-coalescing-window", "0"], 0),
1661 (vec!["anvil", "--transaction-coalescing-window", "20"], 20),
1662 ] {
1663 let config = NodeArgs::parse_from(args).into_node_config().unwrap();
1664 assert_eq!(config.transaction_coalescing_window, Duration::from_millis(expected));
1665 }
1666 assert!(
1667 NodeArgs::try_parse_from(["anvil", "--transaction-coalescing-window", "-1"]).is_err()
1668 );
1669 }
1670}