Skip to main content

anvil/
cmd.rs

1use crate::{
2    AccountGenerator, CHAIN_ID, NodeConfig,
3    config::{DEFAULT_MNEMONIC, DEFAULT_SLOTS_IN_AN_EPOCH, ForkChoice},
4    eth::{EthApi, backend::db::SerializableState, pool::transactions::TransactionOrder},
5};
6use alloy_genesis::Genesis;
7use alloy_network::Network;
8use alloy_primitives::{Address, B256, U256, map::HashMap, utils::Unit};
9use alloy_signer_local::coins_bip39::{English, Mnemonic};
10use anvil_server::ServerConfig;
11use clap::Parser;
12use core::fmt;
13use foundry_common::shell;
14use foundry_config::{Chain, Config, FigmentProviders};
15use foundry_evm::hardfork::FoundryHardfork;
16use foundry_evm_networks::NetworkConfigs;
17use foundry_primitives::FoundryReceiptEnvelope;
18use futures::FutureExt;
19use rand_08::{SeedableRng, rngs::StdRng};
20use std::{
21    net::IpAddr,
22    path::{Path, PathBuf},
23    pin::Pin,
24    str::FromStr,
25    sync::{
26        Arc,
27        atomic::{AtomicUsize, Ordering},
28    },
29    task::{Context, Poll},
30    time::Duration,
31};
32use tokio::time::{Instant, Interval};
33
34#[derive(Clone, Debug, Parser)]
35pub struct NodeArgs {
36    /// Port number to listen on.
37    #[arg(long, short, default_value = "8545", value_name = "NUM")]
38    pub port: u16,
39
40    /// Number of dev accounts to generate and configure.
41    #[arg(long, short, default_value = "10", value_name = "NUM")]
42    pub accounts: u64,
43
44    /// The balance of every dev account in Ether.
45    #[arg(long, default_value = "10000", value_name = "NUM")]
46    pub balance: u64,
47
48    /// The timestamp of the genesis block.
49    #[arg(long, value_name = "NUM")]
50    pub timestamp: Option<u64>,
51
52    /// The number of the genesis block.
53    #[arg(long, value_name = "NUM")]
54    pub number: Option<u64>,
55
56    /// BIP39 mnemonic phrase used for generating accounts.
57    /// Cannot be used if `mnemonic_random` or `mnemonic_seed` are used.
58    #[arg(long, short, conflicts_with_all = &["mnemonic_seed", "mnemonic_random"])]
59    pub mnemonic: Option<String>,
60
61    /// Automatically generates a BIP39 mnemonic phrase, and derives accounts from it.
62    /// Cannot be used with other `mnemonic` options.
63    /// You can specify the number of words you want in the mnemonic.
64    /// [default: 12]
65    #[arg(long, conflicts_with_all = &["mnemonic", "mnemonic_seed"], default_missing_value = "12", num_args(0..=1))]
66    pub mnemonic_random: Option<usize>,
67
68    /// Generates a BIP39 mnemonic phrase from a given seed
69    /// Cannot be used with other `mnemonic` options.
70    ///
71    /// CAREFUL: This is NOT SAFE and should only be used for testing.
72    /// Never use the private keys generated in production.
73    #[arg(long = "mnemonic-seed-unsafe", conflicts_with_all = &["mnemonic", "mnemonic_random"])]
74    pub mnemonic_seed: Option<u64>,
75
76    /// Sets the derivation path of the child key to be derived.
77    ///
78    /// [default: m/44'/60'/0'/0/]
79    #[arg(long)]
80    pub derivation_path: Option<String>,
81
82    /// The account used to sponsor Tempo fee-payer requests (`eth_signRawTransaction` and raw
83    /// transactions carrying the sponsorship placeholder).
84    ///
85    /// Must be an unlocked account. Only used on Tempo networks; defaults to the last dev
86    /// account.
87    #[arg(long = "tempo.fee-payer", value_name = "ADDRESS")]
88    pub tempo_fee_payer: Option<Address>,
89
90    /// The EVM hardfork to use.
91    ///
92    /// Choose the hardfork by name, e.g. `prague`, `cancun`, `shanghai`, `paris`, `london`, etc...
93    /// [default: latest]
94    #[arg(long)]
95    pub hardfork: Option<String>,
96
97    /// Block time in seconds for interval mining.
98    #[arg(short, long, visible_alias = "blockTime", value_name = "SECONDS", value_parser = duration_from_secs_f64)]
99    pub block_time: Option<Duration>,
100
101    /// Slots in an epoch
102    #[arg(long, value_name = "SLOTS_IN_AN_EPOCH", default_value_t = DEFAULT_SLOTS_IN_AN_EPOCH)]
103    pub slots_in_an_epoch: u64,
104
105    /// Writes output of `anvil` as json to user-specified file.
106    #[arg(long, value_name = "FILE", value_hint = clap::ValueHint::FilePath)]
107    pub config_out: Option<PathBuf>,
108
109    /// Disable auto and interval mining, and mine on demand instead.
110    #[arg(long, visible_alias = "no-mine", conflicts_with = "block_time")]
111    pub no_mining: bool,
112
113    /// Enable mixed mining mode. Blocks are mined on a timer (set by `--block-time`),
114    /// but also whenever a transaction is submitted. Requires `--block-time` to be set.
115    #[arg(long, requires = "block_time")]
116    pub mixed_mining: bool,
117
118    /// The hosts the server will listen on.
119    #[arg(
120        long,
121        value_name = "IP_ADDR",
122        env = "ANVIL_IP_ADDR",
123        default_value = "127.0.0.1",
124        help_heading = "Server options",
125        value_delimiter = ','
126    )]
127    pub host: Vec<IpAddr>,
128
129    /// How transactions are sorted in the mempool.
130    #[arg(long, default_value = "fees")]
131    pub order: TransactionOrder,
132
133    /// Initialize the genesis block with the given `genesis.json` file.
134    #[arg(long, value_name = "PATH", value_parser= read_genesis_file)]
135    pub init: Option<Genesis>,
136
137    /// This is an alias for both --load-state and --dump-state.
138    ///
139    /// It initializes the chain with the state and block environment stored at the file, if it
140    /// exists, and dumps the chain's state on exit.
141    #[arg(
142        long,
143        value_name = "PATH",
144        value_parser = StateFile::parse,
145        conflicts_with_all = &[
146            "init",
147            "dump_state",
148            "load_state"
149        ]
150    )]
151    pub state: Option<StateFile>,
152
153    /// Interval in seconds at which the state and block environment is to be dumped to disk.
154    ///
155    /// See --state and --dump-state
156    #[arg(short, long, value_name = "SECONDS")]
157    pub state_interval: Option<u64>,
158
159    /// Dump the state and block environment of chain on exit to the given file.
160    ///
161    /// If the value is a directory, the state will be written to `<VALUE>/state.json`.
162    #[arg(long, value_name = "PATH", conflicts_with = "init")]
163    pub dump_state: Option<PathBuf>,
164
165    /// Preserve historical state snapshots when dumping the state.
166    ///
167    /// This will save the in-memory states of the chain at particular block hashes.
168    ///
169    /// These historical states will be loaded into the memory when `--load-state` / `--state`, and
170    /// aids in RPC calls beyond the block at which state was dumped.
171    #[arg(long, conflicts_with = "init", default_value = "false")]
172    pub preserve_historical_states: bool,
173
174    /// Initialize the chain from a previously saved state snapshot.
175    #[arg(
176        long,
177        value_name = "PATH",
178        value_parser = SerializableState::parse,
179        conflicts_with = "init"
180    )]
181    pub load_state: Option<SerializableState>,
182
183    /// Fund specific accounts with custom balances on startup.
184    ///
185    /// Accepts multiple address:balance pairs where balance is in ETH.
186    /// Example: --fund-accounts 0x1234...5678:1000 0xabcd...ef01:5000
187    #[arg(long, value_name = "ADDRESS:AMOUNT", value_delimiter = ' ', num_args = 1..)]
188    pub fund_accounts: Vec<String>,
189
190    #[arg(long, help = IPC_HELP, value_name = "PATH", visible_alias = "ipcpath")]
191    pub ipc: Option<Option<String>>,
192
193    /// Don't keep full chain history.
194    /// If a number argument is specified, at most this number of states is kept in memory.
195    ///
196    /// If enabled, no state will be persisted on disk, so `max_persisted_states` will be 0.
197    #[arg(long)]
198    pub prune_history: Option<Option<usize>>,
199
200    /// Max number of states to persist on disk.
201    ///
202    /// Note that `prune_history` will overwrite `max_persisted_states` to 0.
203    #[arg(long, conflicts_with = "prune_history")]
204    pub max_persisted_states: Option<usize>,
205
206    /// Number of blocks with transactions to keep in memory.
207    #[arg(long)]
208    pub transaction_block_keeper: Option<usize>,
209
210    /// Maximum number of transactions in a block.
211    #[arg(long)]
212    pub max_transactions: Option<usize>,
213
214    #[command(flatten)]
215    pub evm: AnvilEvmArgs,
216
217    #[command(flatten)]
218    pub server_config: ServerConfig,
219
220    /// Path to the cache directory where persisted states are stored (see
221    /// `--max-persisted-states`).
222    ///
223    /// Note: This does not affect the fork RPC cache location, which uses endpoint-specific files
224    /// under `~/.foundry/cache/rpc/<chain>/<block>/`.
225    #[arg(long, value_name = "PATH")]
226    pub cache_path: Option<PathBuf>,
227}
228
229#[cfg(windows)]
230const IPC_HELP: &str =
231    "Launch an ipc server at the given path or default path = `\\.\\pipe\\anvil.ipc`";
232
233/// The default IPC endpoint
234#[cfg(not(windows))]
235const IPC_HELP: &str = "Launch an ipc server at the given path or default path = `/tmp/anvil.ipc`";
236
237/// Default interval for periodically dumping the state.
238const DEFAULT_DUMP_INTERVAL: Duration = Duration::from_secs(60);
239
240impl NodeArgs {
241    pub fn into_node_config(self) -> eyre::Result<NodeConfig> {
242        let genesis_balance = Unit::ETHER.wei().saturating_mul(U256::from(self.balance));
243        let compute_units_per_second =
244            if self.evm.no_rate_limit { Some(u64::MAX) } else { self.evm.compute_units_per_second };
245
246        // Validate that secondary fork URLs don't have conflicting block number suffixes
247        if self.evm.fork_url.len() > 1 {
248            for fork in &self.evm.fork_url[1..] {
249                if fork.block.is_some() {
250                    eyre::bail!(
251                        "Block number suffixes (@block) on secondary --fork-url values are not supported. \
252                         Use --fork-block-number to set the fork block for all endpoints."
253                    );
254                }
255            }
256        }
257
258        let funded_accounts = self.parse_funded_accounts()?;
259
260        let local_chain_id = self
261            .evm
262            .chain_id
263            .map(u64::from)
264            .or_else(|| self.init.as_ref().map(|genesis| genesis.config.chain_id));
265        let inferred_network_chain_id = self
266            .evm
267            .fork_chain_id
268            .map(u64::from)
269            .or(if self.evm.fork_url.is_empty() { local_chain_id } else { None });
270        let networks = if let Some(chain_id) = inferred_network_chain_id {
271            self.evm.networks.try_with_chain_id(chain_id).map_err(eyre::Report::msg)?
272        } else {
273            self.evm.networks
274        };
275
276        let hardfork = match &self.hardfork {
277            Some(hf) => Some(parse_hardfork(hf, &networks)?),
278            None => None,
279        };
280        let networks = if let Some(hardfork) = hardfork {
281            networks.normalize_for_hardfork(hardfork).map_err(eyre::Report::msg)?
282        } else {
283            networks
284        };
285
286        Ok(NodeConfig::default()
287            .with_gas_limit(self.evm.gas_limit)
288            .disable_block_gas_limit(self.evm.disable_block_gas_limit)
289            .enable_tx_gas_limit(self.evm.enable_tx_gas_limit)
290            .with_gas_price(self.evm.gas_price)
291            .with_hardfork(hardfork)
292            .with_blocktime(self.block_time)
293            .with_no_mining(self.no_mining)
294            .with_mixed_mining(self.mixed_mining, self.block_time)
295            .with_account_generator(self.account_generator())?
296            .with_genesis_balance(genesis_balance)
297            .with_genesis_timestamp(self.timestamp)
298            .with_genesis_block_number(self.number)
299            .with_port(self.port)
300            .with_fork_choice(match (self.evm.fork_block_number, self.evm.fork_transaction_hash) {
301                (Some(block), None) => Some(ForkChoice::Block(block)),
302                (None, Some(hash)) => Some(ForkChoice::Transaction(hash)),
303                _ => self
304                    .evm
305                    .fork_url
306                    .first()
307                    .and_then(|f| f.block)
308                    .map(|num| ForkChoice::Block(num as i128)),
309            })
310            .with_fork_headers(self.evm.fork_headers)
311            .with_fork_chain_id(self.evm.fork_chain_id.map(u64::from).map(U256::from))
312            .fork_request_timeout(self.evm.fork_request_timeout.map(Duration::from_millis))
313            .fork_request_retries(self.evm.fork_request_retries)
314            .fork_retry_backoff(self.evm.fork_retry_backoff.map(Duration::from_millis))
315            .fork_compute_units_per_second(compute_units_per_second)
316            .with_fork_urls(self.evm.fork_url.into_iter().map(|f| f.url).collect())
317            .with_base_fee(self.evm.block_base_fee_per_gas)
318            .disable_min_priority_fee(self.evm.disable_min_priority_fee)
319            .with_no_storage_caching(self.evm.no_storage_caching)
320            .with_server_config(self.server_config)
321            .with_host(self.host)
322            .set_silent(shell::is_quiet())
323            .set_config_out(self.config_out)
324            .with_transaction_order(self.order)
325            .with_genesis(self.init)
326            .with_steps_tracing(self.evm.steps_tracing)
327            .with_print_logs(!self.evm.disable_console_log)
328            .with_print_traces(self.evm.print_traces)
329            .with_auto_impersonate(self.evm.auto_impersonate)
330            .with_ipc(self.ipc)
331            .with_code_size_limit(self.evm.code_size_limit)
332            .disable_code_size_limit(self.evm.disable_code_size_limit)
333            .set_pruned_history(self.prune_history)
334            .with_init_state(self.load_state.or_else(|| self.state.and_then(|s| s.state)))
335            .with_transaction_block_keeper(self.transaction_block_keeper)
336            .with_max_transactions(self.max_transactions)
337            .with_max_persisted_states(self.max_persisted_states)
338            .with_chain_id(self.evm.chain_id)
339            // Restore the source-derived or explicitly selected network after applying the
340            // execution chain ID. Fork source discovery can refine an unresolved network later.
341            .with_networks(networks)
342            .with_tempo_fee_payer(self.tempo_fee_payer)
343            .with_disable_default_create2_deployer(self.evm.disable_default_create2_deployer)
344            .with_disable_pool_balance_checks(self.evm.disable_pool_balance_checks)
345            .with_slots_in_an_epoch(self.slots_in_an_epoch)
346            .with_memory_limit(self.evm.memory_limit)
347            .with_cache_path(self.cache_path)
348            .with_funded_accounts(funded_accounts))
349    }
350
351    fn parse_funded_accounts(&self) -> eyre::Result<HashMap<Address, U256>> {
352        let mut accounts = HashMap::default();
353        for entry in &self.fund_accounts {
354            let parts: Vec<&str> = entry.split(':').collect();
355            if parts.len() != 2 {
356                eyre::bail!(
357                    "Invalid fund-accounts entry '{}'. Expected format: ADDRESS:AMOUNT",
358                    entry
359                );
360            }
361            let address = parts[0]
362                .parse::<Address>()
363                .map_err(|e| eyre::eyre!("Invalid address '{}': {}", parts[0], e))?;
364            let amount: u64 = parts[1]
365                .parse()
366                .map_err(|e| eyre::eyre!("Invalid amount '{}': {}", parts[1], e))?;
367            let balance = Unit::ETHER.wei().saturating_mul(U256::from(amount));
368            accounts.insert(address, balance);
369        }
370        Ok(accounts)
371    }
372
373    fn account_generator(&self) -> AccountGenerator {
374        let mut generator = AccountGenerator::new(self.accounts as usize)
375            .phrase(DEFAULT_MNEMONIC)
376            .chain_id(self.evm.chain_id.unwrap_or(CHAIN_ID.into()));
377        if let Some(ref mnemonic) = self.mnemonic {
378            generator = generator.phrase(mnemonic);
379        } else if let Some(count) = self.mnemonic_random {
380            let mut rng = rand_08::thread_rng();
381            let mnemonic = match Mnemonic::<English>::new_with_count(&mut rng, count) {
382                Ok(mnemonic) => mnemonic.to_phrase(),
383                Err(err) => {
384                    warn!(target: "node", ?count, %err, "failed to generate mnemonic, falling back to 12-word random mnemonic");
385                    // Fallback: generate a valid 12-word random mnemonic instead of using
386                    // DEFAULT_MNEMONIC
387                    Mnemonic::<English>::new_with_count(&mut rng, 12)
388                        .expect("valid default word count")
389                        .to_phrase()
390                }
391            };
392            generator = generator.phrase(mnemonic);
393        } else if let Some(seed) = self.mnemonic_seed {
394            let mut seed = StdRng::seed_from_u64(seed);
395            let mnemonic = Mnemonic::<English>::new(&mut seed).to_phrase();
396            generator = generator.phrase(mnemonic);
397        }
398        if let Some(ref derivation) = self.derivation_path {
399            generator = generator.derivation_path(derivation);
400        }
401        generator
402    }
403
404    /// Starts the node
405    ///
406    /// See also [crate::spawn()]
407    pub async fn run(self) -> eyre::Result<()> {
408        let dump_state =
409            self.dump_state.as_ref().or_else(|| self.state.as_ref().map(|s| &s.path)).cloned();
410        let dump_interval =
411            self.state_interval.map(Duration::from_secs).unwrap_or(DEFAULT_DUMP_INTERVAL);
412        let preserve_historical_states = self.preserve_historical_states;
413
414        let (api, mut handle) = crate::try_spawn(self.into_node_config()?).await?;
415
416        // sets the signal handler to gracefully shutdown.
417        let mut fork = api.get_fork();
418        let running = Arc::new(AtomicUsize::new(0));
419
420        // handle for the currently running rt, this must be obtained before setting the crtlc
421        // handler, See [Handle::current]
422        let mut signal = handle.shutdown_signal_mut().take();
423
424        let task_manager = handle.task_manager();
425        let mut on_shutdown = task_manager.on_shutdown();
426
427        let mut state_dumper =
428            PeriodicStateDumper::new(api, dump_state, dump_interval, preserve_historical_states);
429
430        task_manager.spawn(async move {
431            // wait for the SIGTERM signal on unix systems
432            #[cfg(unix)]
433            let mut sigterm = Box::pin(async {
434                if let Ok(mut stream) =
435                    tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
436                {
437                    stream.recv().await;
438                } else {
439                    futures::future::pending::<()>().await;
440                }
441            });
442
443            // On windows, this will never fire.
444            #[cfg(not(unix))]
445            let mut sigterm = Box::pin(futures::future::pending::<()>());
446
447            // await shutdown signal but also periodically flush state
448            tokio::select! {
449                 _ = &mut sigterm => {
450                    trace!("received sigterm signal, shutting down");
451                }
452                _ = &mut on_shutdown => {}
453                _ = &mut state_dumper => {}
454            }
455
456            // shutdown received
457            state_dumper.dump().await;
458
459            // cleaning up and shutting down
460            // this will make sure that the fork RPC cache is flushed if caching is configured
461            if let Some(fork) = fork.take() {
462                trace!("flushing cache on shutdown");
463                fork.database
464                    .read()
465                    .await
466                    .maybe_flush_cache()
467                    .expect("Could not flush cache on fork DB");
468                // cleaning up and shutting down
469                // this will make sure that the fork RPC cache is flushed if caching is configured
470            }
471            std::process::exit(0);
472        });
473
474        ctrlc::set_handler(move || {
475            let prev = running.fetch_add(1, Ordering::SeqCst);
476            if prev == 0 {
477                trace!("received shutdown signal, shutting down");
478                let _ = signal.take();
479            }
480        })
481        .expect("Error setting Ctrl-C handler");
482
483        Ok(handle.await??)
484    }
485}
486
487/// Anvil's EVM related arguments.
488#[derive(Clone, Debug, Parser)]
489#[command(next_help_heading = "EVM options")]
490pub struct AnvilEvmArgs {
491    /// Fetch state over a remote endpoint instead of starting from an empty state.
492    ///
493    /// If you want to fetch state from a specific block number, add a block number like `http://localhost:8545@1400000` or use the `--fork-block-number` argument.
494    ///
495    /// Multiple `--fork-url` flags can be provided to distribute requests across endpoints
496    /// using round-robin load balancing. On failure, the retry layer rotates to the next
497    /// endpoint.
498    #[arg(
499        long,
500        short,
501        visible_alias = "rpc-url",
502        value_name = "URL",
503        help_heading = "Fork config"
504    )]
505    pub fork_url: Vec<ForkUrl>,
506
507    /// Headers to use for the rpc client, e.g. "User-Agent: test-agent"
508    ///
509    /// See --fork-url.
510    #[arg(
511        long = "fork-header",
512        value_name = "HEADERS",
513        help_heading = "Fork config",
514        requires = "fork_url"
515    )]
516    pub fork_headers: Vec<String>,
517
518    /// Timeout in ms for requests sent to remote JSON-RPC server in forking mode.
519    ///
520    /// Default value 45000
521    #[arg(id = "timeout", long = "timeout", help_heading = "Fork config", requires = "fork_url")]
522    pub fork_request_timeout: Option<u64>,
523
524    /// Number of retry requests for spurious networks (timed out requests)
525    ///
526    /// Default value 5
527    #[arg(id = "retries", long = "retries", help_heading = "Fork config", requires = "fork_url")]
528    pub fork_request_retries: Option<u32>,
529
530    /// Fetch state from a specific block number over a remote endpoint.
531    ///
532    /// If negative, the given value is subtracted from the `latest` block number.
533    ///
534    /// See --fork-url.
535    #[arg(
536        long,
537        requires = "fork_url",
538        value_name = "BLOCK",
539        help_heading = "Fork config",
540        allow_hyphen_values = true
541    )]
542    pub fork_block_number: Option<i128>,
543
544    /// Fetch state from after a specific transaction hash has been applied over a remote endpoint.
545    ///
546    /// See --fork-url.
547    #[arg(
548        long,
549        requires = "fork_url",
550        value_name = "TRANSACTION",
551        help_heading = "Fork config",
552        conflicts_with = "fork_block_number"
553    )]
554    pub fork_transaction_hash: Option<B256>,
555
556    /// Initial retry backoff on encountering errors.
557    ///
558    /// See --fork-url.
559    #[arg(long, requires = "fork_url", value_name = "BACKOFF", help_heading = "Fork config")]
560    pub fork_retry_backoff: Option<u64>,
561
562    /// Specify chain id to skip fetching it from remote endpoint. This enables offline-start mode.
563    ///
564    /// You still must pass both `--fork-url` and `--fork-block-number`, and already have your
565    /// required state cached on disk, anything missing locally would be fetched from the
566    /// remote.
567    #[arg(
568        long,
569        help_heading = "Fork config",
570        value_name = "CHAIN",
571        requires = "fork_block_number"
572    )]
573    pub fork_chain_id: Option<Chain>,
574
575    /// Sets the number of assumed available compute units per second for this provider
576    ///
577    /// default value: 330
578    ///
579    /// See also --fork-url and <https://docs.alchemy.com/reference/compute-units#what-are-cups-compute-units-per-second>
580    #[arg(
581        long,
582        requires = "fork_url",
583        alias = "cups",
584        value_name = "CUPS",
585        help_heading = "Fork config"
586    )]
587    pub compute_units_per_second: Option<u64>,
588
589    /// Disables rate limiting for this node's provider.
590    ///
591    /// default value: false
592    ///
593    /// See also --fork-url and <https://docs.alchemy.com/reference/compute-units#what-are-cups-compute-units-per-second>
594    #[arg(
595        long,
596        requires = "fork_url",
597        value_name = "NO_RATE_LIMITS",
598        help_heading = "Fork config",
599        visible_alias = "no-rpc-rate-limit"
600    )]
601    pub no_rate_limit: bool,
602
603    /// Explicitly disables the use of RPC caching.
604    ///
605    /// All storage slots are read entirely from the endpoint.
606    ///
607    /// This flag overrides the project's configuration file.
608    ///
609    /// See --fork-url.
610    #[arg(long, requires = "fork_url", help_heading = "Fork config")]
611    pub no_storage_caching: bool,
612
613    /// The block gas limit.
614    #[arg(long, alias = "block-gas-limit", help_heading = "Environment config")]
615    pub gas_limit: Option<u64>,
616
617    /// Disable the `call.gas_limit <= block.gas_limit` constraint.
618    #[arg(
619        long,
620        value_name = "DISABLE_GAS_LIMIT",
621        help_heading = "Environment config",
622        alias = "disable-gas-limit",
623        conflicts_with = "gas_limit"
624    )]
625    pub disable_block_gas_limit: bool,
626
627    /// Enable the transaction gas limit check as imposed by EIP-7825 (Osaka hardfork).
628    #[arg(long, visible_alias = "tx-gas-limit", help_heading = "Environment config")]
629    pub enable_tx_gas_limit: bool,
630
631    /// EIP-170: Contract code size limit in bytes. Useful to increase this because of tests. To
632    /// disable entirely, use `--disable-code-size-limit`. By default, it is 0x6000 (~25kb).
633    #[arg(long, value_name = "CODE_SIZE", help_heading = "Environment config")]
634    pub code_size_limit: Option<usize>,
635
636    /// Disable EIP-170: Contract code size limit.
637    #[arg(
638        long,
639        value_name = "DISABLE_CODE_SIZE_LIMIT",
640        conflicts_with = "code_size_limit",
641        help_heading = "Environment config"
642    )]
643    pub disable_code_size_limit: bool,
644
645    /// The gas price.
646    #[arg(long, help_heading = "Environment config")]
647    pub gas_price: Option<u128>,
648
649    /// The base fee in a block.
650    #[arg(
651        long,
652        visible_alias = "base-fee",
653        value_name = "FEE",
654        help_heading = "Environment config"
655    )]
656    pub block_base_fee_per_gas: Option<u64>,
657
658    /// Disable the enforcement of a minimum suggested priority fee.
659    #[arg(long, visible_alias = "no-priority-fee", help_heading = "Environment config")]
660    pub disable_min_priority_fee: bool,
661
662    /// The chain ID.
663    #[arg(long, alias = "chain", help_heading = "Environment config")]
664    pub chain_id: Option<Chain>,
665
666    /// Enable steps tracing used for debug calls returning geth-style traces
667    #[arg(long, visible_alias = "tracing")]
668    pub steps_tracing: bool,
669
670    /// Disable printing of `console.log` invocations to stdout.
671    #[arg(long, visible_alias = "no-console-log")]
672    pub disable_console_log: bool,
673
674    /// Enable printing of traces for executed transactions and `eth_call` to stdout.
675    #[arg(long, visible_alias = "enable-trace-printing")]
676    pub print_traces: bool,
677
678    /// Enables automatic impersonation on startup. This allows any transaction sender to be
679    /// simulated as different accounts, which is useful for testing contract behavior.
680    #[arg(long, visible_alias = "auto-unlock")]
681    pub auto_impersonate: bool,
682
683    /// Disable the default create2 deployer
684    #[arg(long, visible_alias = "no-create2")]
685    pub disable_default_create2_deployer: bool,
686
687    /// Disable pool balance checks
688    #[arg(long)]
689    pub disable_pool_balance_checks: bool,
690
691    /// The memory limit per EVM execution in bytes.
692    #[arg(long)]
693    pub memory_limit: Option<u64>,
694
695    #[command(flatten)]
696    pub networks: NetworkConfigs,
697}
698
699/// Resolves an alias passed as fork-url to the matching url defined in the rpc_endpoints section
700/// of the project configuration file.
701/// Does nothing if the fork-url is not a configured alias.
702///
703/// When an alias maps to an `RpcEndpoint` with multiple `endpoints`, all URLs are expanded
704/// into additional `--fork-url` entries for multi-endpoint load balancing.
705impl AnvilEvmArgs {
706    pub fn resolve_rpc_alias(&mut self) {
707        if let Ok(config) = Config::load_with_providers(FigmentProviders::Anvil) {
708            let mut resolved_urls = Vec::new();
709            for fork_url in &self.fork_url {
710                let mut endpoints = config.rpc_endpoints.clone().resolved();
711                if let Some(endpoint) = endpoints.remove(&fork_url.url) {
712                    // Alias matched — expand all URLs from the endpoint config
713                    match endpoint.all_urls() {
714                        Ok(urls) => {
715                            for (i, url) in urls.into_iter().enumerate() {
716                                resolved_urls.push(ForkUrl {
717                                    url,
718                                    // Only the first URL inherits the block suffix
719                                    block: if i == 0 { fork_url.block } else { None },
720                                });
721                            }
722                        }
723                        Err(e) => {
724                            warn!(target: "node", alias=%fork_url.url, %e, "could not resolve all endpoints, using primary endpoint only");
725                            if let Ok(url) = endpoint.url() {
726                                resolved_urls.push(ForkUrl { url, block: fork_url.block });
727                            } else {
728                                resolved_urls.push(fork_url.clone());
729                            }
730                        }
731                    }
732                } else if let Some(Ok(url)) = config.get_rpc_url_with_alias(&fork_url.url) {
733                    // Try mesc or other resolution
734                    resolved_urls.push(ForkUrl { url: url.to_string(), block: fork_url.block });
735                } else {
736                    // Not an alias — keep as-is
737                    resolved_urls.push(fork_url.clone());
738                }
739            }
740            self.fork_url = resolved_urls;
741        }
742    }
743}
744
745/// Helper type to periodically dump the state of the chain to disk
746struct PeriodicStateDumper<N: Network> {
747    in_progress_dump: Option<Pin<Box<dyn Future<Output = ()> + Send + Sync + 'static>>>,
748    api: EthApi<N>,
749    dump_state: Option<PathBuf>,
750    preserve_historical_states: bool,
751    interval: Interval,
752}
753
754impl<N: Network<ReceiptEnvelope = FoundryReceiptEnvelope>> PeriodicStateDumper<N> {
755    fn new(
756        api: EthApi<N>,
757        dump_state: Option<PathBuf>,
758        interval: Duration,
759        preserve_historical_states: bool,
760    ) -> Self {
761        let dump_state = dump_state.map(|mut dump_state| {
762            if dump_state.is_dir() {
763                dump_state = dump_state.join("state.json");
764            }
765            dump_state
766        });
767
768        // periodically flush the state
769        let interval = tokio::time::interval_at(Instant::now() + interval, interval);
770        Self { in_progress_dump: None, api, dump_state, preserve_historical_states, interval }
771    }
772
773    async fn dump(&self) {
774        if let Some(state) = self.dump_state.clone() {
775            Self::dump_state(self.api.clone(), state, self.preserve_historical_states).await
776        }
777    }
778
779    /// Infallible state dump
780    async fn dump_state(api: EthApi<N>, dump_state: PathBuf, preserve_historical_states: bool) {
781        trace!(path=?dump_state, "Dumping state on shutdown");
782        match api.serialized_state(preserve_historical_states).await {
783            Ok(state) => {
784                if let Err(err) = foundry_common::fs::write_json_file(&dump_state, &state) {
785                    error!(?err, "Failed to dump state");
786                } else {
787                    trace!(path=?dump_state, "Dumped state on shutdown");
788                }
789            }
790            Err(err) => {
791                error!(?err, "Failed to extract state");
792            }
793        }
794    }
795}
796
797// An endless future that periodically dumps the state to disk if configured.
798impl<N: Network<ReceiptEnvelope = FoundryReceiptEnvelope>> Future for PeriodicStateDumper<N> {
799    type Output = ();
800
801    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
802        let this = self.get_mut();
803        if this.dump_state.is_none() {
804            return Poll::Pending;
805        }
806
807        loop {
808            if let Some(mut flush) = this.in_progress_dump.take() {
809                match flush.poll_unpin(cx) {
810                    Poll::Ready(_) => {
811                        this.interval.reset();
812                    }
813                    Poll::Pending => {
814                        this.in_progress_dump = Some(flush);
815                        return Poll::Pending;
816                    }
817                }
818            }
819
820            if this.interval.poll_tick(cx).is_ready() {
821                let api = this.api.clone();
822                let path = this.dump_state.clone().expect("exists; see above");
823                this.in_progress_dump =
824                    Some(Box::pin(Self::dump_state(api, path, this.preserve_historical_states)));
825            } else {
826                break;
827            }
828        }
829
830        Poll::Pending
831    }
832}
833
834/// Represents the --state flag and where to load from, or dump the state to
835#[derive(Clone, Debug)]
836pub struct StateFile {
837    pub path: PathBuf,
838    pub state: Option<SerializableState>,
839}
840
841impl StateFile {
842    /// This is used as the clap `value_parser` implementation to parse from file but only if it
843    /// exists
844    fn parse(path: &str) -> Result<Self, String> {
845        Self::parse_path(path)
846    }
847
848    /// Parse from file but only if it exists
849    pub fn parse_path(path: impl AsRef<Path>) -> Result<Self, String> {
850        let mut path = path.as_ref().to_path_buf();
851        if path.is_dir() {
852            path = path.join("state.json");
853        }
854        let mut state = Self { path, state: None };
855        if !state.path.exists() {
856            return Ok(state);
857        }
858
859        state.state = Some(SerializableState::load(&state.path).map_err(|err| err.to_string())?);
860
861        Ok(state)
862    }
863}
864
865/// Represents the input URL for a fork with an optional trailing block number:
866/// `http://localhost:8545@1000000`
867#[derive(Clone, Debug, PartialEq, Eq)]
868pub struct ForkUrl {
869    /// The endpoint url
870    pub url: String,
871    /// Optional trailing block
872    pub block: Option<u64>,
873}
874
875impl fmt::Display for ForkUrl {
876    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
877        self.url.fmt(f)?;
878        if let Some(block) = self.block {
879            write!(f, "@{block}")?;
880        }
881        Ok(())
882    }
883}
884
885impl FromStr for ForkUrl {
886    type Err = String;
887
888    fn from_str(s: &str) -> Result<Self, Self::Err> {
889        if let Some((url, block)) = s.rsplit_once('@') {
890            if block == "latest" {
891                return Ok(Self { url: url.to_string(), block: None });
892            }
893            // this will prevent false positives for auths `user:password@example.com`
894            if !block.is_empty() && !block.contains(':') && !block.contains('.') {
895                let block: u64 = block
896                    .parse()
897                    .map_err(|_| format!("Failed to parse block number: `{block}`"))?;
898                return Ok(Self { url: url.to_string(), block: Some(block) });
899            }
900        }
901        Ok(Self { url: s.to_string(), block: None })
902    }
903}
904
905/// Parses a hardfork string against the active network configuration.
906fn parse_hardfork(hf: &str, networks: &NetworkConfigs) -> eyre::Result<FoundryHardfork> {
907    if let Ok(hardfork) = FoundryHardfork::from_str(hf) {
908        networks.normalize_for_hardfork(hardfork).map_err(eyre::Report::msg)?;
909        return Ok(hardfork);
910    }
911
912    networks.execution_network().parse_hardfork(hf).map_err(eyre::Report::msg)
913}
914
915/// Clap's value parser for genesis. Loads a genesis.json file.
916fn read_genesis_file(path: &str) -> Result<Genesis, String> {
917    foundry_common::fs::read_json_file(path.as_ref()).map_err(|err| err.to_string())
918}
919
920fn duration_from_secs_f64(s: &str) -> Result<Duration, String> {
921    let s = s.parse::<f64>().map_err(|e| e.to_string())?;
922    if s == 0.0 {
923        return Err("Duration must be greater than 0".to_string());
924    }
925    Duration::try_from_secs_f64(s).map_err(|e| e.to_string())
926}
927
928#[cfg(test)]
929mod tests {
930    use super::*;
931    use foundry_evm::hardfork::EthereumHardfork;
932    #[cfg(feature = "optimism")]
933    use foundry_evm::hardfork::OpHardfork;
934    use std::{env, net::Ipv4Addr};
935    use tempo_hardfork::TempoHardfork;
936
937    #[test]
938    fn test_parse_fork_url() {
939        let fork: ForkUrl = "http://localhost:8545@1000000".parse().unwrap();
940        assert_eq!(
941            fork,
942            ForkUrl { url: "http://localhost:8545".to_string(), block: Some(1000000) }
943        );
944
945        let fork: ForkUrl = "http://localhost:8545".parse().unwrap();
946        assert_eq!(fork, ForkUrl { url: "http://localhost:8545".to_string(), block: None });
947
948        let fork: ForkUrl = "wss://user:password@example.com/".parse().unwrap();
949        assert_eq!(
950            fork,
951            ForkUrl { url: "wss://user:password@example.com/".to_string(), block: None }
952        );
953
954        let fork: ForkUrl = "wss://user:password@example.com/@latest".parse().unwrap();
955        assert_eq!(
956            fork,
957            ForkUrl { url: "wss://user:password@example.com/".to_string(), block: None }
958        );
959
960        let fork: ForkUrl = "wss://user:password@example.com/@100000".parse().unwrap();
961        assert_eq!(
962            fork,
963            ForkUrl { url: "wss://user:password@example.com/".to_string(), block: Some(100000) }
964        );
965    }
966
967    #[test]
968    fn can_parse_ethereum_hardfork() {
969        let args: NodeArgs = NodeArgs::parse_from(["anvil", "--hardfork", "berlin"]);
970        let config = args.into_node_config().unwrap();
971        assert_eq!(config.hardfork, Some(EthereumHardfork::Berlin.into()));
972    }
973
974    #[cfg(feature = "optimism")]
975    #[test]
976    fn can_parse_optimism_hardfork() {
977        let args: NodeArgs =
978            NodeArgs::parse_from(["anvil", "--optimism", "--hardfork", "Regolith"]);
979        let config = args.into_node_config().unwrap();
980        assert_eq!(config.hardfork, Some(OpHardfork::Regolith.into()));
981    }
982
983    #[test]
984    fn can_parse_tempo_hardfork_from_network() {
985        let args: NodeArgs =
986            NodeArgs::parse_from(["anvil", "--network", "tempo", "--hardfork", "T5"]);
987        let config = args.into_node_config().unwrap();
988
989        assert!(config.networks.is_tempo());
990        assert_eq!(config.hardfork, Some(TempoHardfork::T5.into()));
991    }
992
993    #[test]
994    fn can_parse_namespaced_tempo_hardfork() {
995        let args = NodeArgs::parse_from(["anvil", "--hardfork", "tempo:T5"]);
996        let config = args.into_node_config().unwrap();
997
998        assert!(config.networks.is_tempo());
999        assert_eq!(config.hardfork, Some(TempoHardfork::T5.into()));
1000    }
1001
1002    #[cfg(feature = "optimism")]
1003    #[test]
1004    fn chain_id_infers_optimism_network_in_node_config() {
1005        let args: NodeArgs = NodeArgs::parse_from(["anvil", "--chain-id", "10"]);
1006        let config = args.into_node_config().unwrap();
1007
1008        assert!(config.networks.is_optimism());
1009    }
1010
1011    #[test]
1012    #[cfg(not(feature = "optimism"))]
1013    fn chain_id_rejects_disabled_optimism_network() {
1014        let args = NodeArgs::parse_from(["anvil", "--chain-id", "10"]);
1015        let error = args.into_node_config().unwrap_err();
1016
1017        assert_eq!(
1018            error.to_string(),
1019            "cannot infer execution network from chain ID 10: network family `optimism` is not \
1020             enabled in this build"
1021        );
1022    }
1023
1024    #[test]
1025    #[cfg(not(feature = "monad"))]
1026    fn chain_id_rejects_disabled_monad_network() {
1027        for chain_id in ["143", "10143"] {
1028            let args = NodeArgs::parse_from(["anvil", "--chain-id", chain_id]);
1029            let error = args.into_node_config().unwrap_err();
1030
1031            assert_eq!(
1032                error.to_string(),
1033                format!(
1034                    "cannot infer execution network from chain ID {chain_id}: network family \
1035                     `monad` is not enabled in this build"
1036                )
1037            );
1038        }
1039    }
1040
1041    #[test]
1042    #[cfg(not(feature = "monad"))]
1043    fn explicit_ethereum_allows_monad_chain_id() {
1044        let args = NodeArgs::parse_from(["anvil", "--network", "ethereum", "--chain-id", "143"]);
1045        let config = args.into_node_config().unwrap();
1046
1047        assert_eq!(config.networks, NetworkConfigs::with_ethereum());
1048        assert_eq!(config.get_chain_id(), 143);
1049    }
1050
1051    #[test]
1052    #[cfg(not(feature = "monad"))]
1053    fn genesis_chain_id_rejects_disabled_monad_network() {
1054        let mut args = NodeArgs::parse_from(["anvil"]);
1055        let mut genesis = Genesis::default();
1056        genesis.config.chain_id = 143;
1057        args.init = Some(genesis);
1058
1059        let error = args.into_node_config().unwrap_err();
1060
1061        assert_eq!(
1062            error.to_string(),
1063            "cannot infer execution network from chain ID 143: network family `monad` is not \
1064             enabled in this build"
1065        );
1066    }
1067
1068    #[test]
1069    #[cfg(not(feature = "monad"))]
1070    fn explicit_network_overrides_genesis_chain_id_inference() {
1071        let mut args = NodeArgs::parse_from(["anvil", "--network", "ethereum"]);
1072        let mut genesis = Genesis::default();
1073        genesis.config.chain_id = 143;
1074        args.init = Some(genesis);
1075
1076        let config = args.into_node_config().unwrap();
1077
1078        assert_eq!(config.networks, NetworkConfigs::with_ethereum());
1079        assert_eq!(config.get_chain_id(), 143);
1080    }
1081
1082    #[test]
1083    #[cfg(not(feature = "monad"))]
1084    fn explicit_chain_id_precedes_genesis_network_inference() {
1085        let mut args = NodeArgs::parse_from(["anvil", "--chain-id", "1"]);
1086        let mut genesis = Genesis::default();
1087        genesis.config.chain_id = 143;
1088        args.init = Some(genesis);
1089
1090        let config = args.into_node_config().unwrap();
1091
1092        assert!(!config.networks.has_network_selection());
1093        assert_eq!(config.get_chain_id(), 1);
1094    }
1095
1096    #[test]
1097    #[cfg(not(feature = "monad"))]
1098    fn disabled_fork_chain_id_precedes_execution_chain_id() {
1099        let args = NodeArgs::parse_from([
1100            "anvil",
1101            "--fork-url",
1102            "http://localhost:8545",
1103            "--fork-block-number",
1104            "1",
1105            "--fork-chain-id",
1106            "10143",
1107            "--chain-id",
1108            "1",
1109        ]);
1110        let error = args.into_node_config().unwrap_err();
1111
1112        assert!(error.to_string().contains(
1113            "cannot infer execution network from chain ID 10143: network family `monad` is not \
1114             enabled in this build"
1115        ));
1116    }
1117
1118    #[test]
1119    fn unknown_chain_id_preserves_ethereum_fallback() {
1120        let args = NodeArgs::parse_from(["anvil", "--chain-id", "98765432"]);
1121        let config = args.into_node_config().unwrap();
1122
1123        assert!(!config.networks.has_network_selection());
1124        assert_eq!(config.get_chain_id(), 98_765_432);
1125    }
1126
1127    #[test]
1128    fn chain_id_infers_tempo_network_for_hardfork() {
1129        let args = NodeArgs::parse_from(["anvil", "--chain-id", "4217", "--hardfork", "T5"]);
1130        let config = args.into_node_config().unwrap();
1131
1132        assert!(config.networks.is_tempo());
1133        assert_eq!(config.hardfork, Some(TempoHardfork::T5.into()));
1134    }
1135
1136    #[test]
1137    fn fork_chain_id_infers_tempo_network_for_hardfork() {
1138        let args = NodeArgs::parse_from([
1139            "anvil",
1140            "--fork-url",
1141            "http://localhost:8545",
1142            "--fork-block-number",
1143            "1",
1144            "--fork-chain-id",
1145            "4217",
1146            "--hardfork",
1147            "T5",
1148        ]);
1149        let config = args.into_node_config().unwrap();
1150
1151        assert!(config.networks.is_tempo());
1152        assert_eq!(config.hardfork, Some(TempoHardfork::T5.into()));
1153    }
1154
1155    #[test]
1156    #[cfg(feature = "monad")]
1157    fn chain_id_infers_monad_network_in_node_config() {
1158        let args = NodeArgs::parse_from(["anvil", "--chain-id", "143"]);
1159        let config = args.into_node_config().unwrap();
1160
1161        assert!(config.networks.is_monad());
1162    }
1163
1164    #[test]
1165    #[cfg(feature = "monad")]
1166    fn fork_chain_id_precedes_execution_chain_id_for_network() {
1167        let args = NodeArgs::parse_from([
1168            "anvil",
1169            "--fork-url",
1170            "http://localhost:8545",
1171            "--fork-block-number",
1172            "1",
1173            "--fork-chain-id",
1174            "10143",
1175            "--chain-id",
1176            "1",
1177        ]);
1178        let config = args.into_node_config().unwrap();
1179
1180        assert!(config.networks.is_monad());
1181        assert_eq!(config.get_chain_id(), 1);
1182    }
1183
1184    #[test]
1185    fn fork_execution_chain_id_does_not_infer_source_network() {
1186        let args = NodeArgs::parse_from([
1187            "anvil",
1188            "--fork-url",
1189            "http://localhost:8545",
1190            "--chain-id",
1191            "4217",
1192        ]);
1193        let config = args.into_node_config().unwrap();
1194
1195        assert!(config.networks.resolved_network().is_none());
1196        assert_eq!(config.get_chain_id(), 4217);
1197    }
1198
1199    #[test]
1200    #[cfg(feature = "monad")]
1201    fn can_parse_monad_hardfork() {
1202        let args: NodeArgs =
1203            NodeArgs::parse_from(["anvil", "--network", "monad", "--hardfork", "MonadNine"]);
1204        let config = args.into_node_config().unwrap();
1205        assert_eq!(config.hardfork, Some(foundry_evm::hardfork::MonadHardfork::MonadNine.into()));
1206        assert!(config.networks.is_monad());
1207    }
1208
1209    #[test]
1210    #[cfg(feature = "monad")]
1211    fn monad_uses_monad_default_hardfork() {
1212        let args: NodeArgs = NodeArgs::parse_from(["anvil", "--network", "monad"]);
1213        let config = args.into_node_config().unwrap();
1214        assert_eq!(config.hardfork, None);
1215        assert_eq!(config.get_hardfork(), foundry_evm::hardfork::MonadHardfork::default().into());
1216        assert!(config.networks.is_monad());
1217    }
1218
1219    #[test]
1220    fn cant_parse_invalid_hardfork() {
1221        let args: NodeArgs = NodeArgs::parse_from(["anvil", "--hardfork", "Regolith"]);
1222        let config = args.into_node_config();
1223        assert!(config.is_err());
1224    }
1225
1226    #[test]
1227    fn can_parse_fork_headers() {
1228        let args: NodeArgs = NodeArgs::parse_from([
1229            "anvil",
1230            "--fork-url",
1231            "http,://localhost:8545",
1232            "--fork-header",
1233            "User-Agent: test-agent",
1234            "--fork-header",
1235            "Referrer: example.com",
1236        ]);
1237        assert_eq!(args.evm.fork_headers, vec!["User-Agent: test-agent", "Referrer: example.com"]);
1238    }
1239
1240    #[test]
1241    fn can_parse_prune_config() {
1242        let args: NodeArgs = NodeArgs::parse_from(["anvil", "--prune-history"]);
1243        assert!(args.prune_history.is_some());
1244
1245        let args: NodeArgs = NodeArgs::parse_from(["anvil", "--prune-history", "100"]);
1246        assert_eq!(args.prune_history, Some(Some(100)));
1247    }
1248
1249    #[test]
1250    fn can_parse_max_persisted_states_config() {
1251        let args: NodeArgs = NodeArgs::parse_from(["anvil", "--max-persisted-states", "500"]);
1252        assert_eq!(args.max_persisted_states, (Some(500)));
1253    }
1254
1255    #[test]
1256    fn can_parse_disable_block_gas_limit() {
1257        let args: NodeArgs = NodeArgs::parse_from(["anvil", "--disable-block-gas-limit"]);
1258        assert!(args.evm.disable_block_gas_limit);
1259
1260        let args =
1261            NodeArgs::try_parse_from(["anvil", "--disable-block-gas-limit", "--gas-limit", "100"]);
1262        assert!(args.is_err());
1263    }
1264
1265    #[test]
1266    fn can_parse_enable_tx_gas_limit() {
1267        let args: NodeArgs = NodeArgs::parse_from(["anvil", "--enable-tx-gas-limit"]);
1268        assert!(args.evm.enable_tx_gas_limit);
1269
1270        // Also test the alias
1271        let args: NodeArgs = NodeArgs::parse_from(["anvil", "--tx-gas-limit"]);
1272        assert!(args.evm.enable_tx_gas_limit);
1273    }
1274
1275    #[test]
1276    fn can_parse_disable_code_size_limit() {
1277        let args: NodeArgs = NodeArgs::parse_from(["anvil", "--disable-code-size-limit"]);
1278        assert!(args.evm.disable_code_size_limit);
1279
1280        let args = NodeArgs::try_parse_from([
1281            "anvil",
1282            "--disable-code-size-limit",
1283            "--code-size-limit",
1284            "100",
1285        ]);
1286        // can't be used together
1287        assert!(args.is_err());
1288    }
1289
1290    #[test]
1291    fn can_parse_host() {
1292        let args = NodeArgs::parse_from(["anvil"]);
1293        assert_eq!(args.host, vec![IpAddr::V4(Ipv4Addr::LOCALHOST)]);
1294
1295        let args = NodeArgs::parse_from([
1296            "anvil", "--host", "::1", "--host", "1.1.1.1", "--host", "2.2.2.2",
1297        ]);
1298        assert_eq!(
1299            args.host,
1300            ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::<IpAddr>().unwrap()).to_vec()
1301        );
1302
1303        let args = NodeArgs::parse_from(["anvil", "--host", "::1,1.1.1.1,2.2.2.2"]);
1304        assert_eq!(
1305            args.host,
1306            ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::<IpAddr>().unwrap()).to_vec()
1307        );
1308
1309        unsafe { env::set_var("ANVIL_IP_ADDR", "1.1.1.1") };
1310        let args = NodeArgs::parse_from(["anvil"]);
1311        assert_eq!(args.host, vec!["1.1.1.1".parse::<IpAddr>().unwrap()]);
1312
1313        unsafe { env::set_var("ANVIL_IP_ADDR", "::1,1.1.1.1,2.2.2.2") };
1314        let args = NodeArgs::parse_from(["anvil"]);
1315        assert_eq!(
1316            args.host,
1317            ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::<IpAddr>().unwrap()).to_vec()
1318        );
1319    }
1320
1321    #[test]
1322    fn can_parse_multiple_fork_urls() {
1323        let args: NodeArgs = NodeArgs::parse_from([
1324            "anvil",
1325            "--fork-url",
1326            "http://localhost:8545",
1327            "--fork-url",
1328            "http://localhost:8546",
1329            "--fork-url",
1330            "http://localhost:8547",
1331        ]);
1332        assert_eq!(args.evm.fork_url.len(), 3);
1333        assert_eq!(args.evm.fork_url[0].url, "http://localhost:8545");
1334        assert_eq!(args.evm.fork_url[1].url, "http://localhost:8546");
1335        assert_eq!(args.evm.fork_url[2].url, "http://localhost:8547");
1336
1337        // Block suffix on first URL should work
1338        let args: NodeArgs = NodeArgs::parse_from([
1339            "anvil",
1340            "--fork-url",
1341            "http://localhost:8545@1000000",
1342            "--fork-url",
1343            "http://localhost:8546",
1344        ]);
1345        assert_eq!(args.evm.fork_url[0].block, Some(1000000));
1346        assert_eq!(args.evm.fork_url[1].block, None);
1347    }
1348
1349    #[test]
1350    fn rejects_block_suffix_on_secondary_fork_urls() {
1351        let args: NodeArgs = NodeArgs::parse_from([
1352            "anvil",
1353            "--fork-url",
1354            "http://localhost:8545@1000000",
1355            "--fork-url",
1356            "http://localhost:8546@2000000",
1357        ]);
1358        let result = args.into_node_config();
1359        assert!(result.is_err());
1360        assert!(
1361            result.unwrap_err().to_string().contains("Block number suffixes"),
1362            "should reject block suffix on secondary fork URL"
1363        );
1364    }
1365
1366    #[test]
1367    fn fork_dependent_args_require_fork_url() {
1368        // All these args have `requires = "fork_url"` — they should fail without --fork-url
1369        let cases = [
1370            vec!["anvil", "--fork-header", "X-Api-Key: test"],
1371            vec!["anvil", "--timeout", "5000"],
1372            vec!["anvil", "--retries", "3"],
1373            vec!["anvil", "--fork-block-number", "100"],
1374            vec!["anvil", "--fork-retry-backoff", "500"],
1375        ];
1376        for args in &cases {
1377            let result = NodeArgs::try_parse_from(args);
1378            assert!(result.is_err(), "expected error when using {:?} without --fork-url", args[1]);
1379        }
1380    }
1381}