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 #[arg(short, long, visible_alias = "blockTime", value_name = "SECONDS", value_parser = duration_from_secs_f64)]
99 pub block_time: Option<Duration>,
100
101 #[arg(long, value_name = "SLOTS_IN_AN_EPOCH", default_value_t = DEFAULT_SLOTS_IN_AN_EPOCH)]
103 pub slots_in_an_epoch: u64,
104
105 #[arg(long, value_name = "FILE", value_hint = clap::ValueHint::FilePath)]
107 pub config_out: Option<PathBuf>,
108
109 #[arg(long, visible_alias = "no-mine", conflicts_with = "block_time")]
111 pub no_mining: bool,
112
113 #[arg(long, requires = "block_time")]
116 pub mixed_mining: bool,
117
118 #[arg(
120 long,
121 value_name = "IP_ADDR",
122 env = "ANVIL_IP_ADDR",
123 default_value = "127.0.0.1",
124 help_heading = "Server options",
125 value_delimiter = ','
126 )]
127 pub host: Vec<IpAddr>,
128
129 #[arg(long, default_value = "fees")]
131 pub order: TransactionOrder,
132
133 #[arg(long, value_name = "PATH", value_parser= read_genesis_file)]
135 pub init: Option<Genesis>,
136
137 #[arg(
142 long,
143 value_name = "PATH",
144 value_parser = StateFile::parse,
145 conflicts_with_all = &[
146 "init",
147 "dump_state",
148 "load_state"
149 ]
150 )]
151 pub state: Option<StateFile>,
152
153 #[arg(short, long, value_name = "SECONDS")]
157 pub state_interval: Option<u64>,
158
159 #[arg(long, value_name = "PATH", conflicts_with = "init")]
163 pub dump_state: Option<PathBuf>,
164
165 #[arg(long, conflicts_with = "init", default_value = "false")]
172 pub preserve_historical_states: bool,
173
174 #[arg(
176 long,
177 value_name = "PATH",
178 value_parser = SerializableState::parse,
179 conflicts_with = "init"
180 )]
181 pub load_state: Option<SerializableState>,
182
183 #[arg(long, value_name = "ADDRESS:AMOUNT", value_delimiter = ' ', num_args = 1..)]
188 pub fund_accounts: Vec<String>,
189
190 #[arg(long, help = IPC_HELP, value_name = "PATH", visible_alias = "ipcpath")]
191 pub ipc: Option<Option<String>>,
192
193 #[arg(long)]
198 pub prune_history: Option<Option<usize>>,
199
200 #[arg(long, conflicts_with = "prune_history")]
204 pub max_persisted_states: Option<usize>,
205
206 #[arg(long)]
208 pub transaction_block_keeper: Option<usize>,
209
210 #[arg(long)]
212 pub max_transactions: Option<usize>,
213
214 #[command(flatten)]
215 pub evm: AnvilEvmArgs,
216
217 #[command(flatten)]
218 pub server_config: ServerConfig,
219
220 #[arg(long, value_name = "PATH")]
226 pub cache_path: Option<PathBuf>,
227}
228
229#[cfg(windows)]
230const IPC_HELP: &str =
231 "Launch an ipc server at the given path or default path = `\\.\\pipe\\anvil.ipc`";
232
233#[cfg(not(windows))]
235const IPC_HELP: &str = "Launch an ipc server at the given path or default path = `/tmp/anvil.ipc`";
236
237const DEFAULT_DUMP_INTERVAL: Duration = Duration::from_secs(60);
239
240impl NodeArgs {
241 pub fn into_node_config(self) -> eyre::Result<NodeConfig> {
242 let genesis_balance = Unit::ETHER.wei().saturating_mul(U256::from(self.balance));
243 let compute_units_per_second =
244 if self.evm.no_rate_limit { Some(u64::MAX) } else { self.evm.compute_units_per_second };
245
246 if self.evm.fork_url.len() > 1 {
248 for fork in &self.evm.fork_url[1..] {
249 if fork.block.is_some() {
250 eyre::bail!(
251 "Block number suffixes (@block) on secondary --fork-url values are not supported. \
252 Use --fork-block-number to set the fork block for all endpoints."
253 );
254 }
255 }
256 }
257
258 let funded_accounts = self.parse_funded_accounts()?;
259
260 let local_chain_id = self
261 .evm
262 .chain_id
263 .map(u64::from)
264 .or_else(|| self.init.as_ref().map(|genesis| genesis.config.chain_id));
265 let inferred_network_chain_id = self
266 .evm
267 .fork_chain_id
268 .map(u64::from)
269 .or(if self.evm.fork_url.is_empty() { local_chain_id } else { None });
270 let networks = if let Some(chain_id) = inferred_network_chain_id {
271 self.evm.networks.try_with_chain_id(chain_id).map_err(eyre::Report::msg)?
272 } else {
273 self.evm.networks
274 };
275
276 let hardfork = match &self.hardfork {
277 Some(hf) => Some(parse_hardfork(hf, &networks)?),
278 None => None,
279 };
280 let networks = if let Some(hardfork) = hardfork {
281 networks.normalize_for_hardfork(hardfork).map_err(eyre::Report::msg)?
282 } else {
283 networks
284 };
285
286 Ok(NodeConfig::default()
287 .with_gas_limit(self.evm.gas_limit)
288 .disable_block_gas_limit(self.evm.disable_block_gas_limit)
289 .enable_tx_gas_limit(self.evm.enable_tx_gas_limit)
290 .with_gas_price(self.evm.gas_price)
291 .with_hardfork(hardfork)
292 .with_blocktime(self.block_time)
293 .with_no_mining(self.no_mining)
294 .with_mixed_mining(self.mixed_mining, self.block_time)
295 .with_account_generator(self.account_generator())?
296 .with_genesis_balance(genesis_balance)
297 .with_genesis_timestamp(self.timestamp)
298 .with_genesis_block_number(self.number)
299 .with_port(self.port)
300 .with_fork_choice(match (self.evm.fork_block_number, self.evm.fork_transaction_hash) {
301 (Some(block), None) => Some(ForkChoice::Block(block)),
302 (None, Some(hash)) => Some(ForkChoice::Transaction(hash)),
303 _ => self
304 .evm
305 .fork_url
306 .first()
307 .and_then(|f| f.block)
308 .map(|num| ForkChoice::Block(num as i128)),
309 })
310 .with_fork_headers(self.evm.fork_headers)
311 .with_fork_chain_id(self.evm.fork_chain_id.map(u64::from).map(U256::from))
312 .fork_request_timeout(self.evm.fork_request_timeout.map(Duration::from_millis))
313 .fork_request_retries(self.evm.fork_request_retries)
314 .fork_retry_backoff(self.evm.fork_retry_backoff.map(Duration::from_millis))
315 .fork_compute_units_per_second(compute_units_per_second)
316 .with_fork_urls(self.evm.fork_url.into_iter().map(|f| f.url).collect())
317 .with_base_fee(self.evm.block_base_fee_per_gas)
318 .disable_min_priority_fee(self.evm.disable_min_priority_fee)
319 .with_no_storage_caching(self.evm.no_storage_caching)
320 .with_server_config(self.server_config)
321 .with_host(self.host)
322 .set_silent(shell::is_quiet())
323 .set_config_out(self.config_out)
324 .with_transaction_order(self.order)
325 .with_genesis(self.init)
326 .with_steps_tracing(self.evm.steps_tracing)
327 .with_print_logs(!self.evm.disable_console_log)
328 .with_print_traces(self.evm.print_traces)
329 .with_auto_impersonate(self.evm.auto_impersonate)
330 .with_ipc(self.ipc)
331 .with_code_size_limit(self.evm.code_size_limit)
332 .disable_code_size_limit(self.evm.disable_code_size_limit)
333 .set_pruned_history(self.prune_history)
334 .with_init_state(self.load_state.or_else(|| self.state.and_then(|s| s.state)))
335 .with_transaction_block_keeper(self.transaction_block_keeper)
336 .with_max_transactions(self.max_transactions)
337 .with_max_persisted_states(self.max_persisted_states)
338 .with_chain_id(self.evm.chain_id)
339 .with_networks(networks)
342 .with_tempo_fee_payer(self.tempo_fee_payer)
343 .with_disable_default_create2_deployer(self.evm.disable_default_create2_deployer)
344 .with_disable_pool_balance_checks(self.evm.disable_pool_balance_checks)
345 .with_slots_in_an_epoch(self.slots_in_an_epoch)
346 .with_memory_limit(self.evm.memory_limit)
347 .with_cache_path(self.cache_path)
348 .with_funded_accounts(funded_accounts))
349 }
350
351 fn parse_funded_accounts(&self) -> eyre::Result<HashMap<Address, U256>> {
352 let mut accounts = HashMap::default();
353 for entry in &self.fund_accounts {
354 let parts: Vec<&str> = entry.split(':').collect();
355 if parts.len() != 2 {
356 eyre::bail!(
357 "Invalid fund-accounts entry '{}'. Expected format: ADDRESS:AMOUNT",
358 entry
359 );
360 }
361 let address = parts[0]
362 .parse::<Address>()
363 .map_err(|e| eyre::eyre!("Invalid address '{}': {}", parts[0], e))?;
364 let amount: u64 = parts[1]
365 .parse()
366 .map_err(|e| eyre::eyre!("Invalid amount '{}': {}", parts[1], e))?;
367 let balance = Unit::ETHER.wei().saturating_mul(U256::from(amount));
368 accounts.insert(address, balance);
369 }
370 Ok(accounts)
371 }
372
373 fn account_generator(&self) -> AccountGenerator {
374 let mut generator = AccountGenerator::new(self.accounts as usize)
375 .phrase(DEFAULT_MNEMONIC)
376 .chain_id(self.evm.chain_id.unwrap_or(CHAIN_ID.into()));
377 if let Some(ref mnemonic) = self.mnemonic {
378 generator = generator.phrase(mnemonic);
379 } else if let Some(count) = self.mnemonic_random {
380 let mut rng = rand_08::thread_rng();
381 let mnemonic = match Mnemonic::<English>::new_with_count(&mut rng, count) {
382 Ok(mnemonic) => mnemonic.to_phrase(),
383 Err(err) => {
384 warn!(target: "node", ?count, %err, "failed to generate mnemonic, falling back to 12-word random mnemonic");
385 Mnemonic::<English>::new_with_count(&mut rng, 12)
388 .expect("valid default word count")
389 .to_phrase()
390 }
391 };
392 generator = generator.phrase(mnemonic);
393 } else if let Some(seed) = self.mnemonic_seed {
394 let mut seed = StdRng::seed_from_u64(seed);
395 let mnemonic = Mnemonic::<English>::new(&mut seed).to_phrase();
396 generator = generator.phrase(mnemonic);
397 }
398 if let Some(ref derivation) = self.derivation_path {
399 generator = generator.derivation_path(derivation);
400 }
401 generator
402 }
403
404 pub async fn run(self) -> eyre::Result<()> {
408 let dump_state =
409 self.dump_state.as_ref().or_else(|| self.state.as_ref().map(|s| &s.path)).cloned();
410 let dump_interval =
411 self.state_interval.map(Duration::from_secs).unwrap_or(DEFAULT_DUMP_INTERVAL);
412 let preserve_historical_states = self.preserve_historical_states;
413
414 let (api, mut handle) = crate::try_spawn(self.into_node_config()?).await?;
415
416 let mut fork = api.get_fork();
418 let running = Arc::new(AtomicUsize::new(0));
419
420 let mut signal = handle.shutdown_signal_mut().take();
423
424 let task_manager = handle.task_manager();
425 let mut on_shutdown = task_manager.on_shutdown();
426
427 let mut state_dumper =
428 PeriodicStateDumper::new(api, dump_state, dump_interval, preserve_historical_states);
429
430 task_manager.spawn(async move {
431 #[cfg(unix)]
433 let mut sigterm = Box::pin(async {
434 if let Ok(mut stream) =
435 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
436 {
437 stream.recv().await;
438 } else {
439 futures::future::pending::<()>().await;
440 }
441 });
442
443 #[cfg(not(unix))]
445 let mut sigterm = Box::pin(futures::future::pending::<()>());
446
447 tokio::select! {
449 _ = &mut sigterm => {
450 trace!("received sigterm signal, shutting down");
451 }
452 _ = &mut on_shutdown => {}
453 _ = &mut state_dumper => {}
454 }
455
456 state_dumper.dump().await;
458
459 if let Some(fork) = fork.take() {
462 trace!("flushing cache on shutdown");
463 fork.database
464 .read()
465 .await
466 .maybe_flush_cache()
467 .expect("Could not flush cache on fork DB");
468 }
471 std::process::exit(0);
472 });
473
474 ctrlc::set_handler(move || {
475 let prev = running.fetch_add(1, Ordering::SeqCst);
476 if prev == 0 {
477 trace!("received shutdown signal, shutting down");
478 let _ = signal.take();
479 }
480 })
481 .expect("Error setting Ctrl-C handler");
482
483 Ok(handle.await??)
484 }
485}
486
487#[derive(Clone, Debug, Parser)]
489#[command(next_help_heading = "EVM options")]
490pub struct AnvilEvmArgs {
491 #[arg(
499 long,
500 short,
501 visible_alias = "rpc-url",
502 value_name = "URL",
503 help_heading = "Fork config"
504 )]
505 pub fork_url: Vec<ForkUrl>,
506
507 #[arg(
511 long = "fork-header",
512 value_name = "HEADERS",
513 help_heading = "Fork config",
514 requires = "fork_url"
515 )]
516 pub fork_headers: Vec<String>,
517
518 #[arg(id = "timeout", long = "timeout", help_heading = "Fork config", requires = "fork_url")]
522 pub fork_request_timeout: Option<u64>,
523
524 #[arg(id = "retries", long = "retries", help_heading = "Fork config", requires = "fork_url")]
528 pub fork_request_retries: Option<u32>,
529
530 #[arg(
536 long,
537 requires = "fork_url",
538 value_name = "BLOCK",
539 help_heading = "Fork config",
540 allow_hyphen_values = true
541 )]
542 pub fork_block_number: Option<i128>,
543
544 #[arg(
548 long,
549 requires = "fork_url",
550 value_name = "TRANSACTION",
551 help_heading = "Fork config",
552 conflicts_with = "fork_block_number"
553 )]
554 pub fork_transaction_hash: Option<B256>,
555
556 #[arg(long, requires = "fork_url", value_name = "BACKOFF", help_heading = "Fork config")]
560 pub fork_retry_backoff: Option<u64>,
561
562 #[arg(
568 long,
569 help_heading = "Fork config",
570 value_name = "CHAIN",
571 requires = "fork_block_number"
572 )]
573 pub fork_chain_id: Option<Chain>,
574
575 #[arg(
581 long,
582 requires = "fork_url",
583 alias = "cups",
584 value_name = "CUPS",
585 help_heading = "Fork config"
586 )]
587 pub compute_units_per_second: Option<u64>,
588
589 #[arg(
595 long,
596 requires = "fork_url",
597 value_name = "NO_RATE_LIMITS",
598 help_heading = "Fork config",
599 visible_alias = "no-rpc-rate-limit"
600 )]
601 pub no_rate_limit: bool,
602
603 #[arg(long, requires = "fork_url", help_heading = "Fork config")]
611 pub no_storage_caching: bool,
612
613 #[arg(long, alias = "block-gas-limit", help_heading = "Environment config")]
615 pub gas_limit: Option<u64>,
616
617 #[arg(
619 long,
620 value_name = "DISABLE_GAS_LIMIT",
621 help_heading = "Environment config",
622 alias = "disable-gas-limit",
623 conflicts_with = "gas_limit"
624 )]
625 pub disable_block_gas_limit: bool,
626
627 #[arg(long, visible_alias = "tx-gas-limit", help_heading = "Environment config")]
629 pub enable_tx_gas_limit: bool,
630
631 #[arg(long, value_name = "CODE_SIZE", help_heading = "Environment config")]
634 pub code_size_limit: Option<usize>,
635
636 #[arg(
638 long,
639 value_name = "DISABLE_CODE_SIZE_LIMIT",
640 conflicts_with = "code_size_limit",
641 help_heading = "Environment config"
642 )]
643 pub disable_code_size_limit: bool,
644
645 #[arg(long, help_heading = "Environment config")]
647 pub gas_price: Option<u128>,
648
649 #[arg(
651 long,
652 visible_alias = "base-fee",
653 value_name = "FEE",
654 help_heading = "Environment config"
655 )]
656 pub block_base_fee_per_gas: Option<u64>,
657
658 #[arg(long, visible_alias = "no-priority-fee", help_heading = "Environment config")]
660 pub disable_min_priority_fee: bool,
661
662 #[arg(long, alias = "chain", help_heading = "Environment config")]
664 pub chain_id: Option<Chain>,
665
666 #[arg(long, visible_alias = "tracing")]
668 pub steps_tracing: bool,
669
670 #[arg(long, visible_alias = "no-console-log")]
672 pub disable_console_log: bool,
673
674 #[arg(long, visible_alias = "enable-trace-printing")]
676 pub print_traces: bool,
677
678 #[arg(long, visible_alias = "auto-unlock")]
681 pub auto_impersonate: bool,
682
683 #[arg(long, visible_alias = "no-create2")]
685 pub disable_default_create2_deployer: bool,
686
687 #[arg(long)]
689 pub disable_pool_balance_checks: bool,
690
691 #[arg(long)]
693 pub memory_limit: Option<u64>,
694
695 #[command(flatten)]
696 pub networks: NetworkConfigs,
697}
698
699impl AnvilEvmArgs {
706 pub fn resolve_rpc_alias(&mut self) {
707 if let Ok(config) = Config::load_with_providers(FigmentProviders::Anvil) {
708 let mut resolved_urls = Vec::new();
709 for fork_url in &self.fork_url {
710 let mut endpoints = config.rpc_endpoints.clone().resolved();
711 if let Some(endpoint) = endpoints.remove(&fork_url.url) {
712 match endpoint.all_urls() {
714 Ok(urls) => {
715 for (i, url) in urls.into_iter().enumerate() {
716 resolved_urls.push(ForkUrl {
717 url,
718 block: if i == 0 { fork_url.block } else { None },
720 });
721 }
722 }
723 Err(e) => {
724 warn!(target: "node", alias=%fork_url.url, %e, "could not resolve all endpoints, using primary endpoint only");
725 if let Ok(url) = endpoint.url() {
726 resolved_urls.push(ForkUrl { url, block: fork_url.block });
727 } else {
728 resolved_urls.push(fork_url.clone());
729 }
730 }
731 }
732 } else if let Some(Ok(url)) = config.get_rpc_url_with_alias(&fork_url.url) {
733 resolved_urls.push(ForkUrl { url: url.to_string(), block: fork_url.block });
735 } else {
736 resolved_urls.push(fork_url.clone());
738 }
739 }
740 self.fork_url = resolved_urls;
741 }
742 }
743}
744
745struct PeriodicStateDumper<N: Network> {
747 in_progress_dump: Option<Pin<Box<dyn Future<Output = ()> + Send + Sync + 'static>>>,
748 api: EthApi<N>,
749 dump_state: Option<PathBuf>,
750 preserve_historical_states: bool,
751 interval: Interval,
752}
753
754impl<N: Network<ReceiptEnvelope = FoundryReceiptEnvelope>> PeriodicStateDumper<N> {
755 fn new(
756 api: EthApi<N>,
757 dump_state: Option<PathBuf>,
758 interval: Duration,
759 preserve_historical_states: bool,
760 ) -> Self {
761 let dump_state = dump_state.map(|mut dump_state| {
762 if dump_state.is_dir() {
763 dump_state = dump_state.join("state.json");
764 }
765 dump_state
766 });
767
768 let interval = tokio::time::interval_at(Instant::now() + interval, interval);
770 Self { in_progress_dump: None, api, dump_state, preserve_historical_states, interval }
771 }
772
773 async fn dump(&self) {
774 if let Some(state) = self.dump_state.clone() {
775 Self::dump_state(self.api.clone(), state, self.preserve_historical_states).await
776 }
777 }
778
779 async fn dump_state(api: EthApi<N>, dump_state: PathBuf, preserve_historical_states: bool) {
781 trace!(path=?dump_state, "Dumping state on shutdown");
782 match api.serialized_state(preserve_historical_states).await {
783 Ok(state) => {
784 if let Err(err) = foundry_common::fs::write_json_file(&dump_state, &state) {
785 error!(?err, "Failed to dump state");
786 } else {
787 trace!(path=?dump_state, "Dumped state on shutdown");
788 }
789 }
790 Err(err) => {
791 error!(?err, "Failed to extract state");
792 }
793 }
794 }
795}
796
797impl<N: Network<ReceiptEnvelope = FoundryReceiptEnvelope>> Future for PeriodicStateDumper<N> {
799 type Output = ();
800
801 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
802 let this = self.get_mut();
803 if this.dump_state.is_none() {
804 return Poll::Pending;
805 }
806
807 loop {
808 if let Some(mut flush) = this.in_progress_dump.take() {
809 match flush.poll_unpin(cx) {
810 Poll::Ready(_) => {
811 this.interval.reset();
812 }
813 Poll::Pending => {
814 this.in_progress_dump = Some(flush);
815 return Poll::Pending;
816 }
817 }
818 }
819
820 if this.interval.poll_tick(cx).is_ready() {
821 let api = this.api.clone();
822 let path = this.dump_state.clone().expect("exists; see above");
823 this.in_progress_dump =
824 Some(Box::pin(Self::dump_state(api, path, this.preserve_historical_states)));
825 } else {
826 break;
827 }
828 }
829
830 Poll::Pending
831 }
832}
833
834#[derive(Clone, Debug)]
836pub struct StateFile {
837 pub path: PathBuf,
838 pub state: Option<SerializableState>,
839}
840
841impl StateFile {
842 fn parse(path: &str) -> Result<Self, String> {
845 Self::parse_path(path)
846 }
847
848 pub fn parse_path(path: impl AsRef<Path>) -> Result<Self, String> {
850 let mut path = path.as_ref().to_path_buf();
851 if path.is_dir() {
852 path = path.join("state.json");
853 }
854 let mut state = Self { path, state: None };
855 if !state.path.exists() {
856 return Ok(state);
857 }
858
859 state.state = Some(SerializableState::load(&state.path).map_err(|err| err.to_string())?);
860
861 Ok(state)
862 }
863}
864
865#[derive(Clone, Debug, PartialEq, Eq)]
868pub struct ForkUrl {
869 pub url: String,
871 pub block: Option<u64>,
873}
874
875impl fmt::Display for ForkUrl {
876 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
877 self.url.fmt(f)?;
878 if let Some(block) = self.block {
879 write!(f, "@{block}")?;
880 }
881 Ok(())
882 }
883}
884
885impl FromStr for ForkUrl {
886 type Err = String;
887
888 fn from_str(s: &str) -> Result<Self, Self::Err> {
889 if let Some((url, block)) = s.rsplit_once('@') {
890 if block == "latest" {
891 return Ok(Self { url: url.to_string(), block: None });
892 }
893 if !block.is_empty() && !block.contains(':') && !block.contains('.') {
895 let block: u64 = block
896 .parse()
897 .map_err(|_| format!("Failed to parse block number: `{block}`"))?;
898 return Ok(Self { url: url.to_string(), block: Some(block) });
899 }
900 }
901 Ok(Self { url: s.to_string(), block: None })
902 }
903}
904
905fn parse_hardfork(hf: &str, networks: &NetworkConfigs) -> eyre::Result<FoundryHardfork> {
907 if let Ok(hardfork) = FoundryHardfork::from_str(hf) {
908 networks.normalize_for_hardfork(hardfork).map_err(eyre::Report::msg)?;
909 return Ok(hardfork);
910 }
911
912 networks.execution_network().parse_hardfork(hf).map_err(eyre::Report::msg)
913}
914
915fn read_genesis_file(path: &str) -> Result<Genesis, String> {
917 foundry_common::fs::read_json_file(path.as_ref()).map_err(|err| err.to_string())
918}
919
920fn duration_from_secs_f64(s: &str) -> Result<Duration, String> {
921 let s = s.parse::<f64>().map_err(|e| e.to_string())?;
922 if s == 0.0 {
923 return Err("Duration must be greater than 0".to_string());
924 }
925 Duration::try_from_secs_f64(s).map_err(|e| e.to_string())
926}
927
928#[cfg(test)]
929mod tests {
930 use super::*;
931 use foundry_evm::hardfork::EthereumHardfork;
932 #[cfg(feature = "optimism")]
933 use foundry_evm::hardfork::OpHardfork;
934 use std::{env, net::Ipv4Addr};
935 use tempo_hardfork::TempoHardfork;
936
937 #[test]
938 fn test_parse_fork_url() {
939 let fork: ForkUrl = "http://localhost:8545@1000000".parse().unwrap();
940 assert_eq!(
941 fork,
942 ForkUrl { url: "http://localhost:8545".to_string(), block: Some(1000000) }
943 );
944
945 let fork: ForkUrl = "http://localhost:8545".parse().unwrap();
946 assert_eq!(fork, ForkUrl { url: "http://localhost:8545".to_string(), block: None });
947
948 let fork: ForkUrl = "wss://user:password@example.com/".parse().unwrap();
949 assert_eq!(
950 fork,
951 ForkUrl { url: "wss://user:password@example.com/".to_string(), block: None }
952 );
953
954 let fork: ForkUrl = "wss://user:password@example.com/@latest".parse().unwrap();
955 assert_eq!(
956 fork,
957 ForkUrl { url: "wss://user:password@example.com/".to_string(), block: None }
958 );
959
960 let fork: ForkUrl = "wss://user:password@example.com/@100000".parse().unwrap();
961 assert_eq!(
962 fork,
963 ForkUrl { url: "wss://user:password@example.com/".to_string(), block: Some(100000) }
964 );
965 }
966
967 #[test]
968 fn can_parse_ethereum_hardfork() {
969 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--hardfork", "berlin"]);
970 let config = args.into_node_config().unwrap();
971 assert_eq!(config.hardfork, Some(EthereumHardfork::Berlin.into()));
972 }
973
974 #[cfg(feature = "optimism")]
975 #[test]
976 fn can_parse_optimism_hardfork() {
977 let args: NodeArgs =
978 NodeArgs::parse_from(["anvil", "--optimism", "--hardfork", "Regolith"]);
979 let config = args.into_node_config().unwrap();
980 assert_eq!(config.hardfork, Some(OpHardfork::Regolith.into()));
981 }
982
983 #[test]
984 fn can_parse_tempo_hardfork_from_network() {
985 let args: NodeArgs =
986 NodeArgs::parse_from(["anvil", "--network", "tempo", "--hardfork", "T5"]);
987 let config = args.into_node_config().unwrap();
988
989 assert!(config.networks.is_tempo());
990 assert_eq!(config.hardfork, Some(TempoHardfork::T5.into()));
991 }
992
993 #[test]
994 fn can_parse_namespaced_tempo_hardfork() {
995 let args = NodeArgs::parse_from(["anvil", "--hardfork", "tempo:T5"]);
996 let config = args.into_node_config().unwrap();
997
998 assert!(config.networks.is_tempo());
999 assert_eq!(config.hardfork, Some(TempoHardfork::T5.into()));
1000 }
1001
1002 #[cfg(feature = "optimism")]
1003 #[test]
1004 fn chain_id_infers_optimism_network_in_node_config() {
1005 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--chain-id", "10"]);
1006 let config = args.into_node_config().unwrap();
1007
1008 assert!(config.networks.is_optimism());
1009 }
1010
1011 #[test]
1012 #[cfg(not(feature = "optimism"))]
1013 fn chain_id_rejects_disabled_optimism_network() {
1014 let args = NodeArgs::parse_from(["anvil", "--chain-id", "10"]);
1015 let error = args.into_node_config().unwrap_err();
1016
1017 assert_eq!(
1018 error.to_string(),
1019 "cannot infer execution network from chain ID 10: network family `optimism` is not \
1020 enabled in this build"
1021 );
1022 }
1023
1024 #[test]
1025 #[cfg(not(feature = "monad"))]
1026 fn chain_id_rejects_disabled_monad_network() {
1027 for chain_id in ["143", "10143"] {
1028 let args = NodeArgs::parse_from(["anvil", "--chain-id", chain_id]);
1029 let error = args.into_node_config().unwrap_err();
1030
1031 assert_eq!(
1032 error.to_string(),
1033 format!(
1034 "cannot infer execution network from chain ID {chain_id}: network family \
1035 `monad` is not enabled in this build"
1036 )
1037 );
1038 }
1039 }
1040
1041 #[test]
1042 #[cfg(not(feature = "monad"))]
1043 fn explicit_ethereum_allows_monad_chain_id() {
1044 let args = NodeArgs::parse_from(["anvil", "--network", "ethereum", "--chain-id", "143"]);
1045 let config = args.into_node_config().unwrap();
1046
1047 assert_eq!(config.networks, NetworkConfigs::with_ethereum());
1048 assert_eq!(config.get_chain_id(), 143);
1049 }
1050
1051 #[test]
1052 #[cfg(not(feature = "monad"))]
1053 fn genesis_chain_id_rejects_disabled_monad_network() {
1054 let mut args = NodeArgs::parse_from(["anvil"]);
1055 let mut genesis = Genesis::default();
1056 genesis.config.chain_id = 143;
1057 args.init = Some(genesis);
1058
1059 let error = args.into_node_config().unwrap_err();
1060
1061 assert_eq!(
1062 error.to_string(),
1063 "cannot infer execution network from chain ID 143: network family `monad` is not \
1064 enabled in this build"
1065 );
1066 }
1067
1068 #[test]
1069 #[cfg(not(feature = "monad"))]
1070 fn explicit_network_overrides_genesis_chain_id_inference() {
1071 let mut args = NodeArgs::parse_from(["anvil", "--network", "ethereum"]);
1072 let mut genesis = Genesis::default();
1073 genesis.config.chain_id = 143;
1074 args.init = Some(genesis);
1075
1076 let config = args.into_node_config().unwrap();
1077
1078 assert_eq!(config.networks, NetworkConfigs::with_ethereum());
1079 assert_eq!(config.get_chain_id(), 143);
1080 }
1081
1082 #[test]
1083 #[cfg(not(feature = "monad"))]
1084 fn explicit_chain_id_precedes_genesis_network_inference() {
1085 let mut args = NodeArgs::parse_from(["anvil", "--chain-id", "1"]);
1086 let mut genesis = Genesis::default();
1087 genesis.config.chain_id = 143;
1088 args.init = Some(genesis);
1089
1090 let config = args.into_node_config().unwrap();
1091
1092 assert!(!config.networks.has_network_selection());
1093 assert_eq!(config.get_chain_id(), 1);
1094 }
1095
1096 #[test]
1097 #[cfg(not(feature = "monad"))]
1098 fn disabled_fork_chain_id_precedes_execution_chain_id() {
1099 let args = NodeArgs::parse_from([
1100 "anvil",
1101 "--fork-url",
1102 "http://localhost:8545",
1103 "--fork-block-number",
1104 "1",
1105 "--fork-chain-id",
1106 "10143",
1107 "--chain-id",
1108 "1",
1109 ]);
1110 let error = args.into_node_config().unwrap_err();
1111
1112 assert!(error.to_string().contains(
1113 "cannot infer execution network from chain ID 10143: network family `monad` is not \
1114 enabled in this build"
1115 ));
1116 }
1117
1118 #[test]
1119 fn unknown_chain_id_preserves_ethereum_fallback() {
1120 let args = NodeArgs::parse_from(["anvil", "--chain-id", "98765432"]);
1121 let config = args.into_node_config().unwrap();
1122
1123 assert!(!config.networks.has_network_selection());
1124 assert_eq!(config.get_chain_id(), 98_765_432);
1125 }
1126
1127 #[test]
1128 fn chain_id_infers_tempo_network_for_hardfork() {
1129 let args = NodeArgs::parse_from(["anvil", "--chain-id", "4217", "--hardfork", "T5"]);
1130 let config = args.into_node_config().unwrap();
1131
1132 assert!(config.networks.is_tempo());
1133 assert_eq!(config.hardfork, Some(TempoHardfork::T5.into()));
1134 }
1135
1136 #[test]
1137 fn fork_chain_id_infers_tempo_network_for_hardfork() {
1138 let args = NodeArgs::parse_from([
1139 "anvil",
1140 "--fork-url",
1141 "http://localhost:8545",
1142 "--fork-block-number",
1143 "1",
1144 "--fork-chain-id",
1145 "4217",
1146 "--hardfork",
1147 "T5",
1148 ]);
1149 let config = args.into_node_config().unwrap();
1150
1151 assert!(config.networks.is_tempo());
1152 assert_eq!(config.hardfork, Some(TempoHardfork::T5.into()));
1153 }
1154
1155 #[test]
1156 #[cfg(feature = "monad")]
1157 fn chain_id_infers_monad_network_in_node_config() {
1158 let args = NodeArgs::parse_from(["anvil", "--chain-id", "143"]);
1159 let config = args.into_node_config().unwrap();
1160
1161 assert!(config.networks.is_monad());
1162 }
1163
1164 #[test]
1165 #[cfg(feature = "monad")]
1166 fn fork_chain_id_precedes_execution_chain_id_for_network() {
1167 let args = NodeArgs::parse_from([
1168 "anvil",
1169 "--fork-url",
1170 "http://localhost:8545",
1171 "--fork-block-number",
1172 "1",
1173 "--fork-chain-id",
1174 "10143",
1175 "--chain-id",
1176 "1",
1177 ]);
1178 let config = args.into_node_config().unwrap();
1179
1180 assert!(config.networks.is_monad());
1181 assert_eq!(config.get_chain_id(), 1);
1182 }
1183
1184 #[test]
1185 fn fork_execution_chain_id_does_not_infer_source_network() {
1186 let args = NodeArgs::parse_from([
1187 "anvil",
1188 "--fork-url",
1189 "http://localhost:8545",
1190 "--chain-id",
1191 "4217",
1192 ]);
1193 let config = args.into_node_config().unwrap();
1194
1195 assert!(config.networks.resolved_network().is_none());
1196 assert_eq!(config.get_chain_id(), 4217);
1197 }
1198
1199 #[test]
1200 #[cfg(feature = "monad")]
1201 fn can_parse_monad_hardfork() {
1202 let args: NodeArgs =
1203 NodeArgs::parse_from(["anvil", "--network", "monad", "--hardfork", "MonadNine"]);
1204 let config = args.into_node_config().unwrap();
1205 assert_eq!(config.hardfork, Some(foundry_evm::hardfork::MonadHardfork::MonadNine.into()));
1206 assert!(config.networks.is_monad());
1207 }
1208
1209 #[test]
1210 #[cfg(feature = "monad")]
1211 fn monad_uses_monad_default_hardfork() {
1212 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--network", "monad"]);
1213 let config = args.into_node_config().unwrap();
1214 assert_eq!(config.hardfork, None);
1215 assert_eq!(config.get_hardfork(), foundry_evm::hardfork::MonadHardfork::default().into());
1216 assert!(config.networks.is_monad());
1217 }
1218
1219 #[test]
1220 fn cant_parse_invalid_hardfork() {
1221 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--hardfork", "Regolith"]);
1222 let config = args.into_node_config();
1223 assert!(config.is_err());
1224 }
1225
1226 #[test]
1227 fn can_parse_fork_headers() {
1228 let args: NodeArgs = NodeArgs::parse_from([
1229 "anvil",
1230 "--fork-url",
1231 "http,://localhost:8545",
1232 "--fork-header",
1233 "User-Agent: test-agent",
1234 "--fork-header",
1235 "Referrer: example.com",
1236 ]);
1237 assert_eq!(args.evm.fork_headers, vec!["User-Agent: test-agent", "Referrer: example.com"]);
1238 }
1239
1240 #[test]
1241 fn can_parse_prune_config() {
1242 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--prune-history"]);
1243 assert!(args.prune_history.is_some());
1244
1245 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--prune-history", "100"]);
1246 assert_eq!(args.prune_history, Some(Some(100)));
1247 }
1248
1249 #[test]
1250 fn can_parse_max_persisted_states_config() {
1251 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--max-persisted-states", "500"]);
1252 assert_eq!(args.max_persisted_states, (Some(500)));
1253 }
1254
1255 #[test]
1256 fn can_parse_disable_block_gas_limit() {
1257 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--disable-block-gas-limit"]);
1258 assert!(args.evm.disable_block_gas_limit);
1259
1260 let args =
1261 NodeArgs::try_parse_from(["anvil", "--disable-block-gas-limit", "--gas-limit", "100"]);
1262 assert!(args.is_err());
1263 }
1264
1265 #[test]
1266 fn can_parse_enable_tx_gas_limit() {
1267 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--enable-tx-gas-limit"]);
1268 assert!(args.evm.enable_tx_gas_limit);
1269
1270 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--tx-gas-limit"]);
1272 assert!(args.evm.enable_tx_gas_limit);
1273 }
1274
1275 #[test]
1276 fn can_parse_disable_code_size_limit() {
1277 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--disable-code-size-limit"]);
1278 assert!(args.evm.disable_code_size_limit);
1279
1280 let args = NodeArgs::try_parse_from([
1281 "anvil",
1282 "--disable-code-size-limit",
1283 "--code-size-limit",
1284 "100",
1285 ]);
1286 assert!(args.is_err());
1288 }
1289
1290 #[test]
1291 fn can_parse_host() {
1292 let args = NodeArgs::parse_from(["anvil"]);
1293 assert_eq!(args.host, vec![IpAddr::V4(Ipv4Addr::LOCALHOST)]);
1294
1295 let args = NodeArgs::parse_from([
1296 "anvil", "--host", "::1", "--host", "1.1.1.1", "--host", "2.2.2.2",
1297 ]);
1298 assert_eq!(
1299 args.host,
1300 ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::<IpAddr>().unwrap()).to_vec()
1301 );
1302
1303 let args = NodeArgs::parse_from(["anvil", "--host", "::1,1.1.1.1,2.2.2.2"]);
1304 assert_eq!(
1305 args.host,
1306 ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::<IpAddr>().unwrap()).to_vec()
1307 );
1308
1309 unsafe { env::set_var("ANVIL_IP_ADDR", "1.1.1.1") };
1310 let args = NodeArgs::parse_from(["anvil"]);
1311 assert_eq!(args.host, vec!["1.1.1.1".parse::<IpAddr>().unwrap()]);
1312
1313 unsafe { env::set_var("ANVIL_IP_ADDR", "::1,1.1.1.1,2.2.2.2") };
1314 let args = NodeArgs::parse_from(["anvil"]);
1315 assert_eq!(
1316 args.host,
1317 ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::<IpAddr>().unwrap()).to_vec()
1318 );
1319 }
1320
1321 #[test]
1322 fn can_parse_multiple_fork_urls() {
1323 let args: NodeArgs = NodeArgs::parse_from([
1324 "anvil",
1325 "--fork-url",
1326 "http://localhost:8545",
1327 "--fork-url",
1328 "http://localhost:8546",
1329 "--fork-url",
1330 "http://localhost:8547",
1331 ]);
1332 assert_eq!(args.evm.fork_url.len(), 3);
1333 assert_eq!(args.evm.fork_url[0].url, "http://localhost:8545");
1334 assert_eq!(args.evm.fork_url[1].url, "http://localhost:8546");
1335 assert_eq!(args.evm.fork_url[2].url, "http://localhost:8547");
1336
1337 let args: NodeArgs = NodeArgs::parse_from([
1339 "anvil",
1340 "--fork-url",
1341 "http://localhost:8545@1000000",
1342 "--fork-url",
1343 "http://localhost:8546",
1344 ]);
1345 assert_eq!(args.evm.fork_url[0].block, Some(1000000));
1346 assert_eq!(args.evm.fork_url[1].block, None);
1347 }
1348
1349 #[test]
1350 fn rejects_block_suffix_on_secondary_fork_urls() {
1351 let args: NodeArgs = NodeArgs::parse_from([
1352 "anvil",
1353 "--fork-url",
1354 "http://localhost:8545@1000000",
1355 "--fork-url",
1356 "http://localhost:8546@2000000",
1357 ]);
1358 let result = args.into_node_config();
1359 assert!(result.is_err());
1360 assert!(
1361 result.unwrap_err().to_string().contains("Block number suffixes"),
1362 "should reject block suffix on secondary fork URL"
1363 );
1364 }
1365
1366 #[test]
1367 fn fork_dependent_args_require_fork_url() {
1368 let cases = [
1370 vec!["anvil", "--fork-header", "X-Api-Key: test"],
1371 vec!["anvil", "--timeout", "5000"],
1372 vec!["anvil", "--retries", "3"],
1373 vec!["anvil", "--fork-block-number", "100"],
1374 vec!["anvil", "--fork-retry-backoff", "500"],
1375 ];
1376 for args in &cases {
1377 let result = NodeArgs::try_parse_from(args);
1378 assert!(result.is_err(), "expected error when using {:?} without --fork-url", args[1]);
1379 }
1380 }
1381}