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 hardfork = match &self.hardfork {
254 Some(hf) => Some(parse_hardfork(hf, &self.evm.networks)?),
255 None => None,
256 };
257
258 Ok(NodeConfig::default()
259 .with_gas_limit(self.evm.gas_limit)
260 .disable_block_gas_limit(self.evm.disable_block_gas_limit)
261 .enable_tx_gas_limit(self.evm.enable_tx_gas_limit)
262 .with_gas_price(self.evm.gas_price)
263 .with_hardfork(hardfork)
264 .with_blocktime(self.block_time)
265 .with_no_mining(self.no_mining)
266 .with_mixed_mining(self.mixed_mining, self.block_time)
267 .with_account_generator(self.account_generator())?
268 .with_genesis_balance(genesis_balance)
269 .with_genesis_timestamp(self.timestamp)
270 .with_genesis_block_number(self.number)
271 .with_port(self.port)
272 .with_fork_choice(match (self.evm.fork_block_number, self.evm.fork_transaction_hash) {
273 (Some(block), None) => Some(ForkChoice::Block(block)),
274 (None, Some(hash)) => Some(ForkChoice::Transaction(hash)),
275 _ => self
276 .evm
277 .fork_url
278 .first()
279 .and_then(|f| f.block)
280 .map(|num| ForkChoice::Block(num as i128)),
281 })
282 .with_fork_headers(self.evm.fork_headers)
283 .with_fork_chain_id(self.evm.fork_chain_id.map(u64::from).map(U256::from))
284 .fork_request_timeout(self.evm.fork_request_timeout.map(Duration::from_millis))
285 .fork_request_retries(self.evm.fork_request_retries)
286 .fork_retry_backoff(self.evm.fork_retry_backoff.map(Duration::from_millis))
287 .fork_compute_units_per_second(compute_units_per_second)
288 .with_fork_urls(self.evm.fork_url.into_iter().map(|f| f.url).collect())
289 .with_base_fee(self.evm.block_base_fee_per_gas)
290 .disable_min_priority_fee(self.evm.disable_min_priority_fee)
291 .with_no_storage_caching(self.evm.no_storage_caching)
292 .with_server_config(self.server_config)
293 .with_host(self.host)
294 .set_silent(shell::is_quiet())
295 .set_config_out(self.config_out)
296 .with_chain_id(self.evm.chain_id)
297 .with_transaction_order(self.order)
298 .with_genesis(self.init)
299 .with_steps_tracing(self.evm.steps_tracing)
300 .with_print_logs(!self.evm.disable_console_log)
301 .with_print_traces(self.evm.print_traces)
302 .with_auto_impersonate(self.evm.auto_impersonate)
303 .with_ipc(self.ipc)
304 .with_code_size_limit(self.evm.code_size_limit)
305 .disable_code_size_limit(self.evm.disable_code_size_limit)
306 .set_pruned_history(self.prune_history)
307 .with_init_state(self.load_state.or_else(|| self.state.and_then(|s| s.state)))
308 .with_transaction_block_keeper(self.transaction_block_keeper)
309 .with_max_transactions(self.max_transactions)
310 .with_max_persisted_states(self.max_persisted_states)
311 .with_networks(self.evm.networks)
312 .with_disable_default_create2_deployer(self.evm.disable_default_create2_deployer)
313 .with_disable_pool_balance_checks(self.evm.disable_pool_balance_checks)
314 .with_slots_in_an_epoch(self.slots_in_an_epoch)
315 .with_memory_limit(self.evm.memory_limit)
316 .with_cache_path(self.cache_path)
317 .with_funded_accounts(funded_accounts))
318 }
319
320 fn parse_funded_accounts(&self) -> eyre::Result<HashMap<Address, U256>> {
321 let mut accounts = HashMap::default();
322 for entry in &self.fund_accounts {
323 let parts: Vec<&str> = entry.split(':').collect();
324 if parts.len() != 2 {
325 eyre::bail!(
326 "Invalid fund-accounts entry '{}'. Expected format: ADDRESS:AMOUNT",
327 entry
328 );
329 }
330 let address = parts[0]
331 .parse::<Address>()
332 .map_err(|e| eyre::eyre!("Invalid address '{}': {}", parts[0], e))?;
333 let amount: u64 = parts[1]
334 .parse()
335 .map_err(|e| eyre::eyre!("Invalid amount '{}': {}", parts[1], e))?;
336 let balance = Unit::ETHER.wei().saturating_mul(U256::from(amount));
337 accounts.insert(address, balance);
338 }
339 Ok(accounts)
340 }
341
342 fn account_generator(&self) -> AccountGenerator {
343 let mut generator = AccountGenerator::new(self.accounts as usize)
344 .phrase(DEFAULT_MNEMONIC)
345 .chain_id(self.evm.chain_id.unwrap_or(CHAIN_ID.into()));
346 if let Some(ref mnemonic) = self.mnemonic {
347 generator = generator.phrase(mnemonic);
348 } else if let Some(count) = self.mnemonic_random {
349 let mut rng = rand_08::thread_rng();
350 let mnemonic = match Mnemonic::<English>::new_with_count(&mut rng, count) {
351 Ok(mnemonic) => mnemonic.to_phrase(),
352 Err(err) => {
353 warn!(target: "node", ?count, %err, "failed to generate mnemonic, falling back to 12-word random mnemonic");
354 Mnemonic::<English>::new_with_count(&mut rng, 12)
357 .expect("valid default word count")
358 .to_phrase()
359 }
360 };
361 generator = generator.phrase(mnemonic);
362 } else if let Some(seed) = self.mnemonic_seed {
363 let mut seed = StdRng::seed_from_u64(seed);
364 let mnemonic = Mnemonic::<English>::new(&mut seed).to_phrase();
365 generator = generator.phrase(mnemonic);
366 }
367 if let Some(ref derivation) = self.derivation_path {
368 generator = generator.derivation_path(derivation);
369 }
370 generator
371 }
372
373 fn dump_state_path(&self) -> Option<PathBuf> {
375 self.dump_state.as_ref().or_else(|| self.state.as_ref().map(|s| &s.path)).cloned()
376 }
377
378 pub async fn run(self) -> eyre::Result<()> {
382 let dump_state = self.dump_state_path();
383 let dump_interval =
384 self.state_interval.map(Duration::from_secs).unwrap_or(DEFAULT_DUMP_INTERVAL);
385 let preserve_historical_states = self.preserve_historical_states;
386
387 let (api, mut handle) = crate::try_spawn(self.into_node_config()?).await?;
388
389 let mut fork = api.get_fork();
391 let running = Arc::new(AtomicUsize::new(0));
392
393 let mut signal = handle.shutdown_signal_mut().take();
396
397 let task_manager = handle.task_manager();
398 let mut on_shutdown = task_manager.on_shutdown();
399
400 let mut state_dumper =
401 PeriodicStateDumper::new(api, dump_state, dump_interval, preserve_historical_states);
402
403 task_manager.spawn(async move {
404 #[cfg(unix)]
406 let mut sigterm = Box::pin(async {
407 if let Ok(mut stream) =
408 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
409 {
410 stream.recv().await;
411 } else {
412 futures::future::pending::<()>().await;
413 }
414 });
415
416 #[cfg(not(unix))]
418 let mut sigterm = Box::pin(futures::future::pending::<()>());
419
420 tokio::select! {
422 _ = &mut sigterm => {
423 trace!("received sigterm signal, shutting down");
424 }
425 _ = &mut on_shutdown => {}
426 _ = &mut state_dumper => {}
427 }
428
429 state_dumper.dump().await;
431
432 if let Some(fork) = fork.take() {
435 trace!("flushing cache on shutdown");
436 fork.database
437 .read()
438 .await
439 .maybe_flush_cache()
440 .expect("Could not flush cache on fork DB");
441 }
444 std::process::exit(0);
445 });
446
447 ctrlc::set_handler(move || {
448 let prev = running.fetch_add(1, Ordering::SeqCst);
449 if prev == 0 {
450 trace!("received shutdown signal, shutting down");
451 let _ = signal.take();
452 }
453 })
454 .expect("Error setting Ctrl-C handler");
455
456 Ok(handle.await??)
457 }
458}
459
460#[derive(Clone, Debug, Parser)]
462#[command(next_help_heading = "EVM options")]
463pub struct AnvilEvmArgs {
464 #[arg(
472 long,
473 short,
474 visible_alias = "rpc-url",
475 value_name = "URL",
476 help_heading = "Fork config"
477 )]
478 pub fork_url: Vec<ForkUrl>,
479
480 #[arg(
484 long = "fork-header",
485 value_name = "HEADERS",
486 help_heading = "Fork config",
487 requires = "fork_url"
488 )]
489 pub fork_headers: Vec<String>,
490
491 #[arg(id = "timeout", long = "timeout", help_heading = "Fork config", requires = "fork_url")]
495 pub fork_request_timeout: Option<u64>,
496
497 #[arg(id = "retries", long = "retries", help_heading = "Fork config", requires = "fork_url")]
501 pub fork_request_retries: Option<u32>,
502
503 #[arg(
509 long,
510 requires = "fork_url",
511 value_name = "BLOCK",
512 help_heading = "Fork config",
513 allow_hyphen_values = true
514 )]
515 pub fork_block_number: Option<i128>,
516
517 #[arg(
521 long,
522 requires = "fork_url",
523 value_name = "TRANSACTION",
524 help_heading = "Fork config",
525 conflicts_with = "fork_block_number"
526 )]
527 pub fork_transaction_hash: Option<B256>,
528
529 #[arg(long, requires = "fork_url", value_name = "BACKOFF", help_heading = "Fork config")]
533 pub fork_retry_backoff: Option<u64>,
534
535 #[arg(
541 long,
542 help_heading = "Fork config",
543 value_name = "CHAIN",
544 requires = "fork_block_number"
545 )]
546 pub fork_chain_id: Option<Chain>,
547
548 #[arg(
554 long,
555 requires = "fork_url",
556 alias = "cups",
557 value_name = "CUPS",
558 help_heading = "Fork config"
559 )]
560 pub compute_units_per_second: Option<u64>,
561
562 #[arg(
568 long,
569 requires = "fork_url",
570 value_name = "NO_RATE_LIMITS",
571 help_heading = "Fork config",
572 visible_alias = "no-rpc-rate-limit"
573 )]
574 pub no_rate_limit: bool,
575
576 #[arg(long, requires = "fork_url", help_heading = "Fork config")]
584 pub no_storage_caching: bool,
585
586 #[arg(long, alias = "block-gas-limit", help_heading = "Environment config")]
588 pub gas_limit: Option<u64>,
589
590 #[arg(
592 long,
593 value_name = "DISABLE_GAS_LIMIT",
594 help_heading = "Environment config",
595 alias = "disable-gas-limit",
596 conflicts_with = "gas_limit"
597 )]
598 pub disable_block_gas_limit: bool,
599
600 #[arg(long, visible_alias = "tx-gas-limit", help_heading = "Environment config")]
602 pub enable_tx_gas_limit: bool,
603
604 #[arg(long, value_name = "CODE_SIZE", help_heading = "Environment config")]
607 pub code_size_limit: Option<usize>,
608
609 #[arg(
611 long,
612 value_name = "DISABLE_CODE_SIZE_LIMIT",
613 conflicts_with = "code_size_limit",
614 help_heading = "Environment config"
615 )]
616 pub disable_code_size_limit: bool,
617
618 #[arg(long, help_heading = "Environment config")]
620 pub gas_price: Option<u128>,
621
622 #[arg(
624 long,
625 visible_alias = "base-fee",
626 value_name = "FEE",
627 help_heading = "Environment config"
628 )]
629 pub block_base_fee_per_gas: Option<u64>,
630
631 #[arg(long, visible_alias = "no-priority-fee", help_heading = "Environment config")]
633 pub disable_min_priority_fee: bool,
634
635 #[arg(long, alias = "chain", help_heading = "Environment config")]
637 pub chain_id: Option<Chain>,
638
639 #[arg(long, visible_alias = "tracing")]
641 pub steps_tracing: bool,
642
643 #[arg(long, visible_alias = "no-console-log")]
645 pub disable_console_log: bool,
646
647 #[arg(long, visible_alias = "enable-trace-printing")]
649 pub print_traces: bool,
650
651 #[arg(long, visible_alias = "auto-unlock")]
654 pub auto_impersonate: bool,
655
656 #[arg(long, visible_alias = "no-create2")]
658 pub disable_default_create2_deployer: bool,
659
660 #[arg(long)]
662 pub disable_pool_balance_checks: bool,
663
664 #[arg(long)]
666 pub memory_limit: Option<u64>,
667
668 #[command(flatten)]
669 pub networks: NetworkConfigs,
670}
671
672impl AnvilEvmArgs {
679 pub fn resolve_rpc_alias(&mut self) {
680 if let Ok(config) = Config::load_with_providers(FigmentProviders::Anvil) {
681 let mut resolved_urls = Vec::new();
682 for fork_url in &self.fork_url {
683 let mut endpoints = config.rpc_endpoints.clone().resolved();
684 if let Some(endpoint) = endpoints.remove(&fork_url.url) {
685 match endpoint.all_urls() {
687 Ok(urls) => {
688 for (i, url) in urls.into_iter().enumerate() {
689 resolved_urls.push(ForkUrl {
690 url,
691 block: if i == 0 { fork_url.block } else { None },
693 });
694 }
695 }
696 Err(e) => {
697 warn!(target: "node", alias=%fork_url.url, %e, "could not resolve all endpoints, using primary endpoint only");
698 if let Ok(url) = endpoint.url() {
699 resolved_urls.push(ForkUrl { url, block: fork_url.block });
700 } else {
701 resolved_urls.push(fork_url.clone());
702 }
703 }
704 }
705 } else if let Some(Ok(url)) = config.get_rpc_url_with_alias(&fork_url.url) {
706 resolved_urls.push(ForkUrl { url: url.to_string(), block: fork_url.block });
708 } else {
709 resolved_urls.push(fork_url.clone());
711 }
712 }
713 self.fork_url = resolved_urls;
714 }
715 }
716}
717
718struct PeriodicStateDumper<N: Network> {
720 in_progress_dump: Option<Pin<Box<dyn Future<Output = ()> + Send + Sync + 'static>>>,
721 api: EthApi<N>,
722 dump_state: Option<PathBuf>,
723 preserve_historical_states: bool,
724 interval: Interval,
725}
726
727impl<N: Network<ReceiptEnvelope = FoundryReceiptEnvelope>> PeriodicStateDumper<N> {
728 fn new(
729 api: EthApi<N>,
730 dump_state: Option<PathBuf>,
731 interval: Duration,
732 preserve_historical_states: bool,
733 ) -> Self {
734 let dump_state = dump_state.map(|mut dump_state| {
735 if dump_state.is_dir() {
736 dump_state = dump_state.join("state.json");
737 }
738 dump_state
739 });
740
741 let interval = tokio::time::interval_at(Instant::now() + interval, interval);
743 Self { in_progress_dump: None, api, dump_state, preserve_historical_states, interval }
744 }
745
746 async fn dump(&self) {
747 if let Some(state) = self.dump_state.clone() {
748 Self::dump_state(self.api.clone(), state, self.preserve_historical_states).await
749 }
750 }
751
752 async fn dump_state(api: EthApi<N>, dump_state: PathBuf, preserve_historical_states: bool) {
754 trace!(path=?dump_state, "Dumping state on shutdown");
755 match api.serialized_state(preserve_historical_states).await {
756 Ok(state) => {
757 if let Err(err) = foundry_common::fs::write_json_file(&dump_state, &state) {
758 error!(?err, "Failed to dump state");
759 } else {
760 trace!(path=?dump_state, "Dumped state on shutdown");
761 }
762 }
763 Err(err) => {
764 error!(?err, "Failed to extract state");
765 }
766 }
767 }
768}
769
770impl<N: Network<ReceiptEnvelope = FoundryReceiptEnvelope>> Future for PeriodicStateDumper<N> {
772 type Output = ();
773
774 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
775 let this = self.get_mut();
776 if this.dump_state.is_none() {
777 return Poll::Pending;
778 }
779
780 loop {
781 if let Some(mut flush) = this.in_progress_dump.take() {
782 match flush.poll_unpin(cx) {
783 Poll::Ready(_) => {
784 this.interval.reset();
785 }
786 Poll::Pending => {
787 this.in_progress_dump = Some(flush);
788 return Poll::Pending;
789 }
790 }
791 }
792
793 if this.interval.poll_tick(cx).is_ready() {
794 let api = this.api.clone();
795 let path = this.dump_state.clone().expect("exists; see above");
796 this.in_progress_dump =
797 Some(Box::pin(Self::dump_state(api, path, this.preserve_historical_states)));
798 } else {
799 break;
800 }
801 }
802
803 Poll::Pending
804 }
805}
806
807#[derive(Clone, Debug)]
809pub struct StateFile {
810 pub path: PathBuf,
811 pub state: Option<SerializableState>,
812}
813
814impl StateFile {
815 fn parse(path: &str) -> Result<Self, String> {
818 Self::parse_path(path)
819 }
820
821 pub fn parse_path(path: impl AsRef<Path>) -> Result<Self, String> {
823 let mut path = path.as_ref().to_path_buf();
824 if path.is_dir() {
825 path = path.join("state.json");
826 }
827 let mut state = Self { path, state: None };
828 if !state.path.exists() {
829 return Ok(state);
830 }
831
832 state.state = Some(SerializableState::load(&state.path).map_err(|err| err.to_string())?);
833
834 Ok(state)
835 }
836}
837
838#[derive(Clone, Debug, PartialEq, Eq)]
841pub struct ForkUrl {
842 pub url: String,
844 pub block: Option<u64>,
846}
847
848impl fmt::Display for ForkUrl {
849 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
850 self.url.fmt(f)?;
851 if let Some(block) = self.block {
852 write!(f, "@{block}")?;
853 }
854 Ok(())
855 }
856}
857
858impl FromStr for ForkUrl {
859 type Err = String;
860
861 fn from_str(s: &str) -> Result<Self, Self::Err> {
862 if let Some((url, block)) = s.rsplit_once('@') {
863 if block == "latest" {
864 return Ok(Self { url: url.to_string(), block: None });
865 }
866 if !block.is_empty() && !block.contains(':') && !block.contains('.') {
868 let block: u64 = block
869 .parse()
870 .map_err(|_| format!("Failed to parse block number: `{block}`"))?;
871 return Ok(Self { url: url.to_string(), block: Some(block) });
872 }
873 }
874 Ok(Self { url: s.to_string(), block: None })
875 }
876}
877
878fn parse_hardfork(hf: &str, networks: &NetworkConfigs) -> eyre::Result<FoundryHardfork> {
880 #[cfg(feature = "optimism")]
881 if networks.is_optimism() {
882 return Ok(OpHardfork::from_str(hf)?.into());
883 }
884 if networks.is_tempo() {
885 Ok(TempoHardfork::from_str(hf)?.into())
886 } else {
887 Ok(EthereumHardfork::from_str(hf)?.into())
888 }
889}
890
891fn read_genesis_file(path: &str) -> Result<Genesis, String> {
893 foundry_common::fs::read_json_file(path.as_ref()).map_err(|err| err.to_string())
894}
895
896fn duration_from_secs_f64(s: &str) -> Result<Duration, String> {
897 let s = s.parse::<f64>().map_err(|e| e.to_string())?;
898 if s == 0.0 {
899 return Err("Duration must be greater than 0".to_string());
900 }
901 Duration::try_from_secs_f64(s).map_err(|e| e.to_string())
902}
903
904#[cfg(test)]
905mod tests {
906 use super::*;
907 use std::{env, net::Ipv4Addr};
908
909 #[test]
910 fn test_parse_fork_url() {
911 let fork: ForkUrl = "http://localhost:8545@1000000".parse().unwrap();
912 assert_eq!(
913 fork,
914 ForkUrl { url: "http://localhost:8545".to_string(), block: Some(1000000) }
915 );
916
917 let fork: ForkUrl = "http://localhost:8545".parse().unwrap();
918 assert_eq!(fork, ForkUrl { url: "http://localhost:8545".to_string(), block: None });
919
920 let fork: ForkUrl = "wss://user:password@example.com/".parse().unwrap();
921 assert_eq!(
922 fork,
923 ForkUrl { url: "wss://user:password@example.com/".to_string(), block: None }
924 );
925
926 let fork: ForkUrl = "wss://user:password@example.com/@latest".parse().unwrap();
927 assert_eq!(
928 fork,
929 ForkUrl { url: "wss://user:password@example.com/".to_string(), block: None }
930 );
931
932 let fork: ForkUrl = "wss://user:password@example.com/@100000".parse().unwrap();
933 assert_eq!(
934 fork,
935 ForkUrl { url: "wss://user:password@example.com/".to_string(), block: Some(100000) }
936 );
937 }
938
939 #[test]
940 fn can_parse_ethereum_hardfork() {
941 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--hardfork", "berlin"]);
942 let config = args.into_node_config().unwrap();
943 assert_eq!(config.hardfork, Some(EthereumHardfork::Berlin.into()));
944 }
945
946 #[test]
947 fn can_parse_optimism_hardfork() {
948 let args: NodeArgs =
949 NodeArgs::parse_from(["anvil", "--optimism", "--hardfork", "Regolith"]);
950 let config = args.into_node_config().unwrap();
951 assert_eq!(config.hardfork, Some(OpHardfork::Regolith.into()));
952 }
953
954 #[test]
955 fn cant_parse_invalid_hardfork() {
956 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--hardfork", "Regolith"]);
957 let config = args.into_node_config();
958 assert!(config.is_err());
959 }
960
961 #[test]
962 fn can_parse_fork_headers() {
963 let args: NodeArgs = NodeArgs::parse_from([
964 "anvil",
965 "--fork-url",
966 "http,://localhost:8545",
967 "--fork-header",
968 "User-Agent: test-agent",
969 "--fork-header",
970 "Referrer: example.com",
971 ]);
972 assert_eq!(args.evm.fork_headers, vec!["User-Agent: test-agent", "Referrer: example.com"]);
973 }
974
975 #[test]
976 fn can_parse_prune_config() {
977 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--prune-history"]);
978 assert!(args.prune_history.is_some());
979
980 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--prune-history", "100"]);
981 assert_eq!(args.prune_history, Some(Some(100)));
982 }
983
984 #[test]
985 fn can_parse_max_persisted_states_config() {
986 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--max-persisted-states", "500"]);
987 assert_eq!(args.max_persisted_states, (Some(500)));
988 }
989
990 #[test]
991 fn can_parse_disable_block_gas_limit() {
992 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--disable-block-gas-limit"]);
993 assert!(args.evm.disable_block_gas_limit);
994
995 let args =
996 NodeArgs::try_parse_from(["anvil", "--disable-block-gas-limit", "--gas-limit", "100"]);
997 assert!(args.is_err());
998 }
999
1000 #[test]
1001 fn can_parse_enable_tx_gas_limit() {
1002 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--enable-tx-gas-limit"]);
1003 assert!(args.evm.enable_tx_gas_limit);
1004
1005 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--tx-gas-limit"]);
1007 assert!(args.evm.enable_tx_gas_limit);
1008 }
1009
1010 #[test]
1011 fn can_parse_disable_code_size_limit() {
1012 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--disable-code-size-limit"]);
1013 assert!(args.evm.disable_code_size_limit);
1014
1015 let args = NodeArgs::try_parse_from([
1016 "anvil",
1017 "--disable-code-size-limit",
1018 "--code-size-limit",
1019 "100",
1020 ]);
1021 assert!(args.is_err());
1023 }
1024
1025 #[test]
1026 fn can_parse_host() {
1027 let args = NodeArgs::parse_from(["anvil"]);
1028 assert_eq!(args.host, vec![IpAddr::V4(Ipv4Addr::LOCALHOST)]);
1029
1030 let args = NodeArgs::parse_from([
1031 "anvil", "--host", "::1", "--host", "1.1.1.1", "--host", "2.2.2.2",
1032 ]);
1033 assert_eq!(
1034 args.host,
1035 ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::<IpAddr>().unwrap()).to_vec()
1036 );
1037
1038 let args = NodeArgs::parse_from(["anvil", "--host", "::1,1.1.1.1,2.2.2.2"]);
1039 assert_eq!(
1040 args.host,
1041 ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::<IpAddr>().unwrap()).to_vec()
1042 );
1043
1044 unsafe { env::set_var("ANVIL_IP_ADDR", "1.1.1.1") };
1045 let args = NodeArgs::parse_from(["anvil"]);
1046 assert_eq!(args.host, vec!["1.1.1.1".parse::<IpAddr>().unwrap()]);
1047
1048 unsafe { env::set_var("ANVIL_IP_ADDR", "::1,1.1.1.1,2.2.2.2") };
1049 let args = NodeArgs::parse_from(["anvil"]);
1050 assert_eq!(
1051 args.host,
1052 ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::<IpAddr>().unwrap()).to_vec()
1053 );
1054 }
1055
1056 #[test]
1057 fn can_parse_multiple_fork_urls() {
1058 let args: NodeArgs = NodeArgs::parse_from([
1059 "anvil",
1060 "--fork-url",
1061 "http://localhost:8545",
1062 "--fork-url",
1063 "http://localhost:8546",
1064 "--fork-url",
1065 "http://localhost:8547",
1066 ]);
1067 assert_eq!(args.evm.fork_url.len(), 3);
1068 assert_eq!(args.evm.fork_url[0].url, "http://localhost:8545");
1069 assert_eq!(args.evm.fork_url[1].url, "http://localhost:8546");
1070 assert_eq!(args.evm.fork_url[2].url, "http://localhost:8547");
1071
1072 let args: NodeArgs = NodeArgs::parse_from([
1074 "anvil",
1075 "--fork-url",
1076 "http://localhost:8545@1000000",
1077 "--fork-url",
1078 "http://localhost:8546",
1079 ]);
1080 assert_eq!(args.evm.fork_url[0].block, Some(1000000));
1081 assert_eq!(args.evm.fork_url[1].block, None);
1082 }
1083
1084 #[test]
1085 fn rejects_block_suffix_on_secondary_fork_urls() {
1086 let args: NodeArgs = NodeArgs::parse_from([
1087 "anvil",
1088 "--fork-url",
1089 "http://localhost:8545@1000000",
1090 "--fork-url",
1091 "http://localhost:8546@2000000",
1092 ]);
1093 let result = args.into_node_config();
1094 assert!(result.is_err());
1095 assert!(
1096 result.unwrap_err().to_string().contains("Block number suffixes"),
1097 "should reject block suffix on secondary fork URL"
1098 );
1099 }
1100
1101 #[test]
1102 fn fork_dependent_args_require_fork_url() {
1103 let cases = [
1105 vec!["anvil", "--fork-header", "X-Api-Key: test"],
1106 vec!["anvil", "--timeout", "5000"],
1107 vec!["anvil", "--retries", "3"],
1108 vec!["anvil", "--fork-block-number", "100"],
1109 vec!["anvil", "--fork-retry-backoff", "500"],
1110 ];
1111 for args in &cases {
1112 let result = NodeArgs::try_parse_from(args);
1113 assert!(result.is_err(), "expected error when using {:?} without --fork-url", args[1]);
1114 }
1115 }
1116}