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 fn dump_state_path(&self) -> Option<PathBuf> {
391 self.dump_state.as_ref().or_else(|| self.state.as_ref().map(|s| &s.path)).cloned()
392 }
393
394 pub async fn run(self) -> eyre::Result<()> {
398 let dump_state = self.dump_state_path();
399 let dump_interval =
400 self.state_interval.map(Duration::from_secs).unwrap_or(DEFAULT_DUMP_INTERVAL);
401 let preserve_historical_states = self.preserve_historical_states;
402
403 let (api, mut handle) = crate::try_spawn(self.into_node_config()?).await?;
404
405 let mut fork = api.get_fork();
407 let running = Arc::new(AtomicUsize::new(0));
408
409 let mut signal = handle.shutdown_signal_mut().take();
412
413 let task_manager = handle.task_manager();
414 let mut on_shutdown = task_manager.on_shutdown();
415
416 let mut state_dumper =
417 PeriodicStateDumper::new(api, dump_state, dump_interval, preserve_historical_states);
418
419 task_manager.spawn(async move {
420 #[cfg(unix)]
422 let mut sigterm = Box::pin(async {
423 if let Ok(mut stream) =
424 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
425 {
426 stream.recv().await;
427 } else {
428 futures::future::pending::<()>().await;
429 }
430 });
431
432 #[cfg(not(unix))]
434 let mut sigterm = Box::pin(futures::future::pending::<()>());
435
436 tokio::select! {
438 _ = &mut sigterm => {
439 trace!("received sigterm signal, shutting down");
440 }
441 _ = &mut on_shutdown => {}
442 _ = &mut state_dumper => {}
443 }
444
445 state_dumper.dump().await;
447
448 if let Some(fork) = fork.take() {
451 trace!("flushing cache on shutdown");
452 fork.database
453 .read()
454 .await
455 .maybe_flush_cache()
456 .expect("Could not flush cache on fork DB");
457 }
460 std::process::exit(0);
461 });
462
463 ctrlc::set_handler(move || {
464 let prev = running.fetch_add(1, Ordering::SeqCst);
465 if prev == 0 {
466 trace!("received shutdown signal, shutting down");
467 let _ = signal.take();
468 }
469 })
470 .expect("Error setting Ctrl-C handler");
471
472 Ok(handle.await??)
473 }
474}
475
476#[derive(Clone, Debug, Parser)]
478#[command(next_help_heading = "EVM options")]
479pub struct AnvilEvmArgs {
480 #[arg(
488 long,
489 short,
490 visible_alias = "rpc-url",
491 value_name = "URL",
492 help_heading = "Fork config"
493 )]
494 pub fork_url: Vec<ForkUrl>,
495
496 #[arg(
500 long = "fork-header",
501 value_name = "HEADERS",
502 help_heading = "Fork config",
503 requires = "fork_url"
504 )]
505 pub fork_headers: Vec<String>,
506
507 #[arg(id = "timeout", long = "timeout", help_heading = "Fork config", requires = "fork_url")]
511 pub fork_request_timeout: Option<u64>,
512
513 #[arg(id = "retries", long = "retries", help_heading = "Fork config", requires = "fork_url")]
517 pub fork_request_retries: Option<u32>,
518
519 #[arg(
525 long,
526 requires = "fork_url",
527 value_name = "BLOCK",
528 help_heading = "Fork config",
529 allow_hyphen_values = true
530 )]
531 pub fork_block_number: Option<i128>,
532
533 #[arg(
537 long,
538 requires = "fork_url",
539 value_name = "TRANSACTION",
540 help_heading = "Fork config",
541 conflicts_with = "fork_block_number"
542 )]
543 pub fork_transaction_hash: Option<B256>,
544
545 #[arg(long, requires = "fork_url", value_name = "BACKOFF", help_heading = "Fork config")]
549 pub fork_retry_backoff: Option<u64>,
550
551 #[arg(
557 long,
558 help_heading = "Fork config",
559 value_name = "CHAIN",
560 requires = "fork_block_number"
561 )]
562 pub fork_chain_id: Option<Chain>,
563
564 #[arg(
570 long,
571 requires = "fork_url",
572 alias = "cups",
573 value_name = "CUPS",
574 help_heading = "Fork config"
575 )]
576 pub compute_units_per_second: Option<u64>,
577
578 #[arg(
584 long,
585 requires = "fork_url",
586 value_name = "NO_RATE_LIMITS",
587 help_heading = "Fork config",
588 visible_alias = "no-rpc-rate-limit"
589 )]
590 pub no_rate_limit: bool,
591
592 #[arg(long, requires = "fork_url", help_heading = "Fork config")]
600 pub no_storage_caching: bool,
601
602 #[arg(long, alias = "block-gas-limit", help_heading = "Environment config")]
604 pub gas_limit: Option<u64>,
605
606 #[arg(
608 long,
609 value_name = "DISABLE_GAS_LIMIT",
610 help_heading = "Environment config",
611 alias = "disable-gas-limit",
612 conflicts_with = "gas_limit"
613 )]
614 pub disable_block_gas_limit: bool,
615
616 #[arg(long, visible_alias = "tx-gas-limit", help_heading = "Environment config")]
618 pub enable_tx_gas_limit: bool,
619
620 #[arg(long, value_name = "CODE_SIZE", help_heading = "Environment config")]
623 pub code_size_limit: Option<usize>,
624
625 #[arg(
627 long,
628 value_name = "DISABLE_CODE_SIZE_LIMIT",
629 conflicts_with = "code_size_limit",
630 help_heading = "Environment config"
631 )]
632 pub disable_code_size_limit: bool,
633
634 #[arg(long, help_heading = "Environment config")]
636 pub gas_price: Option<u128>,
637
638 #[arg(
640 long,
641 visible_alias = "base-fee",
642 value_name = "FEE",
643 help_heading = "Environment config"
644 )]
645 pub block_base_fee_per_gas: Option<u64>,
646
647 #[arg(long, visible_alias = "no-priority-fee", help_heading = "Environment config")]
649 pub disable_min_priority_fee: bool,
650
651 #[arg(long, alias = "chain", help_heading = "Environment config")]
653 pub chain_id: Option<Chain>,
654
655 #[arg(long, visible_alias = "tracing")]
657 pub steps_tracing: bool,
658
659 #[arg(long, visible_alias = "no-console-log")]
661 pub disable_console_log: bool,
662
663 #[arg(long, visible_alias = "enable-trace-printing")]
665 pub print_traces: bool,
666
667 #[arg(long, visible_alias = "auto-unlock")]
670 pub auto_impersonate: bool,
671
672 #[arg(long, visible_alias = "no-create2")]
674 pub disable_default_create2_deployer: bool,
675
676 #[arg(long)]
678 pub disable_pool_balance_checks: bool,
679
680 #[arg(long)]
682 pub memory_limit: Option<u64>,
683
684 #[command(flatten)]
685 pub networks: NetworkConfigs,
686}
687
688impl AnvilEvmArgs {
695 pub fn resolve_rpc_alias(&mut self) {
696 if let Ok(config) = Config::load_with_providers(FigmentProviders::Anvil) {
697 let mut resolved_urls = Vec::new();
698 for fork_url in &self.fork_url {
699 let mut endpoints = config.rpc_endpoints.clone().resolved();
700 if let Some(endpoint) = endpoints.remove(&fork_url.url) {
701 match endpoint.all_urls() {
703 Ok(urls) => {
704 for (i, url) in urls.into_iter().enumerate() {
705 resolved_urls.push(ForkUrl {
706 url,
707 block: if i == 0 { fork_url.block } else { None },
709 });
710 }
711 }
712 Err(e) => {
713 warn!(target: "node", alias=%fork_url.url, %e, "could not resolve all endpoints, using primary endpoint only");
714 if let Ok(url) = endpoint.url() {
715 resolved_urls.push(ForkUrl { url, block: fork_url.block });
716 } else {
717 resolved_urls.push(fork_url.clone());
718 }
719 }
720 }
721 } else if let Some(Ok(url)) = config.get_rpc_url_with_alias(&fork_url.url) {
722 resolved_urls.push(ForkUrl { url: url.to_string(), block: fork_url.block });
724 } else {
725 resolved_urls.push(fork_url.clone());
727 }
728 }
729 self.fork_url = resolved_urls;
730 }
731 }
732}
733
734struct PeriodicStateDumper<N: Network> {
736 in_progress_dump: Option<Pin<Box<dyn Future<Output = ()> + Send + Sync + 'static>>>,
737 api: EthApi<N>,
738 dump_state: Option<PathBuf>,
739 preserve_historical_states: bool,
740 interval: Interval,
741}
742
743impl<N: Network<ReceiptEnvelope = FoundryReceiptEnvelope>> PeriodicStateDumper<N> {
744 fn new(
745 api: EthApi<N>,
746 dump_state: Option<PathBuf>,
747 interval: Duration,
748 preserve_historical_states: bool,
749 ) -> Self {
750 let dump_state = dump_state.map(|mut dump_state| {
751 if dump_state.is_dir() {
752 dump_state = dump_state.join("state.json");
753 }
754 dump_state
755 });
756
757 let interval = tokio::time::interval_at(Instant::now() + interval, interval);
759 Self { in_progress_dump: None, api, dump_state, preserve_historical_states, interval }
760 }
761
762 async fn dump(&self) {
763 if let Some(state) = self.dump_state.clone() {
764 Self::dump_state(self.api.clone(), state, self.preserve_historical_states).await
765 }
766 }
767
768 async fn dump_state(api: EthApi<N>, dump_state: PathBuf, preserve_historical_states: bool) {
770 trace!(path=?dump_state, "Dumping state on shutdown");
771 match api.serialized_state(preserve_historical_states).await {
772 Ok(state) => {
773 if let Err(err) = foundry_common::fs::write_json_file(&dump_state, &state) {
774 error!(?err, "Failed to dump state");
775 } else {
776 trace!(path=?dump_state, "Dumped state on shutdown");
777 }
778 }
779 Err(err) => {
780 error!(?err, "Failed to extract state");
781 }
782 }
783 }
784}
785
786impl<N: Network<ReceiptEnvelope = FoundryReceiptEnvelope>> Future for PeriodicStateDumper<N> {
788 type Output = ();
789
790 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
791 let this = self.get_mut();
792 if this.dump_state.is_none() {
793 return Poll::Pending;
794 }
795
796 loop {
797 if let Some(mut flush) = this.in_progress_dump.take() {
798 match flush.poll_unpin(cx) {
799 Poll::Ready(_) => {
800 this.interval.reset();
801 }
802 Poll::Pending => {
803 this.in_progress_dump = Some(flush);
804 return Poll::Pending;
805 }
806 }
807 }
808
809 if this.interval.poll_tick(cx).is_ready() {
810 let api = this.api.clone();
811 let path = this.dump_state.clone().expect("exists; see above");
812 this.in_progress_dump =
813 Some(Box::pin(Self::dump_state(api, path, this.preserve_historical_states)));
814 } else {
815 break;
816 }
817 }
818
819 Poll::Pending
820 }
821}
822
823#[derive(Clone, Debug)]
825pub struct StateFile {
826 pub path: PathBuf,
827 pub state: Option<SerializableState>,
828}
829
830impl StateFile {
831 fn parse(path: &str) -> Result<Self, String> {
834 Self::parse_path(path)
835 }
836
837 pub fn parse_path(path: impl AsRef<Path>) -> Result<Self, String> {
839 let mut path = path.as_ref().to_path_buf();
840 if path.is_dir() {
841 path = path.join("state.json");
842 }
843 let mut state = Self { path, state: None };
844 if !state.path.exists() {
845 return Ok(state);
846 }
847
848 state.state = Some(SerializableState::load(&state.path).map_err(|err| err.to_string())?);
849
850 Ok(state)
851 }
852}
853
854#[derive(Clone, Debug, PartialEq, Eq)]
857pub struct ForkUrl {
858 pub url: String,
860 pub block: Option<u64>,
862}
863
864impl fmt::Display for ForkUrl {
865 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
866 self.url.fmt(f)?;
867 if let Some(block) = self.block {
868 write!(f, "@{block}")?;
869 }
870 Ok(())
871 }
872}
873
874impl FromStr for ForkUrl {
875 type Err = String;
876
877 fn from_str(s: &str) -> Result<Self, Self::Err> {
878 if let Some((url, block)) = s.rsplit_once('@') {
879 if block == "latest" {
880 return Ok(Self { url: url.to_string(), block: None });
881 }
882 if !block.is_empty() && !block.contains(':') && !block.contains('.') {
884 let block: u64 = block
885 .parse()
886 .map_err(|_| format!("Failed to parse block number: `{block}`"))?;
887 return Ok(Self { url: url.to_string(), block: Some(block) });
888 }
889 }
890 Ok(Self { url: s.to_string(), block: None })
891 }
892}
893
894fn parse_hardfork(hf: &str, networks: &NetworkConfigs) -> eyre::Result<FoundryHardfork> {
896 if let Ok(hardfork) = FoundryHardfork::from_str(hf) {
897 networks.normalize_for_hardfork(hardfork).map_err(eyre::Report::msg)?;
898 return Ok(hardfork);
899 }
900
901 #[cfg(feature = "optimism")]
902 if networks.is_optimism() {
903 return Ok(OpHardfork::from_str(hf)?.into());
904 }
905 if networks.is_tempo() {
906 Ok(TempoHardfork::from_str(hf)?.into())
907 } else {
908 Ok(EthereumHardfork::from_str(hf)?.into())
909 }
910}
911
912fn read_genesis_file(path: &str) -> Result<Genesis, String> {
914 foundry_common::fs::read_json_file(path.as_ref()).map_err(|err| err.to_string())
915}
916
917fn duration_from_secs_f64(s: &str) -> Result<Duration, String> {
918 let s = s.parse::<f64>().map_err(|e| e.to_string())?;
919 if s == 0.0 {
920 return Err("Duration must be greater than 0".to_string());
921 }
922 Duration::try_from_secs_f64(s).map_err(|e| e.to_string())
923}
924
925#[cfg(test)]
926mod tests {
927 use super::*;
928 use std::{env, net::Ipv4Addr};
929
930 #[test]
931 fn test_parse_fork_url() {
932 let fork: ForkUrl = "http://localhost:8545@1000000".parse().unwrap();
933 assert_eq!(
934 fork,
935 ForkUrl { url: "http://localhost:8545".to_string(), block: Some(1000000) }
936 );
937
938 let fork: ForkUrl = "http://localhost:8545".parse().unwrap();
939 assert_eq!(fork, ForkUrl { url: "http://localhost:8545".to_string(), block: None });
940
941 let fork: ForkUrl = "wss://user:password@example.com/".parse().unwrap();
942 assert_eq!(
943 fork,
944 ForkUrl { url: "wss://user:password@example.com/".to_string(), block: None }
945 );
946
947 let fork: ForkUrl = "wss://user:password@example.com/@latest".parse().unwrap();
948 assert_eq!(
949 fork,
950 ForkUrl { url: "wss://user:password@example.com/".to_string(), block: None }
951 );
952
953 let fork: ForkUrl = "wss://user:password@example.com/@100000".parse().unwrap();
954 assert_eq!(
955 fork,
956 ForkUrl { url: "wss://user:password@example.com/".to_string(), block: Some(100000) }
957 );
958 }
959
960 #[test]
961 fn can_parse_ethereum_hardfork() {
962 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--hardfork", "berlin"]);
963 let config = args.into_node_config().unwrap();
964 assert_eq!(config.hardfork, Some(EthereumHardfork::Berlin.into()));
965 }
966
967 #[test]
968 fn can_parse_optimism_hardfork() {
969 let args: NodeArgs =
970 NodeArgs::parse_from(["anvil", "--optimism", "--hardfork", "Regolith"]);
971 let config = args.into_node_config().unwrap();
972 assert_eq!(config.hardfork, Some(OpHardfork::Regolith.into()));
973 }
974
975 #[test]
976 fn can_parse_tempo_hardfork_from_network() {
977 let args: NodeArgs =
978 NodeArgs::parse_from(["anvil", "--network", "tempo", "--hardfork", "T5"]);
979 let config = args.into_node_config().unwrap();
980
981 assert!(config.networks.is_tempo());
982 assert_eq!(config.hardfork, Some(TempoHardfork::T5.into()));
983 }
984
985 #[test]
986 fn can_parse_namespaced_tempo_hardfork() {
987 let args = NodeArgs::parse_from(["anvil", "--hardfork", "tempo:T5"]);
988 let config = args.into_node_config().unwrap();
989
990 assert!(config.networks.is_tempo());
991 assert_eq!(config.hardfork, Some(TempoHardfork::T5.into()));
992 }
993
994 #[cfg(feature = "optimism")]
995 #[test]
996 fn chain_id_infers_optimism_network_in_node_config() {
997 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--chain-id", "10"]);
998 let config = args.into_node_config().unwrap();
999
1000 assert!(config.networks.is_optimism());
1001 }
1002
1003 #[test]
1004 fn chain_id_infers_tempo_network_for_hardfork() {
1005 let args = NodeArgs::parse_from(["anvil", "--chain-id", "4217", "--hardfork", "T5"]);
1006 let config = args.into_node_config().unwrap();
1007
1008 assert!(config.networks.is_tempo());
1009 assert_eq!(config.hardfork, Some(TempoHardfork::T5.into()));
1010 }
1011
1012 #[test]
1013 fn fork_chain_id_infers_tempo_network_for_hardfork() {
1014 let args = NodeArgs::parse_from([
1015 "anvil",
1016 "--fork-url",
1017 "http://localhost:8545",
1018 "--fork-block-number",
1019 "1",
1020 "--fork-chain-id",
1021 "4217",
1022 "--hardfork",
1023 "T5",
1024 ]);
1025 let config = args.into_node_config().unwrap();
1026
1027 assert!(config.networks.is_tempo());
1028 assert_eq!(config.hardfork, Some(TempoHardfork::T5.into()));
1029 }
1030
1031 #[test]
1032 fn cant_parse_invalid_hardfork() {
1033 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--hardfork", "Regolith"]);
1034 let config = args.into_node_config();
1035 assert!(config.is_err());
1036 }
1037
1038 #[test]
1039 fn can_parse_fork_headers() {
1040 let args: NodeArgs = NodeArgs::parse_from([
1041 "anvil",
1042 "--fork-url",
1043 "http,://localhost:8545",
1044 "--fork-header",
1045 "User-Agent: test-agent",
1046 "--fork-header",
1047 "Referrer: example.com",
1048 ]);
1049 assert_eq!(args.evm.fork_headers, vec!["User-Agent: test-agent", "Referrer: example.com"]);
1050 }
1051
1052 #[test]
1053 fn can_parse_prune_config() {
1054 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--prune-history"]);
1055 assert!(args.prune_history.is_some());
1056
1057 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--prune-history", "100"]);
1058 assert_eq!(args.prune_history, Some(Some(100)));
1059 }
1060
1061 #[test]
1062 fn can_parse_max_persisted_states_config() {
1063 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--max-persisted-states", "500"]);
1064 assert_eq!(args.max_persisted_states, (Some(500)));
1065 }
1066
1067 #[test]
1068 fn can_parse_disable_block_gas_limit() {
1069 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--disable-block-gas-limit"]);
1070 assert!(args.evm.disable_block_gas_limit);
1071
1072 let args =
1073 NodeArgs::try_parse_from(["anvil", "--disable-block-gas-limit", "--gas-limit", "100"]);
1074 assert!(args.is_err());
1075 }
1076
1077 #[test]
1078 fn can_parse_enable_tx_gas_limit() {
1079 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--enable-tx-gas-limit"]);
1080 assert!(args.evm.enable_tx_gas_limit);
1081
1082 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--tx-gas-limit"]);
1084 assert!(args.evm.enable_tx_gas_limit);
1085 }
1086
1087 #[test]
1088 fn can_parse_disable_code_size_limit() {
1089 let args: NodeArgs = NodeArgs::parse_from(["anvil", "--disable-code-size-limit"]);
1090 assert!(args.evm.disable_code_size_limit);
1091
1092 let args = NodeArgs::try_parse_from([
1093 "anvil",
1094 "--disable-code-size-limit",
1095 "--code-size-limit",
1096 "100",
1097 ]);
1098 assert!(args.is_err());
1100 }
1101
1102 #[test]
1103 fn can_parse_host() {
1104 let args = NodeArgs::parse_from(["anvil"]);
1105 assert_eq!(args.host, vec![IpAddr::V4(Ipv4Addr::LOCALHOST)]);
1106
1107 let args = NodeArgs::parse_from([
1108 "anvil", "--host", "::1", "--host", "1.1.1.1", "--host", "2.2.2.2",
1109 ]);
1110 assert_eq!(
1111 args.host,
1112 ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::<IpAddr>().unwrap()).to_vec()
1113 );
1114
1115 let args = NodeArgs::parse_from(["anvil", "--host", "::1,1.1.1.1,2.2.2.2"]);
1116 assert_eq!(
1117 args.host,
1118 ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::<IpAddr>().unwrap()).to_vec()
1119 );
1120
1121 unsafe { env::set_var("ANVIL_IP_ADDR", "1.1.1.1") };
1122 let args = NodeArgs::parse_from(["anvil"]);
1123 assert_eq!(args.host, vec!["1.1.1.1".parse::<IpAddr>().unwrap()]);
1124
1125 unsafe { env::set_var("ANVIL_IP_ADDR", "::1,1.1.1.1,2.2.2.2") };
1126 let args = NodeArgs::parse_from(["anvil"]);
1127 assert_eq!(
1128 args.host,
1129 ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::<IpAddr>().unwrap()).to_vec()
1130 );
1131 }
1132
1133 #[test]
1134 fn can_parse_multiple_fork_urls() {
1135 let args: NodeArgs = NodeArgs::parse_from([
1136 "anvil",
1137 "--fork-url",
1138 "http://localhost:8545",
1139 "--fork-url",
1140 "http://localhost:8546",
1141 "--fork-url",
1142 "http://localhost:8547",
1143 ]);
1144 assert_eq!(args.evm.fork_url.len(), 3);
1145 assert_eq!(args.evm.fork_url[0].url, "http://localhost:8545");
1146 assert_eq!(args.evm.fork_url[1].url, "http://localhost:8546");
1147 assert_eq!(args.evm.fork_url[2].url, "http://localhost:8547");
1148
1149 let args: NodeArgs = NodeArgs::parse_from([
1151 "anvil",
1152 "--fork-url",
1153 "http://localhost:8545@1000000",
1154 "--fork-url",
1155 "http://localhost:8546",
1156 ]);
1157 assert_eq!(args.evm.fork_url[0].block, Some(1000000));
1158 assert_eq!(args.evm.fork_url[1].block, None);
1159 }
1160
1161 #[test]
1162 fn rejects_block_suffix_on_secondary_fork_urls() {
1163 let args: NodeArgs = NodeArgs::parse_from([
1164 "anvil",
1165 "--fork-url",
1166 "http://localhost:8545@1000000",
1167 "--fork-url",
1168 "http://localhost:8546@2000000",
1169 ]);
1170 let result = args.into_node_config();
1171 assert!(result.is_err());
1172 assert!(
1173 result.unwrap_err().to_string().contains("Block number suffixes"),
1174 "should reject block suffix on secondary fork URL"
1175 );
1176 }
1177
1178 #[test]
1179 fn fork_dependent_args_require_fork_url() {
1180 let cases = [
1182 vec!["anvil", "--fork-header", "X-Api-Key: test"],
1183 vec!["anvil", "--timeout", "5000"],
1184 vec!["anvil", "--retries", "3"],
1185 vec!["anvil", "--fork-block-number", "100"],
1186 vec!["anvil", "--fork-retry-backoff", "500"],
1187 ];
1188 for args in &cases {
1189 let result = NodeArgs::try_parse_from(args);
1190 assert!(result.is_err(), "expected error when using {:?} without --fork-url", args[1]);
1191 }
1192 }
1193}