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};
15#[cfg(feature = "optimism")]
16use foundry_evm::hardfork::OpHardfork;
17use foundry_evm::hardfork::{EthereumHardfork, FoundryHardfork};
18use foundry_evm_networks::NetworkConfigs;
19use foundry_primitives::FoundryReceiptEnvelope;
20use futures::FutureExt;
21use rand_08::{SeedableRng, rngs::StdRng};
22use std::{
23 net::IpAddr,
24 path::{Path, PathBuf},
25 pin::Pin,
26 str::FromStr,
27 sync::{
28 Arc,
29 atomic::{AtomicUsize, Ordering},
30 },
31 task::{Context, Poll},
32 time::Duration,
33};
34use tempo_chainspec::hardfork::TempoHardfork;
35use tokio::time::{Instant, Interval};
36
37#[derive(Clone, Debug, Parser)]
38pub struct NodeArgs {
39 #[arg(long, short, default_value = "8545", value_name = "NUM")]
41 pub port: u16,
42
43 #[arg(long, short, default_value = "10", value_name = "NUM")]
45 pub accounts: u64,
46
47 #[arg(long, default_value = "10000", value_name = "NUM")]
49 pub balance: u64,
50
51 #[arg(long, value_name = "NUM")]
53 pub timestamp: Option<u64>,
54
55 #[arg(long, value_name = "NUM")]
57 pub number: Option<u64>,
58
59 #[arg(long, short, conflicts_with_all = &["mnemonic_seed", "mnemonic_random"])]
62 pub mnemonic: Option<String>,
63
64 #[arg(long, conflicts_with_all = &["mnemonic", "mnemonic_seed"], default_missing_value = "12", num_args(0..=1))]
69 pub mnemonic_random: Option<usize>,
70
71 #[arg(long = "mnemonic-seed-unsafe", conflicts_with_all = &["mnemonic", "mnemonic_random"])]
77 pub mnemonic_seed: Option<u64>,
78
79 #[arg(long)]
83 pub derivation_path: Option<String>,
84
85 #[arg(long)]
90 pub hardfork: Option<String>,
91
92 #[arg(short, long, visible_alias = "blockTime", value_name = "SECONDS", value_parser = duration_from_secs_f64)]
94 pub block_time: Option<Duration>,
95
96 #[arg(long, value_name = "SLOTS_IN_AN_EPOCH", default_value_t = DEFAULT_SLOTS_IN_AN_EPOCH)]
98 pub slots_in_an_epoch: u64,
99
100 #[arg(long, value_name = "FILE", value_hint = clap::ValueHint::FilePath)]
102 pub config_out: Option<PathBuf>,
103
104 #[arg(long, visible_alias = "no-mine", conflicts_with = "block_time")]
106 pub no_mining: bool,
107
108 #[arg(long, requires = "block_time")]
109 pub mixed_mining: bool,
110
111 #[arg(
113 long,
114 value_name = "IP_ADDR",
115 env = "ANVIL_IP_ADDR",
116 default_value = "127.0.0.1",
117 help_heading = "Server options",
118 value_delimiter = ','
119 )]
120 pub host: Vec<IpAddr>,
121
122 #[arg(long, default_value = "fees")]
124 pub order: TransactionOrder,
125
126 #[arg(long, value_name = "PATH", value_parser= read_genesis_file)]
128 pub init: Option<Genesis>,
129
130 #[arg(
135 long,
136 value_name = "PATH",
137 value_parser = StateFile::parse,
138 conflicts_with_all = &[
139 "init",
140 "dump_state",
141 "load_state"
142 ]
143 )]
144 pub state: Option<StateFile>,
145
146 #[arg(short, long, value_name = "SECONDS")]
150 pub state_interval: Option<u64>,
151
152 #[arg(long, value_name = "PATH", conflicts_with = "init")]
156 pub dump_state: Option<PathBuf>,
157
158 #[arg(long, conflicts_with = "init", default_value = "false")]
165 pub preserve_historical_states: bool,
166
167 #[arg(
169 long,
170 value_name = "PATH",
171 value_parser = SerializableState::parse,
172 conflicts_with = "init"
173 )]
174 pub load_state: Option<SerializableState>,
175
176 #[arg(long, value_name = "ADDRESS:AMOUNT", value_delimiter = ' ', num_args = 1..)]
181 pub fund_accounts: Vec<String>,
182
183 #[arg(long, help = IPC_HELP, value_name = "PATH", visible_alias = "ipcpath")]
184 pub ipc: Option<Option<String>>,
185
186 #[arg(long)]
191 pub prune_history: Option<Option<usize>>,
192
193 #[arg(long, conflicts_with = "prune_history")]
197 pub max_persisted_states: Option<usize>,
198
199 #[arg(long)]
201 pub transaction_block_keeper: Option<usize>,
202
203 #[arg(long)]
205 pub max_transactions: Option<usize>,
206
207 #[command(flatten)]
208 pub evm: AnvilEvmArgs,
209
210 #[command(flatten)]
211 pub server_config: ServerConfig,
212
213 #[arg(long, value_name = "PATH")]
219 pub cache_path: Option<PathBuf>,
220}
221
222#[cfg(windows)]
223const IPC_HELP: &str =
224 "Launch an ipc server at the given path or default path = `\\.\\pipe\\anvil.ipc`";
225
226#[cfg(not(windows))]
228const IPC_HELP: &str = "Launch an ipc server at the given path or default path = `/tmp/anvil.ipc`";
229
230const DEFAULT_DUMP_INTERVAL: Duration = Duration::from_secs(60);
232
233impl NodeArgs {
234 pub fn into_node_config(self) -> eyre::Result<NodeConfig> {
235 let genesis_balance = Unit::ETHER.wei().saturating_mul(U256::from(self.balance));
236 let compute_units_per_second =
237 if self.evm.no_rate_limit { Some(u64::MAX) } else { self.evm.compute_units_per_second };
238
239 if self.evm.fork_url.len() > 1 {
241 for fork in &self.evm.fork_url[1..] {
242 if fork.block.is_some() {
243 eyre::bail!(
244 "Block number suffixes (@block) on secondary --fork-url values are not supported. \
245 Use --fork-block-number to set the fork block for all endpoints."
246 );
247 }
248 }
249 }
250
251 let funded_accounts = self.parse_funded_accounts()?;
252
253 let networks = self
254 .evm
255 .chain_id
256 .map(u64::from)
257 .or_else(|| self.evm.fork_chain_id.map(u64::from))
258 .map_or(self.evm.networks, |chain_id| self.evm.networks.with_chain_id(chain_id));
259
260 let hardfork = match &self.hardfork {
261 Some(hf) => Some(parse_hardfork(hf, &networks)?),
262 None => None,
263 };
264 let networks = if let Some(hardfork) = hardfork {
265 networks.normalize_for_hardfork(hardfork).map_err(eyre::Report::msg)?
266 } else {
267 networks
268 };
269
270 Ok(NodeConfig::default()
271 .with_gas_limit(self.evm.gas_limit)
272 .disable_block_gas_limit(self.evm.disable_block_gas_limit)
273 .enable_tx_gas_limit(self.evm.enable_tx_gas_limit)
274 .with_gas_price(self.evm.gas_price)
275 .with_hardfork(hardfork)
276 .with_blocktime(self.block_time)
277 .with_no_mining(self.no_mining)
278 .with_mixed_mining(self.mixed_mining, self.block_time)
279 .with_account_generator(self.account_generator())?
280 .with_genesis_balance(genesis_balance)
281 .with_genesis_timestamp(self.timestamp)
282 .with_genesis_block_number(self.number)
283 .with_port(self.port)
284 .with_fork_choice(match (self.evm.fork_block_number, self.evm.fork_transaction_hash) {
285 (Some(block), None) => Some(ForkChoice::Block(block)),
286 (None, Some(hash)) => Some(ForkChoice::Transaction(hash)),
287 _ => self
288 .evm
289 .fork_url
290 .first()
291 .and_then(|f| f.block)
292 .map(|num| ForkChoice::Block(num as i128)),
293 })
294 .with_fork_headers(self.evm.fork_headers)
295 .with_fork_chain_id(self.evm.fork_chain_id.map(u64::from).map(U256::from))
296 .fork_request_timeout(self.evm.fork_request_timeout.map(Duration::from_millis))
297 .fork_request_retries(self.evm.fork_request_retries)
298 .fork_retry_backoff(self.evm.fork_retry_backoff.map(Duration::from_millis))
299 .fork_compute_units_per_second(compute_units_per_second)
300 .with_fork_urls(self.evm.fork_url.into_iter().map(|f| f.url).collect())
301 .with_base_fee(self.evm.block_base_fee_per_gas)
302 .disable_min_priority_fee(self.evm.disable_min_priority_fee)
303 .with_no_storage_caching(self.evm.no_storage_caching)
304 .with_server_config(self.server_config)
305 .with_host(self.host)
306 .set_silent(shell::is_quiet())
307 .set_config_out(self.config_out)
308 .with_transaction_order(self.order)
309 .with_genesis(self.init)
310 .with_steps_tracing(self.evm.steps_tracing)
311 .with_print_logs(!self.evm.disable_console_log)
312 .with_print_traces(self.evm.print_traces)
313 .with_auto_impersonate(self.evm.auto_impersonate)
314 .with_ipc(self.ipc)
315 .with_code_size_limit(self.evm.code_size_limit)
316 .disable_code_size_limit(self.evm.disable_code_size_limit)
317 .set_pruned_history(self.prune_history)
318 .with_init_state(self.load_state.or_else(|| self.state.and_then(|s| s.state)))
319 .with_transaction_block_keeper(self.transaction_block_keeper)
320 .with_max_transactions(self.max_transactions)
321 .with_max_persisted_states(self.max_persisted_states)
322 .with_networks(networks)
323 .with_chain_id(self.evm.chain_id)
326 .with_disable_default_create2_deployer(self.evm.disable_default_create2_deployer)
327 .with_disable_pool_balance_checks(self.evm.disable_pool_balance_checks)
328 .with_slots_in_an_epoch(self.slots_in_an_epoch)
329 .with_memory_limit(self.evm.memory_limit)
330 .with_cache_path(self.cache_path)
331 .with_funded_accounts(funded_accounts))
332 }
333
334 fn parse_funded_accounts(&self) -> eyre::Result<HashMap<Address, U256>> {
335 let mut accounts = HashMap::default();
336 for entry in &self.fund_accounts {
337 let parts: Vec<&str> = entry.split(':').collect();
338 if parts.len() != 2 {
339 eyre::bail!(
340 "Invalid fund-accounts entry '{}'. Expected format: ADDRESS:AMOUNT",
341 entry
342 );
343 }
344 let address = parts[0]
345 .parse::<Address>()
346 .map_err(|e| eyre::eyre!("Invalid address '{}': {}", parts[0], e))?;
347 let amount: u64 = parts[1]
348 .parse()
349 .map_err(|e| eyre::eyre!("Invalid amount '{}': {}", parts[1], e))?;
350 let balance = Unit::ETHER.wei().saturating_mul(U256::from(amount));
351 accounts.insert(address, balance);
352 }
353 Ok(accounts)
354 }
355
356 fn account_generator(&self) -> AccountGenerator {
357 let mut generator = AccountGenerator::new(self.accounts as usize)
358 .phrase(DEFAULT_MNEMONIC)
359 .chain_id(self.evm.chain_id.unwrap_or(CHAIN_ID.into()));
360 if let Some(ref mnemonic) = self.mnemonic {
361 generator = generator.phrase(mnemonic);
362 } else if let Some(count) = self.mnemonic_random {
363 let mut rng = rand_08::thread_rng();
364 let mnemonic = match Mnemonic::<English>::new_with_count(&mut rng, count) {
365 Ok(mnemonic) => mnemonic.to_phrase(),
366 Err(err) => {
367 warn!(target: "node", ?count, %err, "failed to generate mnemonic, falling back to 12-word random mnemonic");
368 Mnemonic::<English>::new_with_count(&mut rng, 12)
371 .expect("valid default word count")
372 .to_phrase()
373 }
374 };
375 generator = generator.phrase(mnemonic);
376 } else if let Some(seed) = self.mnemonic_seed {
377 let mut seed = StdRng::seed_from_u64(seed);
378 let mnemonic = Mnemonic::<English>::new(&mut seed).to_phrase();
379 generator = generator.phrase(mnemonic);
380 }
381 if let Some(ref derivation) = self.derivation_path {
382 generator = generator.derivation_path(derivation);
383 }
384 generator
385 }
386
387 fn dump_state_path(&self) -> Option<PathBuf> {
389 self.dump_state.as_ref().or_else(|| self.state.as_ref().map(|s| &s.path)).cloned()
390 }
391
392 pub async fn run(self) -> eyre::Result<()> {
396 let dump_state = self.dump_state_path();
397 let dump_interval =
398 self.state_interval.map(Duration::from_secs).unwrap_or(DEFAULT_DUMP_INTERVAL);
399 let preserve_historical_states = self.preserve_historical_states;
400
401 let (api, mut handle) = crate::try_spawn(self.into_node_config()?).await?;
402
403 let mut fork = api.get_fork();
405 let running = Arc::new(AtomicUsize::new(0));
406
407 let mut signal = handle.shutdown_signal_mut().take();
410
411 let task_manager = handle.task_manager();
412 let mut on_shutdown = task_manager.on_shutdown();
413
414 let mut state_dumper =
415 PeriodicStateDumper::new(api, dump_state, dump_interval, preserve_historical_states);
416
417 task_manager.spawn(async move {
418 #[cfg(unix)]
420 let mut sigterm = Box::pin(async {
421 if let Ok(mut stream) =
422 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
423 {
424 stream.recv().await;
425 } else {
426 futures::future::pending::<()>().await;
427 }
428 });
429
430 #[cfg(not(unix))]
432 let mut sigterm = Box::pin(futures::future::pending::<()>());
433
434 tokio::select! {
436 _ = &mut sigterm => {
437 trace!("received sigterm signal, shutting down");
438 }
439 _ = &mut on_shutdown => {}
440 _ = &mut state_dumper => {}
441 }
442
443 state_dumper.dump().await;
445
446 if let Some(fork) = fork.take() {
449 trace!("flushing cache on shutdown");
450 fork.database
451 .read()
452 .await
453 .maybe_flush_cache()
454 .expect("Could not flush cache on fork DB");
455 }
458 let code = if foundry_cli::is_machine() {
463 foundry_cli::ExitCode::Interrupted.to_i32()
464 } else {
465 0
466 };
467 std::process::exit(code);
468 });
469
470 ctrlc::set_handler(move || {
471 let prev = running.fetch_add(1, Ordering::SeqCst);
472 if prev == 0 {
473 trace!("received shutdown signal, shutting down");
474 let _ = signal.take();
475 }
476 })
477 .expect("Error setting Ctrl-C handler");
478
479 Ok(handle.await??)
480 }
481}
482
483#[derive(Clone, Debug, Parser)]
485#[command(next_help_heading = "EVM options")]
486pub struct AnvilEvmArgs {
487 #[arg(
495 long,
496 short,
497 visible_alias = "rpc-url",
498 value_name = "URL",
499 help_heading = "Fork config"
500 )]
501 pub fork_url: Vec<ForkUrl>,
502
503 #[arg(
507 long = "fork-header",
508 value_name = "HEADERS",
509 help_heading = "Fork config",
510 requires = "fork_url"
511 )]
512 pub fork_headers: Vec<String>,
513
514 #[arg(id = "timeout", long = "timeout", help_heading = "Fork config", requires = "fork_url")]
518 pub fork_request_timeout: Option<u64>,
519
520 #[arg(id = "retries", long = "retries", help_heading = "Fork config", requires = "fork_url")]
524 pub fork_request_retries: Option<u32>,
525
526 #[arg(
532 long,
533 requires = "fork_url",
534 value_name = "BLOCK",
535 help_heading = "Fork config",
536 allow_hyphen_values = true
537 )]
538 pub fork_block_number: Option<i128>,
539
540 #[arg(
544 long,
545 requires = "fork_url",
546 value_name = "TRANSACTION",
547 help_heading = "Fork config",
548 conflicts_with = "fork_block_number"
549 )]
550 pub fork_transaction_hash: Option<B256>,
551
552 #[arg(long, requires = "fork_url", value_name = "BACKOFF", help_heading = "Fork config")]
556 pub fork_retry_backoff: Option<u64>,
557
558 #[arg(
564 long,
565 help_heading = "Fork config",
566 value_name = "CHAIN",
567 requires = "fork_block_number"
568 )]
569 pub fork_chain_id: Option<Chain>,
570
571 #[arg(
577 long,
578 requires = "fork_url",
579 alias = "cups",
580 value_name = "CUPS",
581 help_heading = "Fork config"
582 )]
583 pub compute_units_per_second: Option<u64>,
584
585 #[arg(
591 long,
592 requires = "fork_url",
593 value_name = "NO_RATE_LIMITS",
594 help_heading = "Fork config",
595 visible_alias = "no-rpc-rate-limit"
596 )]
597 pub no_rate_limit: bool,
598
599 #[arg(long, requires = "fork_url", help_heading = "Fork config")]
607 pub no_storage_caching: bool,
608
609 #[arg(long, alias = "block-gas-limit", help_heading = "Environment config")]
611 pub gas_limit: Option<u64>,
612
613 #[arg(
615 long,
616 value_name = "DISABLE_GAS_LIMIT",
617 help_heading = "Environment config",
618 alias = "disable-gas-limit",
619 conflicts_with = "gas_limit"
620 )]
621 pub disable_block_gas_limit: bool,
622
623 #[arg(long, visible_alias = "tx-gas-limit", help_heading = "Environment config")]
625 pub enable_tx_gas_limit: bool,
626
627 #[arg(long, value_name = "CODE_SIZE", help_heading = "Environment config")]
630 pub code_size_limit: Option<usize>,
631
632 #[arg(
634 long,
635 value_name = "DISABLE_CODE_SIZE_LIMIT",
636 conflicts_with = "code_size_limit",
637 help_heading = "Environment config"
638 )]
639 pub disable_code_size_limit: bool,
640
641 #[arg(long, help_heading = "Environment config")]
643 pub gas_price: Option<u128>,
644
645 #[arg(
647 long,
648 visible_alias = "base-fee",
649 value_name = "FEE",
650 help_heading = "Environment config"
651 )]
652 pub block_base_fee_per_gas: Option<u64>,
653
654 #[arg(long, visible_alias = "no-priority-fee", help_heading = "Environment config")]
656 pub disable_min_priority_fee: bool,
657
658 #[arg(long, alias = "chain", help_heading = "Environment config")]
660 pub chain_id: Option<Chain>,
661
662 #[arg(long, visible_alias = "tracing")]
664 pub steps_tracing: bool,
665
666 #[arg(long, visible_alias = "no-console-log")]
668 pub disable_console_log: bool,
669
670 #[arg(long, visible_alias = "enable-trace-printing")]
672 pub print_traces: bool,
673
674 #[arg(long, visible_alias = "auto-unlock")]
677 pub auto_impersonate: bool,
678
679 #[arg(long, visible_alias = "no-create2")]
681 pub disable_default_create2_deployer: bool,
682
683 #[arg(long)]
685 pub disable_pool_balance_checks: bool,
686
687 #[arg(long)]
689 pub memory_limit: Option<u64>,
690
691 #[command(flatten)]
692 pub networks: NetworkConfigs,
693}
694
695impl AnvilEvmArgs {
702 pub fn resolve_rpc_alias(&mut self) {
703 if let Ok(config) = Config::load_with_providers(FigmentProviders::Anvil) {
704 let mut resolved_urls = Vec::new();
705 for fork_url in &self.fork_url {
706 let mut endpoints = config.rpc_endpoints.clone().resolved();
707 if let Some(endpoint) = endpoints.remove(&fork_url.url) {
708 match endpoint.all_urls() {
710 Ok(urls) => {
711 for (i, url) in urls.into_iter().enumerate() {
712 resolved_urls.push(ForkUrl {
713 url,
714 block: if i == 0 { fork_url.block } else { None },
716 });
717 }
718 }
719 Err(e) => {
720 warn!(target: "node", alias=%fork_url.url, %e, "could not resolve all endpoints, using primary endpoint only");
721 if let Ok(url) = endpoint.url() {
722 resolved_urls.push(ForkUrl { url, block: fork_url.block });
723 } else {
724 resolved_urls.push(fork_url.clone());
725 }
726 }
727 }
728 } else if let Some(Ok(url)) = config.get_rpc_url_with_alias(&fork_url.url) {
729 resolved_urls.push(ForkUrl { url: url.to_string(), block: fork_url.block });
731 } else {
732 resolved_urls.push(fork_url.clone());
734 }
735 }
736 self.fork_url = resolved_urls;
737 }
738 }
739}
740
741struct PeriodicStateDumper<N: Network> {
743 in_progress_dump: Option<Pin<Box<dyn Future<Output = ()> + Send + Sync + 'static>>>,
744 api: EthApi<N>,
745 dump_state: Option<PathBuf>,
746 preserve_historical_states: bool,
747 interval: Interval,
748}
749
750impl<N: Network<ReceiptEnvelope = FoundryReceiptEnvelope>> PeriodicStateDumper<N> {
751 fn new(
752 api: EthApi<N>,
753 dump_state: Option<PathBuf>,
754 interval: Duration,
755 preserve_historical_states: bool,
756 ) -> Self {
757 let dump_state = dump_state.map(|mut dump_state| {
758 if dump_state.is_dir() {
759 dump_state = dump_state.join("state.json");
760 }
761 dump_state
762 });
763
764 let interval = tokio::time::interval_at(Instant::now() + interval, interval);
766 Self { in_progress_dump: None, api, dump_state, preserve_historical_states, interval }
767 }
768
769 async fn dump(&self) {
770 if let Some(state) = self.dump_state.clone() {
771 Self::dump_state(self.api.clone(), state, self.preserve_historical_states).await
772 }
773 }
774
775 async fn dump_state(api: EthApi<N>, dump_state: PathBuf, preserve_historical_states: bool) {
777 trace!(path=?dump_state, "Dumping state on shutdown");
778 match api.serialized_state(preserve_historical_states).await {
779 Ok(state) => {
780 if let Err(err) = foundry_common::fs::write_json_file(&dump_state, &state) {
781 error!(?err, "Failed to dump state");
782 } else {
783 trace!(path=?dump_state, "Dumped state on shutdown");
784 }
785 }
786 Err(err) => {
787 error!(?err, "Failed to extract state");
788 }
789 }
790 }
791}
792
793impl<N: Network<ReceiptEnvelope = FoundryReceiptEnvelope>> Future for PeriodicStateDumper<N> {
795 type Output = ();
796
797 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
798 let this = self.get_mut();
799 if this.dump_state.is_none() {
800 return Poll::Pending;
801 }
802
803 loop {
804 if let Some(mut flush) = this.in_progress_dump.take() {
805 match flush.poll_unpin(cx) {
806 Poll::Ready(_) => {
807 this.interval.reset();
808 }
809 Poll::Pending => {
810 this.in_progress_dump = Some(flush);
811 return Poll::Pending;
812 }
813 }
814 }
815
816 if this.interval.poll_tick(cx).is_ready() {
817 let api = this.api.clone();
818 let path = this.dump_state.clone().expect("exists; see above");
819 this.in_progress_dump =
820 Some(Box::pin(Self::dump_state(api, path, this.preserve_historical_states)));
821 } else {
822 break;
823 }
824 }
825
826 Poll::Pending
827 }
828}
829
830#[derive(Clone, Debug)]
832pub struct StateFile {
833 pub path: PathBuf,
834 pub state: Option<SerializableState>,
835}
836
837impl StateFile {
838 fn parse(path: &str) -> Result<Self, String> {
841 Self::parse_path(path)
842 }
843
844 pub fn parse_path(path: impl AsRef<Path>) -> Result<Self, String> {
846 let mut path = path.as_ref().to_path_buf();
847 if path.is_dir() {
848 path = path.join("state.json");
849 }
850 let mut state = Self { path, state: None };
851 if !state.path.exists() {
852 return Ok(state);
853 }
854
855 state.state = Some(SerializableState::load(&state.path).map_err(|err| err.to_string())?);
856
857 Ok(state)
858 }
859}
860
861#[derive(Clone, Debug, PartialEq, Eq)]
864pub struct ForkUrl {
865 pub url: String,
867 pub block: Option<u64>,
869}
870
871impl fmt::Display for ForkUrl {
872 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
873 self.url.fmt(f)?;
874 if let Some(block) = self.block {
875 write!(f, "@{block}")?;
876 }
877 Ok(())
878 }
879}
880
881impl FromStr for ForkUrl {
882 type Err = String;
883
884 fn from_str(s: &str) -> Result<Self, Self::Err> {
885 if let Some((url, block)) = s.rsplit_once('@') {
886 if block == "latest" {
887 return Ok(Self { url: url.to_string(), block: None });
888 }
889 if !block.is_empty() && !block.contains(':') && !block.contains('.') {
891 let block: u64 = block
892 .parse()
893 .map_err(|_| format!("Failed to parse block number: `{block}`"))?;
894 return Ok(Self { url: url.to_string(), block: Some(block) });
895 }
896 }
897 Ok(Self { url: s.to_string(), block: None })
898 }
899}
900
901fn parse_hardfork(hf: &str, networks: &NetworkConfigs) -> eyre::Result<FoundryHardfork> {
903 if let Ok(hardfork) = FoundryHardfork::from_str(hf) {
904 networks.normalize_for_hardfork(hardfork).map_err(eyre::Report::msg)?;
905 return Ok(hardfork);
906 }
907
908 #[cfg(feature = "optimism")]
909 if networks.is_optimism() {
910 return Ok(OpHardfork::from_str(hf)?.into());
911 }
912 if networks.is_tempo() {
913 Ok(TempoHardfork::from_str(hf)?.into())
914 } else {
915 Ok(EthereumHardfork::from_str(hf)?.into())
916 }
917}
918
919fn read_genesis_file(path: &str) -> Result<Genesis, String> {
921 foundry_common::fs::read_json_file(path.as_ref()).map_err(|err| err.to_string())
922}
923
924fn duration_from_secs_f64(s: &str) -> Result<Duration, String> {
925 let s = s.parse::<f64>().map_err(|e| e.to_string())?;
926 if s == 0.0 {
927 return Err("Duration must be greater than 0".to_string());
928 }
929 Duration::try_from_secs_f64(s).map_err(|e| e.to_string())
930}
931
932#[cfg(test)]
933mod tests {
934 use super::*;
935 use std::{env, net::Ipv4Addr};
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 #[test]
975 fn can_parse_optimism_hardfork() {
976 let args: NodeArgs =
977 NodeArgs::parse_from(["anvil", "--optimism", "--hardfork", "Regolith"]);
978 let config = args.into_node_config().unwrap();
979 assert_eq!(config.hardfork, Some(OpHardfork::Regolith.into()));
980 }
981
982 #[test]
983 fn can_parse_tempo_hardfork_from_network() {
984 let args: NodeArgs =
985 NodeArgs::parse_from(["anvil", "--network", "tempo", "--hardfork", "T5"]);
986 let config = args.into_node_config().unwrap();
987
988 assert!(config.networks.is_tempo());
989 assert_eq!(config.hardfork, Some(TempoHardfork::T5.into()));
990 }
991
992 #[test]
993 fn can_parse_namespaced_tempo_hardfork() {
994 let args = NodeArgs::parse_from(["anvil", "--hardfork", "tempo:T5"]);
995 let config = args.into_node_config().unwrap();
996
997 assert!(config.networks.is_tempo());
998 assert_eq!(config.hardfork, Some(TempoHardfork::T5.into()));
999 }
1000
1001 #[cfg(feature = "optimism")]
1002 #[test]
1003 fn chain_id_infers_optimism_network_in_node_config() {
1004 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--chain-id", "10"]);
1005 let config = args.into_node_config().unwrap();
1006
1007 assert!(config.networks.is_optimism());
1008 }
1009
1010 #[test]
1011 fn chain_id_infers_tempo_network_for_hardfork() {
1012 let args = NodeArgs::parse_from(["anvil", "--chain-id", "4217", "--hardfork", "T5"]);
1013 let config = args.into_node_config().unwrap();
1014
1015 assert!(config.networks.is_tempo());
1016 assert_eq!(config.hardfork, Some(TempoHardfork::T5.into()));
1017 }
1018
1019 #[test]
1020 fn fork_chain_id_infers_tempo_network_for_hardfork() {
1021 let args = NodeArgs::parse_from([
1022 "anvil",
1023 "--fork-url",
1024 "http://localhost:8545",
1025 "--fork-block-number",
1026 "1",
1027 "--fork-chain-id",
1028 "4217",
1029 "--hardfork",
1030 "T5",
1031 ]);
1032 let config = args.into_node_config().unwrap();
1033
1034 assert!(config.networks.is_tempo());
1035 assert_eq!(config.hardfork, Some(TempoHardfork::T5.into()));
1036 }
1037
1038 #[test]
1039 fn cant_parse_invalid_hardfork() {
1040 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--hardfork", "Regolith"]);
1041 let config = args.into_node_config();
1042 assert!(config.is_err());
1043 }
1044
1045 #[test]
1046 fn can_parse_fork_headers() {
1047 let args: NodeArgs = NodeArgs::parse_from([
1048 "anvil",
1049 "--fork-url",
1050 "http,://localhost:8545",
1051 "--fork-header",
1052 "User-Agent: test-agent",
1053 "--fork-header",
1054 "Referrer: example.com",
1055 ]);
1056 assert_eq!(args.evm.fork_headers, vec!["User-Agent: test-agent", "Referrer: example.com"]);
1057 }
1058
1059 #[test]
1060 fn can_parse_prune_config() {
1061 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--prune-history"]);
1062 assert!(args.prune_history.is_some());
1063
1064 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--prune-history", "100"]);
1065 assert_eq!(args.prune_history, Some(Some(100)));
1066 }
1067
1068 #[test]
1069 fn can_parse_max_persisted_states_config() {
1070 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--max-persisted-states", "500"]);
1071 assert_eq!(args.max_persisted_states, (Some(500)));
1072 }
1073
1074 #[test]
1075 fn can_parse_disable_block_gas_limit() {
1076 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--disable-block-gas-limit"]);
1077 assert!(args.evm.disable_block_gas_limit);
1078
1079 let args =
1080 NodeArgs::try_parse_from(["anvil", "--disable-block-gas-limit", "--gas-limit", "100"]);
1081 assert!(args.is_err());
1082 }
1083
1084 #[test]
1085 fn can_parse_enable_tx_gas_limit() {
1086 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--enable-tx-gas-limit"]);
1087 assert!(args.evm.enable_tx_gas_limit);
1088
1089 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--tx-gas-limit"]);
1091 assert!(args.evm.enable_tx_gas_limit);
1092 }
1093
1094 #[test]
1095 fn can_parse_disable_code_size_limit() {
1096 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--disable-code-size-limit"]);
1097 assert!(args.evm.disable_code_size_limit);
1098
1099 let args = NodeArgs::try_parse_from([
1100 "anvil",
1101 "--disable-code-size-limit",
1102 "--code-size-limit",
1103 "100",
1104 ]);
1105 assert!(args.is_err());
1107 }
1108
1109 #[test]
1110 fn can_parse_host() {
1111 let args = NodeArgs::parse_from(["anvil"]);
1112 assert_eq!(args.host, vec![IpAddr::V4(Ipv4Addr::LOCALHOST)]);
1113
1114 let args = NodeArgs::parse_from([
1115 "anvil", "--host", "::1", "--host", "1.1.1.1", "--host", "2.2.2.2",
1116 ]);
1117 assert_eq!(
1118 args.host,
1119 ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::<IpAddr>().unwrap()).to_vec()
1120 );
1121
1122 let args = NodeArgs::parse_from(["anvil", "--host", "::1,1.1.1.1,2.2.2.2"]);
1123 assert_eq!(
1124 args.host,
1125 ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::<IpAddr>().unwrap()).to_vec()
1126 );
1127
1128 unsafe { env::set_var("ANVIL_IP_ADDR", "1.1.1.1") };
1129 let args = NodeArgs::parse_from(["anvil"]);
1130 assert_eq!(args.host, vec!["1.1.1.1".parse::<IpAddr>().unwrap()]);
1131
1132 unsafe { env::set_var("ANVIL_IP_ADDR", "::1,1.1.1.1,2.2.2.2") };
1133 let args = NodeArgs::parse_from(["anvil"]);
1134 assert_eq!(
1135 args.host,
1136 ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::<IpAddr>().unwrap()).to_vec()
1137 );
1138 }
1139
1140 #[test]
1141 fn can_parse_multiple_fork_urls() {
1142 let args: NodeArgs = NodeArgs::parse_from([
1143 "anvil",
1144 "--fork-url",
1145 "http://localhost:8545",
1146 "--fork-url",
1147 "http://localhost:8546",
1148 "--fork-url",
1149 "http://localhost:8547",
1150 ]);
1151 assert_eq!(args.evm.fork_url.len(), 3);
1152 assert_eq!(args.evm.fork_url[0].url, "http://localhost:8545");
1153 assert_eq!(args.evm.fork_url[1].url, "http://localhost:8546");
1154 assert_eq!(args.evm.fork_url[2].url, "http://localhost:8547");
1155
1156 let args: NodeArgs = NodeArgs::parse_from([
1158 "anvil",
1159 "--fork-url",
1160 "http://localhost:8545@1000000",
1161 "--fork-url",
1162 "http://localhost:8546",
1163 ]);
1164 assert_eq!(args.evm.fork_url[0].block, Some(1000000));
1165 assert_eq!(args.evm.fork_url[1].block, None);
1166 }
1167
1168 #[test]
1169 fn rejects_block_suffix_on_secondary_fork_urls() {
1170 let args: NodeArgs = NodeArgs::parse_from([
1171 "anvil",
1172 "--fork-url",
1173 "http://localhost:8545@1000000",
1174 "--fork-url",
1175 "http://localhost:8546@2000000",
1176 ]);
1177 let result = args.into_node_config();
1178 assert!(result.is_err());
1179 assert!(
1180 result.unwrap_err().to_string().contains("Block number suffixes"),
1181 "should reject block suffix on secondary fork URL"
1182 );
1183 }
1184
1185 #[test]
1186 fn fork_dependent_args_require_fork_url() {
1187 let cases = [
1189 vec!["anvil", "--fork-header", "X-Api-Key: test"],
1190 vec!["anvil", "--timeout", "5000"],
1191 vec!["anvil", "--retries", "3"],
1192 vec!["anvil", "--fork-block-number", "100"],
1193 vec!["anvil", "--fork-retry-backoff", "500"],
1194 ];
1195 for args in &cases {
1196 let result = NodeArgs::try_parse_from(args);
1197 assert!(result.is_err(), "expected error when using {:?} without --fork-url", args[1]);
1198 }
1199 }
1200}