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_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")]
111 pub mixed_mining: bool,
112
113 #[arg(
115 long,
116 value_name = "IP_ADDR",
117 env = "ANVIL_IP_ADDR",
118 default_value = "127.0.0.1",
119 help_heading = "Server options",
120 value_delimiter = ','
121 )]
122 pub host: Vec<IpAddr>,
123
124 #[arg(long, default_value = "fees")]
126 pub order: TransactionOrder,
127
128 #[arg(long, value_name = "PATH", value_parser= read_genesis_file)]
130 pub init: Option<Genesis>,
131
132 #[arg(
137 long,
138 value_name = "PATH",
139 value_parser = StateFile::parse,
140 conflicts_with_all = &[
141 "init",
142 "dump_state",
143 "load_state"
144 ]
145 )]
146 pub state: Option<StateFile>,
147
148 #[arg(short, long, value_name = "SECONDS")]
152 pub state_interval: Option<u64>,
153
154 #[arg(long, value_name = "PATH", conflicts_with = "init")]
158 pub dump_state: Option<PathBuf>,
159
160 #[arg(long, conflicts_with = "init", default_value = "false")]
167 pub preserve_historical_states: bool,
168
169 #[arg(
171 long,
172 value_name = "PATH",
173 value_parser = SerializableState::parse,
174 conflicts_with = "init"
175 )]
176 pub load_state: Option<SerializableState>,
177
178 #[arg(long, value_name = "ADDRESS:AMOUNT", value_delimiter = ' ', num_args = 1..)]
183 pub fund_accounts: Vec<String>,
184
185 #[arg(long, help = IPC_HELP, value_name = "PATH", visible_alias = "ipcpath")]
186 pub ipc: Option<Option<String>>,
187
188 #[arg(long)]
193 pub prune_history: Option<Option<usize>>,
194
195 #[arg(long, conflicts_with = "prune_history")]
199 pub max_persisted_states: Option<usize>,
200
201 #[arg(long)]
203 pub transaction_block_keeper: Option<usize>,
204
205 #[arg(long)]
207 pub max_transactions: Option<usize>,
208
209 #[command(flatten)]
210 pub evm: AnvilEvmArgs,
211
212 #[command(flatten)]
213 pub server_config: ServerConfig,
214
215 #[arg(long, value_name = "PATH")]
221 pub cache_path: Option<PathBuf>,
222}
223
224#[cfg(windows)]
225const IPC_HELP: &str =
226 "Launch an ipc server at the given path or default path = `\\.\\pipe\\anvil.ipc`";
227
228#[cfg(not(windows))]
230const IPC_HELP: &str = "Launch an ipc server at the given path or default path = `/tmp/anvil.ipc`";
231
232const DEFAULT_DUMP_INTERVAL: Duration = Duration::from_secs(60);
234
235impl NodeArgs {
236 pub fn into_node_config(self) -> eyre::Result<NodeConfig> {
237 let genesis_balance = Unit::ETHER.wei().saturating_mul(U256::from(self.balance));
238 let compute_units_per_second =
239 if self.evm.no_rate_limit { Some(u64::MAX) } else { self.evm.compute_units_per_second };
240
241 if self.evm.fork_url.len() > 1 {
243 for fork in &self.evm.fork_url[1..] {
244 if fork.block.is_some() {
245 eyre::bail!(
246 "Block number suffixes (@block) on secondary --fork-url values are not supported. \
247 Use --fork-block-number to set the fork block for all endpoints."
248 );
249 }
250 }
251 }
252
253 let funded_accounts = self.parse_funded_accounts()?;
254
255 let networks = self
256 .evm
257 .chain_id
258 .map(u64::from)
259 .or_else(|| self.evm.fork_chain_id.map(u64::from))
260 .map_or(self.evm.networks, |chain_id| self.evm.networks.with_chain_id(chain_id));
261
262 let hardfork = match &self.hardfork {
263 Some(hf) => Some(parse_hardfork(hf, &networks)?),
264 None => None,
265 };
266 let networks = if let Some(hardfork) = hardfork {
267 networks.normalize_for_hardfork(hardfork).map_err(eyre::Report::msg)?
268 } else {
269 networks
270 };
271
272 Ok(NodeConfig::default()
273 .with_gas_limit(self.evm.gas_limit)
274 .disable_block_gas_limit(self.evm.disable_block_gas_limit)
275 .enable_tx_gas_limit(self.evm.enable_tx_gas_limit)
276 .with_gas_price(self.evm.gas_price)
277 .with_hardfork(hardfork)
278 .with_blocktime(self.block_time)
279 .with_no_mining(self.no_mining)
280 .with_mixed_mining(self.mixed_mining, self.block_time)
281 .with_account_generator(self.account_generator())?
282 .with_genesis_balance(genesis_balance)
283 .with_genesis_timestamp(self.timestamp)
284 .with_genesis_block_number(self.number)
285 .with_port(self.port)
286 .with_fork_choice(match (self.evm.fork_block_number, self.evm.fork_transaction_hash) {
287 (Some(block), None) => Some(ForkChoice::Block(block)),
288 (None, Some(hash)) => Some(ForkChoice::Transaction(hash)),
289 _ => self
290 .evm
291 .fork_url
292 .first()
293 .and_then(|f| f.block)
294 .map(|num| ForkChoice::Block(num as i128)),
295 })
296 .with_fork_headers(self.evm.fork_headers)
297 .with_fork_chain_id(self.evm.fork_chain_id.map(u64::from).map(U256::from))
298 .fork_request_timeout(self.evm.fork_request_timeout.map(Duration::from_millis))
299 .fork_request_retries(self.evm.fork_request_retries)
300 .fork_retry_backoff(self.evm.fork_retry_backoff.map(Duration::from_millis))
301 .fork_compute_units_per_second(compute_units_per_second)
302 .with_fork_urls(self.evm.fork_url.into_iter().map(|f| f.url).collect())
303 .with_base_fee(self.evm.block_base_fee_per_gas)
304 .disable_min_priority_fee(self.evm.disable_min_priority_fee)
305 .with_no_storage_caching(self.evm.no_storage_caching)
306 .with_server_config(self.server_config)
307 .with_host(self.host)
308 .set_silent(shell::is_quiet())
309 .set_config_out(self.config_out)
310 .with_transaction_order(self.order)
311 .with_genesis(self.init)
312 .with_steps_tracing(self.evm.steps_tracing)
313 .with_print_logs(!self.evm.disable_console_log)
314 .with_print_traces(self.evm.print_traces)
315 .with_auto_impersonate(self.evm.auto_impersonate)
316 .with_ipc(self.ipc)
317 .with_code_size_limit(self.evm.code_size_limit)
318 .disable_code_size_limit(self.evm.disable_code_size_limit)
319 .set_pruned_history(self.prune_history)
320 .with_init_state(self.load_state.or_else(|| self.state.and_then(|s| s.state)))
321 .with_transaction_block_keeper(self.transaction_block_keeper)
322 .with_max_transactions(self.max_transactions)
323 .with_max_persisted_states(self.max_persisted_states)
324 .with_networks(networks)
325 .with_chain_id(self.evm.chain_id)
328 .with_disable_default_create2_deployer(self.evm.disable_default_create2_deployer)
329 .with_disable_pool_balance_checks(self.evm.disable_pool_balance_checks)
330 .with_slots_in_an_epoch(self.slots_in_an_epoch)
331 .with_memory_limit(self.evm.memory_limit)
332 .with_cache_path(self.cache_path)
333 .with_funded_accounts(funded_accounts))
334 }
335
336 fn parse_funded_accounts(&self) -> eyre::Result<HashMap<Address, U256>> {
337 let mut accounts = HashMap::default();
338 for entry in &self.fund_accounts {
339 let parts: Vec<&str> = entry.split(':').collect();
340 if parts.len() != 2 {
341 eyre::bail!(
342 "Invalid fund-accounts entry '{}'. Expected format: ADDRESS:AMOUNT",
343 entry
344 );
345 }
346 let address = parts[0]
347 .parse::<Address>()
348 .map_err(|e| eyre::eyre!("Invalid address '{}': {}", parts[0], e))?;
349 let amount: u64 = parts[1]
350 .parse()
351 .map_err(|e| eyre::eyre!("Invalid amount '{}': {}", parts[1], e))?;
352 let balance = Unit::ETHER.wei().saturating_mul(U256::from(amount));
353 accounts.insert(address, balance);
354 }
355 Ok(accounts)
356 }
357
358 fn account_generator(&self) -> AccountGenerator {
359 let mut generator = AccountGenerator::new(self.accounts as usize)
360 .phrase(DEFAULT_MNEMONIC)
361 .chain_id(self.evm.chain_id.unwrap_or(CHAIN_ID.into()));
362 if let Some(ref mnemonic) = self.mnemonic {
363 generator = generator.phrase(mnemonic);
364 } else if let Some(count) = self.mnemonic_random {
365 let mut rng = rand_08::thread_rng();
366 let mnemonic = match Mnemonic::<English>::new_with_count(&mut rng, count) {
367 Ok(mnemonic) => mnemonic.to_phrase(),
368 Err(err) => {
369 warn!(target: "node", ?count, %err, "failed to generate mnemonic, falling back to 12-word random mnemonic");
370 Mnemonic::<English>::new_with_count(&mut rng, 12)
373 .expect("valid default word count")
374 .to_phrase()
375 }
376 };
377 generator = generator.phrase(mnemonic);
378 } else if let Some(seed) = self.mnemonic_seed {
379 let mut seed = StdRng::seed_from_u64(seed);
380 let mnemonic = Mnemonic::<English>::new(&mut seed).to_phrase();
381 generator = generator.phrase(mnemonic);
382 }
383 if let Some(ref derivation) = self.derivation_path {
384 generator = generator.derivation_path(derivation);
385 }
386 generator
387 }
388
389 pub async fn run(self) -> eyre::Result<()> {
393 let dump_state =
394 self.dump_state.as_ref().or_else(|| self.state.as_ref().map(|s| &s.path)).cloned();
395 let dump_interval =
396 self.state_interval.map(Duration::from_secs).unwrap_or(DEFAULT_DUMP_INTERVAL);
397 let preserve_historical_states = self.preserve_historical_states;
398
399 let (api, mut handle) = crate::try_spawn(self.into_node_config()?).await?;
400
401 let mut fork = api.get_fork();
403 let running = Arc::new(AtomicUsize::new(0));
404
405 let mut signal = handle.shutdown_signal_mut().take();
408
409 let task_manager = handle.task_manager();
410 let mut on_shutdown = task_manager.on_shutdown();
411
412 let mut state_dumper =
413 PeriodicStateDumper::new(api, dump_state, dump_interval, preserve_historical_states);
414
415 task_manager.spawn(async move {
416 #[cfg(unix)]
418 let mut sigterm = Box::pin(async {
419 if let Ok(mut stream) =
420 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
421 {
422 stream.recv().await;
423 } else {
424 futures::future::pending::<()>().await;
425 }
426 });
427
428 #[cfg(not(unix))]
430 let mut sigterm = Box::pin(futures::future::pending::<()>());
431
432 tokio::select! {
434 _ = &mut sigterm => {
435 trace!("received sigterm signal, shutting down");
436 }
437 _ = &mut on_shutdown => {}
438 _ = &mut state_dumper => {}
439 }
440
441 state_dumper.dump().await;
443
444 if let Some(fork) = fork.take() {
447 trace!("flushing cache on shutdown");
448 fork.database
449 .read()
450 .await
451 .maybe_flush_cache()
452 .expect("Could not flush cache on fork DB");
453 }
456 std::process::exit(0);
457 });
458
459 ctrlc::set_handler(move || {
460 let prev = running.fetch_add(1, Ordering::SeqCst);
461 if prev == 0 {
462 trace!("received shutdown signal, shutting down");
463 let _ = signal.take();
464 }
465 })
466 .expect("Error setting Ctrl-C handler");
467
468 Ok(handle.await??)
469 }
470}
471
472#[derive(Clone, Debug, Parser)]
474#[command(next_help_heading = "EVM options")]
475pub struct AnvilEvmArgs {
476 #[arg(
484 long,
485 short,
486 visible_alias = "rpc-url",
487 value_name = "URL",
488 help_heading = "Fork config"
489 )]
490 pub fork_url: Vec<ForkUrl>,
491
492 #[arg(
496 long = "fork-header",
497 value_name = "HEADERS",
498 help_heading = "Fork config",
499 requires = "fork_url"
500 )]
501 pub fork_headers: Vec<String>,
502
503 #[arg(id = "timeout", long = "timeout", help_heading = "Fork config", requires = "fork_url")]
507 pub fork_request_timeout: Option<u64>,
508
509 #[arg(id = "retries", long = "retries", help_heading = "Fork config", requires = "fork_url")]
513 pub fork_request_retries: Option<u32>,
514
515 #[arg(
521 long,
522 requires = "fork_url",
523 value_name = "BLOCK",
524 help_heading = "Fork config",
525 allow_hyphen_values = true
526 )]
527 pub fork_block_number: Option<i128>,
528
529 #[arg(
533 long,
534 requires = "fork_url",
535 value_name = "TRANSACTION",
536 help_heading = "Fork config",
537 conflicts_with = "fork_block_number"
538 )]
539 pub fork_transaction_hash: Option<B256>,
540
541 #[arg(long, requires = "fork_url", value_name = "BACKOFF", help_heading = "Fork config")]
545 pub fork_retry_backoff: Option<u64>,
546
547 #[arg(
553 long,
554 help_heading = "Fork config",
555 value_name = "CHAIN",
556 requires = "fork_block_number"
557 )]
558 pub fork_chain_id: Option<Chain>,
559
560 #[arg(
566 long,
567 requires = "fork_url",
568 alias = "cups",
569 value_name = "CUPS",
570 help_heading = "Fork config"
571 )]
572 pub compute_units_per_second: Option<u64>,
573
574 #[arg(
580 long,
581 requires = "fork_url",
582 value_name = "NO_RATE_LIMITS",
583 help_heading = "Fork config",
584 visible_alias = "no-rpc-rate-limit"
585 )]
586 pub no_rate_limit: bool,
587
588 #[arg(long, requires = "fork_url", help_heading = "Fork config")]
596 pub no_storage_caching: bool,
597
598 #[arg(long, alias = "block-gas-limit", help_heading = "Environment config")]
600 pub gas_limit: Option<u64>,
601
602 #[arg(
604 long,
605 value_name = "DISABLE_GAS_LIMIT",
606 help_heading = "Environment config",
607 alias = "disable-gas-limit",
608 conflicts_with = "gas_limit"
609 )]
610 pub disable_block_gas_limit: bool,
611
612 #[arg(long, visible_alias = "tx-gas-limit", help_heading = "Environment config")]
614 pub enable_tx_gas_limit: bool,
615
616 #[arg(long, value_name = "CODE_SIZE", help_heading = "Environment config")]
619 pub code_size_limit: Option<usize>,
620
621 #[arg(
623 long,
624 value_name = "DISABLE_CODE_SIZE_LIMIT",
625 conflicts_with = "code_size_limit",
626 help_heading = "Environment config"
627 )]
628 pub disable_code_size_limit: bool,
629
630 #[arg(long, help_heading = "Environment config")]
632 pub gas_price: Option<u128>,
633
634 #[arg(
636 long,
637 visible_alias = "base-fee",
638 value_name = "FEE",
639 help_heading = "Environment config"
640 )]
641 pub block_base_fee_per_gas: Option<u64>,
642
643 #[arg(long, visible_alias = "no-priority-fee", help_heading = "Environment config")]
645 pub disable_min_priority_fee: bool,
646
647 #[arg(long, alias = "chain", help_heading = "Environment config")]
649 pub chain_id: Option<Chain>,
650
651 #[arg(long, visible_alias = "tracing")]
653 pub steps_tracing: bool,
654
655 #[arg(long, visible_alias = "no-console-log")]
657 pub disable_console_log: bool,
658
659 #[arg(long, visible_alias = "enable-trace-printing")]
661 pub print_traces: bool,
662
663 #[arg(long, visible_alias = "auto-unlock")]
666 pub auto_impersonate: bool,
667
668 #[arg(long, visible_alias = "no-create2")]
670 pub disable_default_create2_deployer: bool,
671
672 #[arg(long)]
674 pub disable_pool_balance_checks: bool,
675
676 #[arg(long)]
678 pub memory_limit: Option<u64>,
679
680 #[command(flatten)]
681 pub networks: NetworkConfigs,
682}
683
684impl AnvilEvmArgs {
691 pub fn resolve_rpc_alias(&mut self) {
692 if let Ok(config) = Config::load_with_providers(FigmentProviders::Anvil) {
693 let mut resolved_urls = Vec::new();
694 for fork_url in &self.fork_url {
695 let mut endpoints = config.rpc_endpoints.clone().resolved();
696 if let Some(endpoint) = endpoints.remove(&fork_url.url) {
697 match endpoint.all_urls() {
699 Ok(urls) => {
700 for (i, url) in urls.into_iter().enumerate() {
701 resolved_urls.push(ForkUrl {
702 url,
703 block: if i == 0 { fork_url.block } else { None },
705 });
706 }
707 }
708 Err(e) => {
709 warn!(target: "node", alias=%fork_url.url, %e, "could not resolve all endpoints, using primary endpoint only");
710 if let Ok(url) = endpoint.url() {
711 resolved_urls.push(ForkUrl { url, block: fork_url.block });
712 } else {
713 resolved_urls.push(fork_url.clone());
714 }
715 }
716 }
717 } else if let Some(Ok(url)) = config.get_rpc_url_with_alias(&fork_url.url) {
718 resolved_urls.push(ForkUrl { url: url.to_string(), block: fork_url.block });
720 } else {
721 resolved_urls.push(fork_url.clone());
723 }
724 }
725 self.fork_url = resolved_urls;
726 }
727 }
728}
729
730struct PeriodicStateDumper<N: Network> {
732 in_progress_dump: Option<Pin<Box<dyn Future<Output = ()> + Send + Sync + 'static>>>,
733 api: EthApi<N>,
734 dump_state: Option<PathBuf>,
735 preserve_historical_states: bool,
736 interval: Interval,
737}
738
739impl<N: Network<ReceiptEnvelope = FoundryReceiptEnvelope>> PeriodicStateDumper<N> {
740 fn new(
741 api: EthApi<N>,
742 dump_state: Option<PathBuf>,
743 interval: Duration,
744 preserve_historical_states: bool,
745 ) -> Self {
746 let dump_state = dump_state.map(|mut dump_state| {
747 if dump_state.is_dir() {
748 dump_state = dump_state.join("state.json");
749 }
750 dump_state
751 });
752
753 let interval = tokio::time::interval_at(Instant::now() + interval, interval);
755 Self { in_progress_dump: None, api, dump_state, preserve_historical_states, interval }
756 }
757
758 async fn dump(&self) {
759 if let Some(state) = self.dump_state.clone() {
760 Self::dump_state(self.api.clone(), state, self.preserve_historical_states).await
761 }
762 }
763
764 async fn dump_state(api: EthApi<N>, dump_state: PathBuf, preserve_historical_states: bool) {
766 trace!(path=?dump_state, "Dumping state on shutdown");
767 match api.serialized_state(preserve_historical_states).await {
768 Ok(state) => {
769 if let Err(err) = foundry_common::fs::write_json_file(&dump_state, &state) {
770 error!(?err, "Failed to dump state");
771 } else {
772 trace!(path=?dump_state, "Dumped state on shutdown");
773 }
774 }
775 Err(err) => {
776 error!(?err, "Failed to extract state");
777 }
778 }
779 }
780}
781
782impl<N: Network<ReceiptEnvelope = FoundryReceiptEnvelope>> Future for PeriodicStateDumper<N> {
784 type Output = ();
785
786 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
787 let this = self.get_mut();
788 if this.dump_state.is_none() {
789 return Poll::Pending;
790 }
791
792 loop {
793 if let Some(mut flush) = this.in_progress_dump.take() {
794 match flush.poll_unpin(cx) {
795 Poll::Ready(_) => {
796 this.interval.reset();
797 }
798 Poll::Pending => {
799 this.in_progress_dump = Some(flush);
800 return Poll::Pending;
801 }
802 }
803 }
804
805 if this.interval.poll_tick(cx).is_ready() {
806 let api = this.api.clone();
807 let path = this.dump_state.clone().expect("exists; see above");
808 this.in_progress_dump =
809 Some(Box::pin(Self::dump_state(api, path, this.preserve_historical_states)));
810 } else {
811 break;
812 }
813 }
814
815 Poll::Pending
816 }
817}
818
819#[derive(Clone, Debug)]
821pub struct StateFile {
822 pub path: PathBuf,
823 pub state: Option<SerializableState>,
824}
825
826impl StateFile {
827 fn parse(path: &str) -> Result<Self, String> {
830 Self::parse_path(path)
831 }
832
833 pub fn parse_path(path: impl AsRef<Path>) -> Result<Self, String> {
835 let mut path = path.as_ref().to_path_buf();
836 if path.is_dir() {
837 path = path.join("state.json");
838 }
839 let mut state = Self { path, state: None };
840 if !state.path.exists() {
841 return Ok(state);
842 }
843
844 state.state = Some(SerializableState::load(&state.path).map_err(|err| err.to_string())?);
845
846 Ok(state)
847 }
848}
849
850#[derive(Clone, Debug, PartialEq, Eq)]
853pub struct ForkUrl {
854 pub url: String,
856 pub block: Option<u64>,
858}
859
860impl fmt::Display for ForkUrl {
861 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
862 self.url.fmt(f)?;
863 if let Some(block) = self.block {
864 write!(f, "@{block}")?;
865 }
866 Ok(())
867 }
868}
869
870impl FromStr for ForkUrl {
871 type Err = String;
872
873 fn from_str(s: &str) -> Result<Self, Self::Err> {
874 if let Some((url, block)) = s.rsplit_once('@') {
875 if block == "latest" {
876 return Ok(Self { url: url.to_string(), block: None });
877 }
878 if !block.is_empty() && !block.contains(':') && !block.contains('.') {
880 let block: u64 = block
881 .parse()
882 .map_err(|_| format!("Failed to parse block number: `{block}`"))?;
883 return Ok(Self { url: url.to_string(), block: Some(block) });
884 }
885 }
886 Ok(Self { url: s.to_string(), block: None })
887 }
888}
889
890fn parse_hardfork(hf: &str, networks: &NetworkConfigs) -> eyre::Result<FoundryHardfork> {
892 if let Ok(hardfork) = FoundryHardfork::from_str(hf) {
893 networks.normalize_for_hardfork(hardfork).map_err(eyre::Report::msg)?;
894 return Ok(hardfork);
895 }
896
897 #[cfg(feature = "optimism")]
898 if networks.is_optimism() {
899 return Ok(OpHardfork::from_str(hf)?.into());
900 }
901 if networks.is_tempo() {
902 Ok(TempoHardfork::from_str(hf)?.into())
903 } else {
904 Ok(EthereumHardfork::from_str(hf)?.into())
905 }
906}
907
908fn read_genesis_file(path: &str) -> Result<Genesis, String> {
910 foundry_common::fs::read_json_file(path.as_ref()).map_err(|err| err.to_string())
911}
912
913fn duration_from_secs_f64(s: &str) -> Result<Duration, String> {
914 let s = s.parse::<f64>().map_err(|e| e.to_string())?;
915 if s == 0.0 {
916 return Err("Duration must be greater than 0".to_string());
917 }
918 Duration::try_from_secs_f64(s).map_err(|e| e.to_string())
919}
920
921#[cfg(test)]
922mod tests {
923 use super::*;
924 use std::{env, net::Ipv4Addr};
925
926 #[test]
927 fn test_parse_fork_url() {
928 let fork: ForkUrl = "http://localhost:8545@1000000".parse().unwrap();
929 assert_eq!(
930 fork,
931 ForkUrl { url: "http://localhost:8545".to_string(), block: Some(1000000) }
932 );
933
934 let fork: ForkUrl = "http://localhost:8545".parse().unwrap();
935 assert_eq!(fork, ForkUrl { url: "http://localhost:8545".to_string(), block: None });
936
937 let fork: ForkUrl = "wss://user:password@example.com/".parse().unwrap();
938 assert_eq!(
939 fork,
940 ForkUrl { url: "wss://user:password@example.com/".to_string(), block: None }
941 );
942
943 let fork: ForkUrl = "wss://user:password@example.com/@latest".parse().unwrap();
944 assert_eq!(
945 fork,
946 ForkUrl { url: "wss://user:password@example.com/".to_string(), block: None }
947 );
948
949 let fork: ForkUrl = "wss://user:password@example.com/@100000".parse().unwrap();
950 assert_eq!(
951 fork,
952 ForkUrl { url: "wss://user:password@example.com/".to_string(), block: Some(100000) }
953 );
954 }
955
956 #[test]
957 fn can_parse_ethereum_hardfork() {
958 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--hardfork", "berlin"]);
959 let config = args.into_node_config().unwrap();
960 assert_eq!(config.hardfork, Some(EthereumHardfork::Berlin.into()));
961 }
962
963 #[test]
964 fn can_parse_optimism_hardfork() {
965 let args: NodeArgs =
966 NodeArgs::parse_from(["anvil", "--optimism", "--hardfork", "Regolith"]);
967 let config = args.into_node_config().unwrap();
968 assert_eq!(config.hardfork, Some(OpHardfork::Regolith.into()));
969 }
970
971 #[test]
972 fn can_parse_tempo_hardfork_from_network() {
973 let args: NodeArgs =
974 NodeArgs::parse_from(["anvil", "--network", "tempo", "--hardfork", "T5"]);
975 let config = args.into_node_config().unwrap();
976
977 assert!(config.networks.is_tempo());
978 assert_eq!(config.hardfork, Some(TempoHardfork::T5.into()));
979 }
980
981 #[test]
982 fn can_parse_namespaced_tempo_hardfork() {
983 let args = NodeArgs::parse_from(["anvil", "--hardfork", "tempo:T5"]);
984 let config = args.into_node_config().unwrap();
985
986 assert!(config.networks.is_tempo());
987 assert_eq!(config.hardfork, Some(TempoHardfork::T5.into()));
988 }
989
990 #[cfg(feature = "optimism")]
991 #[test]
992 fn chain_id_infers_optimism_network_in_node_config() {
993 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--chain-id", "10"]);
994 let config = args.into_node_config().unwrap();
995
996 assert!(config.networks.is_optimism());
997 }
998
999 #[test]
1000 fn chain_id_infers_tempo_network_for_hardfork() {
1001 let args = NodeArgs::parse_from(["anvil", "--chain-id", "4217", "--hardfork", "T5"]);
1002 let config = args.into_node_config().unwrap();
1003
1004 assert!(config.networks.is_tempo());
1005 assert_eq!(config.hardfork, Some(TempoHardfork::T5.into()));
1006 }
1007
1008 #[test]
1009 fn fork_chain_id_infers_tempo_network_for_hardfork() {
1010 let args = NodeArgs::parse_from([
1011 "anvil",
1012 "--fork-url",
1013 "http://localhost:8545",
1014 "--fork-block-number",
1015 "1",
1016 "--fork-chain-id",
1017 "4217",
1018 "--hardfork",
1019 "T5",
1020 ]);
1021 let config = args.into_node_config().unwrap();
1022
1023 assert!(config.networks.is_tempo());
1024 assert_eq!(config.hardfork, Some(TempoHardfork::T5.into()));
1025 }
1026
1027 #[test]
1028 fn cant_parse_invalid_hardfork() {
1029 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--hardfork", "Regolith"]);
1030 let config = args.into_node_config();
1031 assert!(config.is_err());
1032 }
1033
1034 #[test]
1035 fn can_parse_fork_headers() {
1036 let args: NodeArgs = NodeArgs::parse_from([
1037 "anvil",
1038 "--fork-url",
1039 "http,://localhost:8545",
1040 "--fork-header",
1041 "User-Agent: test-agent",
1042 "--fork-header",
1043 "Referrer: example.com",
1044 ]);
1045 assert_eq!(args.evm.fork_headers, vec!["User-Agent: test-agent", "Referrer: example.com"]);
1046 }
1047
1048 #[test]
1049 fn can_parse_prune_config() {
1050 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--prune-history"]);
1051 assert!(args.prune_history.is_some());
1052
1053 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--prune-history", "100"]);
1054 assert_eq!(args.prune_history, Some(Some(100)));
1055 }
1056
1057 #[test]
1058 fn can_parse_max_persisted_states_config() {
1059 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--max-persisted-states", "500"]);
1060 assert_eq!(args.max_persisted_states, (Some(500)));
1061 }
1062
1063 #[test]
1064 fn can_parse_disable_block_gas_limit() {
1065 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--disable-block-gas-limit"]);
1066 assert!(args.evm.disable_block_gas_limit);
1067
1068 let args =
1069 NodeArgs::try_parse_from(["anvil", "--disable-block-gas-limit", "--gas-limit", "100"]);
1070 assert!(args.is_err());
1071 }
1072
1073 #[test]
1074 fn can_parse_enable_tx_gas_limit() {
1075 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--enable-tx-gas-limit"]);
1076 assert!(args.evm.enable_tx_gas_limit);
1077
1078 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--tx-gas-limit"]);
1080 assert!(args.evm.enable_tx_gas_limit);
1081 }
1082
1083 #[test]
1084 fn can_parse_disable_code_size_limit() {
1085 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--disable-code-size-limit"]);
1086 assert!(args.evm.disable_code_size_limit);
1087
1088 let args = NodeArgs::try_parse_from([
1089 "anvil",
1090 "--disable-code-size-limit",
1091 "--code-size-limit",
1092 "100",
1093 ]);
1094 assert!(args.is_err());
1096 }
1097
1098 #[test]
1099 fn can_parse_host() {
1100 let args = NodeArgs::parse_from(["anvil"]);
1101 assert_eq!(args.host, vec![IpAddr::V4(Ipv4Addr::LOCALHOST)]);
1102
1103 let args = NodeArgs::parse_from([
1104 "anvil", "--host", "::1", "--host", "1.1.1.1", "--host", "2.2.2.2",
1105 ]);
1106 assert_eq!(
1107 args.host,
1108 ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::<IpAddr>().unwrap()).to_vec()
1109 );
1110
1111 let args = NodeArgs::parse_from(["anvil", "--host", "::1,1.1.1.1,2.2.2.2"]);
1112 assert_eq!(
1113 args.host,
1114 ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::<IpAddr>().unwrap()).to_vec()
1115 );
1116
1117 unsafe { env::set_var("ANVIL_IP_ADDR", "1.1.1.1") };
1118 let args = NodeArgs::parse_from(["anvil"]);
1119 assert_eq!(args.host, vec!["1.1.1.1".parse::<IpAddr>().unwrap()]);
1120
1121 unsafe { env::set_var("ANVIL_IP_ADDR", "::1,1.1.1.1,2.2.2.2") };
1122 let args = NodeArgs::parse_from(["anvil"]);
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
1129 #[test]
1130 fn can_parse_multiple_fork_urls() {
1131 let args: NodeArgs = NodeArgs::parse_from([
1132 "anvil",
1133 "--fork-url",
1134 "http://localhost:8545",
1135 "--fork-url",
1136 "http://localhost:8546",
1137 "--fork-url",
1138 "http://localhost:8547",
1139 ]);
1140 assert_eq!(args.evm.fork_url.len(), 3);
1141 assert_eq!(args.evm.fork_url[0].url, "http://localhost:8545");
1142 assert_eq!(args.evm.fork_url[1].url, "http://localhost:8546");
1143 assert_eq!(args.evm.fork_url[2].url, "http://localhost:8547");
1144
1145 let args: NodeArgs = NodeArgs::parse_from([
1147 "anvil",
1148 "--fork-url",
1149 "http://localhost:8545@1000000",
1150 "--fork-url",
1151 "http://localhost:8546",
1152 ]);
1153 assert_eq!(args.evm.fork_url[0].block, Some(1000000));
1154 assert_eq!(args.evm.fork_url[1].block, None);
1155 }
1156
1157 #[test]
1158 fn rejects_block_suffix_on_secondary_fork_urls() {
1159 let args: NodeArgs = NodeArgs::parse_from([
1160 "anvil",
1161 "--fork-url",
1162 "http://localhost:8545@1000000",
1163 "--fork-url",
1164 "http://localhost:8546@2000000",
1165 ]);
1166 let result = args.into_node_config();
1167 assert!(result.is_err());
1168 assert!(
1169 result.unwrap_err().to_string().contains("Block number suffixes"),
1170 "should reject block suffix on secondary fork URL"
1171 );
1172 }
1173
1174 #[test]
1175 fn fork_dependent_args_require_fork_url() {
1176 let cases = [
1178 vec!["anvil", "--fork-header", "X-Api-Key: test"],
1179 vec!["anvil", "--timeout", "5000"],
1180 vec!["anvil", "--retries", "3"],
1181 vec!["anvil", "--fork-block-number", "100"],
1182 vec!["anvil", "--fork-retry-backoff", "500"],
1183 ];
1184 for args in &cases {
1185 let result = NodeArgs::try_parse_from(args);
1186 assert!(result.is_err(), "expected error when using {:?} without --fork-url", args[1]);
1187 }
1188 }
1189}