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    /// Override the Base activation-registry administrator.
98    #[cfg(feature = "base")]
99    #[arg(long, value_name = "ADDRESS")]
100    pub base_activation_admin: Option<Address>,
101
102    /// Block time in seconds for interval mining.
103    #[arg(short, long, visible_alias = "blockTime", value_name = "SECONDS", value_parser = duration_from_secs_f64)]
104    pub block_time: Option<Duration>,
105
106    /// Time in milliseconds to group ready transactions into one auto-mined block.
107    /// Set to 0 to disable the coalescing delay.
108    #[arg(long, value_name = "MILLISECONDS", default_value = "5")]
109    pub transaction_coalescing_window: u64,
110
111    /// Slots in an epoch
112    #[arg(long, value_name = "SLOTS_IN_AN_EPOCH", default_value_t = DEFAULT_SLOTS_IN_AN_EPOCH)]
113    pub slots_in_an_epoch: u64,
114
115    /// Writes output of `anvil` as json to user-specified file.
116    #[arg(long, value_name = "FILE", value_hint = clap::ValueHint::FilePath)]
117    pub config_out: Option<PathBuf>,
118
119    /// Disable auto and interval mining, and mine on demand instead.
120    #[arg(long, visible_alias = "no-mine", conflicts_with = "block_time")]
121    pub no_mining: bool,
122
123    /// Enable mixed mining mode. Blocks are mined on a timer (set by `--block-time`),
124    /// but also whenever a transaction is submitted. Requires `--block-time` to be set.
125    #[arg(long, requires = "block_time")]
126    pub mixed_mining: bool,
127
128    /// The hosts the server will listen on.
129    #[arg(
130        long,
131        value_name = "IP_ADDR",
132        env = "ANVIL_IP_ADDR",
133        default_value = "127.0.0.1",
134        help_heading = "Server options",
135        value_delimiter = ','
136    )]
137    pub host: Vec<IpAddr>,
138
139    /// How transactions are sorted in the mempool.
140    #[arg(long, default_value = "fees")]
141    pub order: TransactionOrder,
142
143    /// Initialize the genesis block with the given `genesis.json` file.
144    #[arg(long, value_name = "PATH", value_parser= read_genesis_file)]
145    pub init: Option<Genesis>,
146
147    /// This is an alias for both --load-state and --dump-state.
148    ///
149    /// It initializes the chain with the state and block environment stored at the file, if it
150    /// exists, and dumps the chain's state on exit.
151    #[arg(
152        long,
153        value_name = "PATH",
154        value_parser = StateFile::parse,
155        conflicts_with_all = &[
156            "init",
157            "dump_state",
158            "load_state"
159        ]
160    )]
161    pub state: Option<StateFile>,
162
163    /// Interval in seconds at which the state and block environment is to be dumped to disk.
164    ///
165    /// See --state and --dump-state
166    #[arg(short, long, value_name = "SECONDS")]
167    pub state_interval: Option<u64>,
168
169    /// Dump the state and block environment of chain on exit to the given file.
170    ///
171    /// If the value is a directory, the state will be written to `<VALUE>/state.json`.
172    #[arg(long, value_name = "PATH", conflicts_with = "init")]
173    pub dump_state: Option<PathBuf>,
174
175    /// Preserve historical state snapshots when dumping the state.
176    ///
177    /// This will save the in-memory states of the chain at particular block hashes.
178    ///
179    /// These historical states will be loaded into the memory when `--load-state` / `--state`, and
180    /// aids in RPC calls beyond the block at which state was dumped.
181    #[arg(long, conflicts_with = "init", default_value = "false")]
182    pub preserve_historical_states: bool,
183
184    /// Initialize the chain from a previously saved state snapshot.
185    #[arg(
186        long,
187        value_name = "PATH",
188        value_parser = SerializableState::parse,
189        conflicts_with = "init"
190    )]
191    pub load_state: Option<SerializableState>,
192
193    /// Fund specific accounts with custom balances on startup.
194    ///
195    /// Accepts multiple address:balance pairs where balance is in ETH.
196    /// Example: --fund-accounts 0x1234...5678:1000 0xabcd...ef01:5000
197    #[arg(long, value_name = "ADDRESS:AMOUNT", value_delimiter = ' ', num_args = 1..)]
198    pub fund_accounts: Vec<String>,
199
200    #[arg(long, help = IPC_HELP, value_name = "PATH", visible_alias = "ipcpath")]
201    pub ipc: Option<Option<String>>,
202
203    /// Don't keep full chain history.
204    /// If a number argument is specified, at most this number of states is kept in memory.
205    ///
206    /// If enabled, no state will be persisted on disk, so `max_persisted_states` will be 0.
207    #[arg(long)]
208    pub prune_history: Option<Option<usize>>,
209
210    /// Max number of states to persist on disk.
211    ///
212    /// Note that `prune_history` will overwrite `max_persisted_states` to 0.
213    #[arg(long, conflicts_with = "prune_history")]
214    pub max_persisted_states: Option<usize>,
215
216    /// Number of blocks with transactions to keep in memory.
217    #[arg(long)]
218    pub transaction_block_keeper: Option<usize>,
219
220    /// Maximum number of transactions in a block.
221    #[arg(long)]
222    pub max_transactions: Option<usize>,
223
224    #[command(flatten)]
225    pub evm: AnvilEvmArgs,
226
227    #[command(flatten)]
228    pub server_config: ServerConfig,
229
230    /// Path to the cache directory where persisted states are stored (see
231    /// `--max-persisted-states`).
232    ///
233    /// Note: This does not affect the fork RPC cache location, which uses endpoint-specific files
234    /// under `~/.foundry/cache/rpc/<chain>/<block>/`.
235    #[arg(long, value_name = "PATH")]
236    pub cache_path: Option<PathBuf>,
237}
238
239#[cfg(windows)]
240const IPC_HELP: &str =
241    "Launch an ipc server at the given path or default path = `\\.\\pipe\\anvil.ipc`";
242
243/// The default IPC endpoint
244#[cfg(not(windows))]
245const IPC_HELP: &str = "Launch an ipc server at the given path or default path = `/tmp/anvil.ipc`";
246
247/// Default interval for periodically dumping the state.
248const DEFAULT_DUMP_INTERVAL: Duration = Duration::from_secs(60);
249
250impl NodeArgs {
251    pub fn into_node_config(self) -> eyre::Result<NodeConfig> {
252        let genesis_balance = Unit::ETHER.wei().saturating_mul(U256::from(self.balance));
253        let compute_units_per_second =
254            if self.evm.no_rate_limit { Some(u64::MAX) } else { self.evm.compute_units_per_second };
255
256        // Validate that secondary fork URLs don't have conflicting block number suffixes
257        if self.evm.fork_url.len() > 1 {
258            for fork in &self.evm.fork_url[1..] {
259                if fork.block.is_some() {
260                    eyre::bail!(
261                        "Block number suffixes (@block) on secondary --fork-url values are not supported. \
262                         Use --fork-block-number to set the fork block for all endpoints."
263                    );
264                }
265            }
266        }
267
268        let funded_accounts = self.parse_funded_accounts()?;
269        #[cfg(feature = "base")]
270        let base_activation_admin = self.base_activation_admin;
271
272        let local_chain_id = self
273            .evm
274            .chain_id
275            .map(u64::from)
276            .or_else(|| self.init.as_ref().map(|genesis| genesis.config.chain_id));
277        let inferred_network_chain_id = self
278            .evm
279            .fork_chain_id
280            .map(u64::from)
281            .or(if self.evm.fork_url.is_empty() { local_chain_id } else { None });
282        let networks = if let Some(chain_id) = inferred_network_chain_id {
283            self.evm.networks.try_with_chain_id(chain_id).map_err(eyre::Report::msg)?
284        } else {
285            self.evm.networks
286        };
287
288        let hardfork = match &self.hardfork {
289            Some(hf) => Some(parse_hardfork(hf, &networks)?),
290            None => None,
291        };
292        let networks = if let Some(hardfork) = hardfork {
293            networks.normalize_for_hardfork(hardfork).map_err(eyre::Report::msg)?
294        } else {
295            networks
296        };
297
298        let config = NodeConfig::default()
299            .with_gas_limit(self.evm.gas_limit)
300            .disable_block_gas_limit(self.evm.disable_block_gas_limit)
301            .enable_tx_gas_limit(self.evm.enable_tx_gas_limit)
302            .with_gas_price(self.evm.gas_price)
303            .with_hardfork(hardfork)
304            .with_blocktime(self.block_time)
305            .with_transaction_coalescing_window(Duration::from_millis(
306                self.transaction_coalescing_window,
307            ))
308            .with_no_mining(self.no_mining)
309            .with_mixed_mining(self.mixed_mining, self.block_time)
310            .with_account_generator(self.account_generator())?
311            .with_genesis_balance(genesis_balance)
312            .with_genesis_timestamp(self.timestamp)
313            .with_genesis_block_number(self.number)
314            .with_port(self.port)
315            .with_fork_choice(match (self.evm.fork_block_number, self.evm.fork_transaction_hash) {
316                (Some(block), None) => Some(ForkChoice::Block(block)),
317                (None, Some(hash)) => Some(ForkChoice::Transaction(hash)),
318                _ => self
319                    .evm
320                    .fork_url
321                    .first()
322                    .and_then(|f| f.block)
323                    .map(|num| ForkChoice::Block(num as i128)),
324            })
325            .with_fork_headers(self.evm.fork_headers)
326            .with_fork_chain_id(self.evm.fork_chain_id.map(u64::from).map(U256::from))
327            .with_no_fork_node_info(self.evm.no_fork_node_info)
328            .with_no_bal(self.evm.no_bal)
329            .with_fork_state_by_number(self.evm.fork_state_by_number)
330            .fork_request_timeout(self.evm.fork_request_timeout.map(Duration::from_millis))
331            .fork_request_retries(self.evm.fork_request_retries)
332            .fork_retry_backoff(self.evm.fork_retry_backoff.map(Duration::from_millis))
333            .fork_compute_units_per_second(compute_units_per_second)
334            .with_fork_urls(self.evm.fork_url.into_iter().map(|f| f.url).collect())
335            .with_base_fee(self.evm.block_base_fee_per_gas)
336            .disable_min_priority_fee(self.evm.disable_min_priority_fee)
337            .with_no_storage_caching(self.evm.no_storage_caching)
338            .with_server_config(self.server_config)
339            .with_host(self.host)
340            .set_silent(shell::is_quiet())
341            .set_config_out(self.config_out)
342            .with_transaction_order(self.order)
343            .with_genesis(self.init)
344            .with_steps_tracing(self.evm.steps_tracing)
345            .with_print_logs(!self.evm.disable_console_log)
346            .with_print_traces(self.evm.print_traces)
347            .with_auto_impersonate(self.evm.auto_impersonate)
348            .with_ipc(self.ipc)
349            .with_code_size_limit(self.evm.code_size_limit)
350            .disable_code_size_limit(self.evm.disable_code_size_limit)
351            .set_pruned_history(self.prune_history)
352            .with_init_state(self.load_state.or_else(|| self.state.and_then(|s| s.state)))
353            .with_transaction_block_keeper(self.transaction_block_keeper)
354            .with_max_transactions(self.max_transactions)
355            .with_max_persisted_states(self.max_persisted_states)
356            .with_chain_id(self.evm.chain_id)
357            // Restore the source-derived or explicitly selected network after applying the
358            // execution chain ID. Fork source discovery can refine an unresolved network later.
359            .with_networks(networks)
360            .with_tempo_fee_payer(self.tempo_fee_payer)
361            .with_disable_default_create2_deployer(self.evm.disable_default_create2_deployer)
362            .with_disable_pool_balance_checks(self.evm.disable_pool_balance_checks)
363            .with_slots_in_an_epoch(self.slots_in_an_epoch)
364            .with_memory_limit(self.evm.memory_limit)
365            .with_cache_path(self.cache_path)
366            .with_funded_accounts(funded_accounts);
367        #[cfg(feature = "base")]
368        let config = config.with_base_activation_admin(base_activation_admin);
369        Ok(config)
370    }
371
372    fn parse_funded_accounts(&self) -> eyre::Result<HashMap<Address, U256>> {
373        let mut accounts = HashMap::default();
374        for entry in &self.fund_accounts {
375            let parts: Vec<&str> = entry.split(':').collect();
376            if parts.len() != 2 {
377                eyre::bail!(
378                    "Invalid fund-accounts entry '{}'. Expected format: ADDRESS:AMOUNT",
379                    entry
380                );
381            }
382            let address = parts[0]
383                .parse::<Address>()
384                .map_err(|e| eyre::eyre!("Invalid address '{}': {}", parts[0], e))?;
385            let amount: u64 = parts[1]
386                .parse()
387                .map_err(|e| eyre::eyre!("Invalid amount '{}': {}", parts[1], e))?;
388            let balance = Unit::ETHER.wei().saturating_mul(U256::from(amount));
389            accounts.insert(address, balance);
390        }
391        Ok(accounts)
392    }
393
394    fn account_generator(&self) -> AccountGenerator {
395        let mut generator = AccountGenerator::new(self.accounts as usize)
396            .phrase(DEFAULT_MNEMONIC)
397            .chain_id(self.evm.chain_id.unwrap_or(CHAIN_ID.into()));
398        if let Some(ref mnemonic) = self.mnemonic {
399            generator = generator.phrase(mnemonic);
400        } else if let Some(count) = self.mnemonic_random {
401            let mut rng = rand_08::thread_rng();
402            let mnemonic = match Mnemonic::<English>::new_with_count(&mut rng, count) {
403                Ok(mnemonic) => mnemonic.to_phrase(),
404                Err(err) => {
405                    warn!(target: "node", ?count, %err, "failed to generate mnemonic, falling back to 12-word random mnemonic");
406                    // Fallback: generate a valid 12-word random mnemonic instead of using
407                    // DEFAULT_MNEMONIC
408                    Mnemonic::<English>::new_with_count(&mut rng, 12)
409                        .expect("valid default word count")
410                        .to_phrase()
411                }
412            };
413            generator = generator.phrase(mnemonic);
414        } else if let Some(seed) = self.mnemonic_seed {
415            let mut seed = StdRng::seed_from_u64(seed);
416            let mnemonic = Mnemonic::<English>::new(&mut seed).to_phrase();
417            generator = generator.phrase(mnemonic);
418        }
419        if let Some(ref derivation) = self.derivation_path {
420            generator = generator.derivation_path(derivation);
421        }
422        generator
423    }
424
425    /// Starts the node
426    ///
427    /// See also [crate::spawn()]
428    pub async fn run(self) -> eyre::Result<()> {
429        let dump_state =
430            self.dump_state.as_ref().or_else(|| self.state.as_ref().map(|s| &s.path)).cloned();
431        let dump_interval =
432            self.state_interval.map(Duration::from_secs).unwrap_or(DEFAULT_DUMP_INTERVAL);
433        let preserve_historical_states = self.preserve_historical_states;
434
435        let (api, mut handle) = crate::try_spawn(self.into_node_config()?).await?;
436
437        // sets the signal handler to gracefully shutdown.
438        let mut fork = api.get_fork();
439        let running = Arc::new(AtomicUsize::new(0));
440
441        // handle for the currently running rt, this must be obtained before setting the crtlc
442        // handler, See [Handle::current]
443        let mut signal = handle.shutdown_signal_mut().take();
444
445        let task_manager = handle.task_manager();
446        let mut on_shutdown = task_manager.on_shutdown();
447
448        let mut state_dumper =
449            PeriodicStateDumper::new(api, dump_state, dump_interval, preserve_historical_states);
450
451        task_manager.spawn(async move {
452            // wait for the SIGTERM signal on unix systems
453            #[cfg(unix)]
454            let mut sigterm = Box::pin(async {
455                if let Ok(mut stream) =
456                    tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
457                {
458                    stream.recv().await;
459                } else {
460                    futures::future::pending::<()>().await;
461                }
462            });
463
464            // On windows, this will never fire.
465            #[cfg(not(unix))]
466            let mut sigterm = Box::pin(futures::future::pending::<()>());
467
468            // await shutdown signal but also periodically flush state
469            tokio::select! {
470                 _ = &mut sigterm => {
471                    trace!("received sigterm signal, shutting down");
472                }
473                _ = &mut on_shutdown => {}
474                _ = &mut state_dumper => {}
475            }
476
477            // shutdown received
478            state_dumper.dump().await;
479
480            // cleaning up and shutting down
481            // this will make sure that the fork RPC cache is flushed if caching is configured
482            if let Some(fork) = fork.take() {
483                trace!("flushing cache on shutdown");
484                fork.database
485                    .read()
486                    .await
487                    .maybe_flush_cache()
488                    .expect("Could not flush cache on fork DB");
489                // cleaning up and shutting down
490                // this will make sure that the fork RPC cache is flushed if caching is configured
491            }
492            std::process::exit(0);
493        });
494
495        ctrlc::set_handler(move || {
496            let prev = running.fetch_add(1, Ordering::SeqCst);
497            if prev == 0 {
498                trace!("received shutdown signal, shutting down");
499                let _ = signal.take();
500            }
501        })
502        .expect("Error setting Ctrl-C handler");
503
504        Ok(handle.await??)
505    }
506}
507
508/// Anvil's EVM related arguments.
509#[derive(Clone, Debug, Parser)]
510#[command(next_help_heading = "EVM options")]
511pub struct AnvilEvmArgs {
512    /// Fetch state over a remote endpoint instead of starting from an empty state.
513    ///
514    /// 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.
515    ///
516    /// Multiple `--fork-url` flags can be provided to distribute requests across endpoints
517    /// using round-robin load balancing. On failure, the retry layer rotates to the next
518    /// endpoint.
519    #[arg(
520        long,
521        short,
522        visible_alias = "rpc-url",
523        value_name = "URL",
524        help_heading = "Fork config"
525    )]
526    pub fork_url: Vec<ForkUrl>,
527
528    /// Headers to use for the rpc client, e.g. "User-Agent: test-agent"
529    ///
530    /// See --fork-url.
531    #[arg(
532        long = "fork-header",
533        value_name = "HEADERS",
534        help_heading = "Fork config",
535        requires = "fork_url"
536    )]
537    pub fork_headers: Vec<String>,
538
539    /// Timeout in ms for requests sent to remote JSON-RPC server in forking mode.
540    ///
541    /// Default value 45000
542    #[arg(id = "timeout", long = "timeout", help_heading = "Fork config", requires = "fork_url")]
543    pub fork_request_timeout: Option<u64>,
544
545    /// Number of retry requests for spurious networks (timed out requests)
546    ///
547    /// Default value 5
548    #[arg(id = "retries", long = "retries", help_heading = "Fork config", requires = "fork_url")]
549    pub fork_request_retries: Option<u32>,
550
551    /// Fetch state from a specific block number over a remote endpoint.
552    ///
553    /// If negative, the given value is subtracted from the `latest` block number.
554    ///
555    /// See --fork-url.
556    #[arg(
557        long,
558        requires = "fork_url",
559        value_name = "BLOCK",
560        help_heading = "Fork config",
561        allow_hyphen_values = true
562    )]
563    pub fork_block_number: Option<i128>,
564
565    /// Fetch state from after a specific transaction hash has been applied over a remote endpoint.
566    ///
567    /// See --fork-url.
568    #[arg(
569        long,
570        requires = "fork_url",
571        value_name = "TRANSACTION",
572        help_heading = "Fork config",
573        conflicts_with = "fork_block_number"
574    )]
575    pub fork_transaction_hash: Option<B256>,
576
577    /// Initial retry backoff on encountering errors.
578    ///
579    /// See --fork-url.
580    #[arg(long, requires = "fork_url", value_name = "BACKOFF", help_heading = "Fork config")]
581    pub fork_retry_backoff: Option<u64>,
582
583    /// Specify chain id to skip fetching it from remote endpoint. This enables offline-start mode.
584    ///
585    /// You still must pass both `--fork-url` and `--fork-block-number`, and already have your
586    /// required state cached on disk, anything missing locally would be fetched from the
587    /// remote.
588    #[arg(
589        long,
590        help_heading = "Fork config",
591        value_name = "CHAIN",
592        requires = "fork_block_number"
593    )]
594    pub fork_chain_id: Option<Chain>,
595
596    /// Do not probe the fork endpoint with `anvil_nodeInfo` / `anvil_metadata`.
597    ///
598    /// Those calls detect a nested Anvil. Some public RPCs retry unknown methods for tens of
599    /// seconds instead of returning method-not-found, which delays listen until the probe
600    /// finishes.
601    #[arg(long, requires = "fork_url", help_heading = "Fork config")]
602    pub no_fork_node_info: bool,
603
604    /// Disable pre-filling the fork cache with block access list (BAL) post-state.
605    ///
606    /// By default, eligible Ethereum forks briefly try fetching a BAL at the fork block.
607    /// Missing or invalid BALs fall back to ordinary state reads. This setting also applies
608    /// to subsequent resets and does not disable serving BAL RPC methods.
609    #[arg(long, help_heading = "Fork config")]
610    pub no_bal: bool,
611
612    /// Read fork state by block number instead of by block hash.
613    ///
614    /// Use this for RPCs that cannot serve `eth_getBalance`, `eth_getCode`, `eth_getStorageAt` or
615    /// `eth_getTransactionCount` for a block hash. Block hash ancestry is still validated against
616    /// the fork block.
617    ///
618    /// See --fork-url.
619    #[arg(long, requires = "fork_url", help_heading = "Fork config")]
620    pub fork_state_by_number: bool,
621
622    /// Sets the number of assumed available compute units per second for this provider
623    ///
624    /// default value: 330
625    ///
626    /// See also --fork-url and <https://docs.alchemy.com/reference/compute-units#what-are-cups-compute-units-per-second>
627    #[arg(
628        long,
629        requires = "fork_url",
630        alias = "cups",
631        value_name = "CUPS",
632        help_heading = "Fork config"
633    )]
634    pub compute_units_per_second: Option<u64>,
635
636    /// Disables rate limiting for this node's provider.
637    ///
638    /// default value: false
639    ///
640    /// See also --fork-url and <https://docs.alchemy.com/reference/compute-units#what-are-cups-compute-units-per-second>
641    #[arg(
642        long,
643        requires = "fork_url",
644        value_name = "NO_RATE_LIMITS",
645        help_heading = "Fork config",
646        visible_alias = "no-rpc-rate-limit"
647    )]
648    pub no_rate_limit: bool,
649
650    /// Explicitly disables the use of RPC caching.
651    ///
652    /// All storage slots are read entirely from the endpoint.
653    ///
654    /// This flag overrides the project's configuration file.
655    ///
656    /// See --fork-url.
657    #[arg(long, requires = "fork_url", help_heading = "Fork config")]
658    pub no_storage_caching: bool,
659
660    /// The block gas limit.
661    #[arg(long, alias = "block-gas-limit", help_heading = "Environment config")]
662    pub gas_limit: Option<u64>,
663
664    /// Disable the `call.gas_limit <= block.gas_limit` constraint.
665    #[arg(
666        long,
667        value_name = "DISABLE_GAS_LIMIT",
668        help_heading = "Environment config",
669        alias = "disable-gas-limit",
670        conflicts_with = "gas_limit"
671    )]
672    pub disable_block_gas_limit: bool,
673
674    /// Enable the transaction gas limit check as imposed by EIP-7825 (Osaka hardfork).
675    #[arg(long, visible_alias = "tx-gas-limit", help_heading = "Environment config")]
676    pub enable_tx_gas_limit: bool,
677
678    /// EIP-170: Contract code size limit in bytes. Useful to increase this because of tests. To
679    /// disable entirely, use `--disable-code-size-limit`. By default, it is 0x6000 (~25kb).
680    #[arg(long, value_name = "CODE_SIZE", help_heading = "Environment config")]
681    pub code_size_limit: Option<usize>,
682
683    /// Disable EIP-170: Contract code size limit.
684    #[arg(
685        long,
686        value_name = "DISABLE_CODE_SIZE_LIMIT",
687        conflicts_with = "code_size_limit",
688        help_heading = "Environment config"
689    )]
690    pub disable_code_size_limit: bool,
691
692    /// The gas price.
693    #[arg(long, help_heading = "Environment config")]
694    pub gas_price: Option<u128>,
695
696    /// The base fee in a block.
697    #[arg(
698        long,
699        visible_alias = "base-fee",
700        value_name = "FEE",
701        help_heading = "Environment config"
702    )]
703    pub block_base_fee_per_gas: Option<u64>,
704
705    /// Disable the enforcement of a minimum suggested priority fee.
706    #[arg(long, visible_alias = "no-priority-fee", help_heading = "Environment config")]
707    pub disable_min_priority_fee: bool,
708
709    /// The chain ID.
710    #[arg(long, alias = "chain", help_heading = "Environment config")]
711    pub chain_id: Option<Chain>,
712
713    /// Enable steps tracing used for debug calls returning geth-style traces
714    ///
715    /// Steps are recorded by replaying the transaction from its parent block's state, so they
716    /// are unavailable once that state is pruned or when loading a state dump created without
717    /// `--preserve-historical-states`.
718    #[arg(long, visible_alias = "tracing")]
719    pub steps_tracing: bool,
720
721    /// Disable printing of `console.log` invocations to stdout.
722    #[arg(long, visible_alias = "no-console-log")]
723    pub disable_console_log: bool,
724
725    /// Enable printing of traces for executed transactions and `eth_call` to stdout.
726    #[arg(long, visible_alias = "enable-trace-printing")]
727    pub print_traces: bool,
728
729    /// Enables automatic impersonation on startup. This allows any transaction sender to be
730    /// simulated as different accounts, which is useful for testing contract behavior.
731    #[arg(long, visible_alias = "auto-unlock")]
732    pub auto_impersonate: bool,
733
734    /// Disable the default create2 deployer
735    #[arg(long, visible_alias = "no-create2")]
736    pub disable_default_create2_deployer: bool,
737
738    /// Disable pool balance checks
739    #[arg(long)]
740    pub disable_pool_balance_checks: bool,
741
742    /// The memory limit per EVM execution in bytes.
743    #[arg(long)]
744    pub memory_limit: Option<u64>,
745
746    #[command(flatten)]
747    pub networks: NetworkConfigs,
748}
749
750/// Resolves an alias passed as fork-url to the matching url defined in the rpc_endpoints section
751/// of the project configuration file.
752/// Does nothing if the fork-url is not a configured alias.
753///
754/// When an alias maps to an `RpcEndpoint` with multiple `endpoints`, all URLs are expanded
755/// into additional `--fork-url` entries for multi-endpoint load balancing.
756impl AnvilEvmArgs {
757    pub fn resolve_rpc_alias(&mut self) {
758        if let Ok(config) = Config::load_with_providers(FigmentProviders::Anvil) {
759            let mut resolved_urls = Vec::new();
760            for fork_url in &self.fork_url {
761                let mut endpoints = config.rpc_endpoints.clone().resolved();
762                if let Some(endpoint) = endpoints.remove(&fork_url.url) {
763                    // Alias matched — expand all URLs from the endpoint config
764                    match endpoint.all_urls() {
765                        Ok(urls) => {
766                            for (i, url) in urls.into_iter().enumerate() {
767                                resolved_urls.push(ForkUrl {
768                                    url,
769                                    // Only the first URL inherits the block suffix
770                                    block: if i == 0 { fork_url.block } else { None },
771                                });
772                            }
773                        }
774                        Err(e) => {
775                            warn!(target: "node", alias=%fork_url.url, %e, "could not resolve all endpoints, using primary endpoint only");
776                            if let Ok(url) = endpoint.url() {
777                                resolved_urls.push(ForkUrl { url, block: fork_url.block });
778                            } else {
779                                resolved_urls.push(fork_url.clone());
780                            }
781                        }
782                    }
783                } else if let Some(Ok(url)) = config.get_rpc_url_with_alias(&fork_url.url) {
784                    // Try mesc or other resolution
785                    resolved_urls.push(ForkUrl { url: url.to_string(), block: fork_url.block });
786                } else {
787                    // Not an alias — keep as-is
788                    resolved_urls.push(fork_url.clone());
789                }
790            }
791            self.fork_url = resolved_urls;
792        }
793    }
794}
795
796/// Helper type to periodically dump the state of the chain to disk
797struct PeriodicStateDumper<N: Network> {
798    in_progress_dump: Option<Pin<Box<dyn Future<Output = ()> + Send + Sync + 'static>>>,
799    api: EthApi<N>,
800    dump_state: Option<PathBuf>,
801    preserve_historical_states: bool,
802    interval: Interval,
803}
804
805impl<N: Network<ReceiptEnvelope = FoundryReceiptEnvelope>> PeriodicStateDumper<N> {
806    fn new(
807        api: EthApi<N>,
808        dump_state: Option<PathBuf>,
809        interval: Duration,
810        preserve_historical_states: bool,
811    ) -> Self {
812        let dump_state = dump_state.map(|mut dump_state| {
813            if dump_state.is_dir() {
814                dump_state = dump_state.join("state.json");
815            }
816            dump_state
817        });
818
819        // periodically flush the state
820        let interval = tokio::time::interval_at(Instant::now() + interval, interval);
821        Self { in_progress_dump: None, api, dump_state, preserve_historical_states, interval }
822    }
823
824    async fn dump(mut self) {
825        // Shutdown no longer polls the periodic future, so release its locks first.
826        self.in_progress_dump = None;
827        if let Some(state) = self.dump_state {
828            Self::dump_state(self.api, state, self.preserve_historical_states).await
829        }
830    }
831
832    /// Infallible state dump
833    async fn dump_state(api: EthApi<N>, dump_state: PathBuf, preserve_historical_states: bool) {
834        trace!(path=?dump_state, "Dumping state on shutdown");
835        match api.serialized_state(preserve_historical_states).await {
836            Ok(state) => {
837                if let Err(err) = foundry_common::fs::write_json_file(&dump_state, &state) {
838                    error!(?err, "Failed to dump state");
839                } else {
840                    trace!(path=?dump_state, "Dumped state on shutdown");
841                }
842            }
843            Err(err) => {
844                error!(?err, "Failed to extract state");
845            }
846        }
847    }
848}
849
850// An endless future that periodically dumps the state to disk if configured.
851impl<N: Network<ReceiptEnvelope = FoundryReceiptEnvelope>> Future for PeriodicStateDumper<N> {
852    type Output = ();
853
854    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
855        let this = self.get_mut();
856        if this.dump_state.is_none() {
857            return Poll::Pending;
858        }
859
860        loop {
861            if let Some(mut flush) = this.in_progress_dump.take() {
862                match flush.poll_unpin(cx) {
863                    Poll::Ready(_) => {
864                        this.interval.reset();
865                    }
866                    Poll::Pending => {
867                        this.in_progress_dump = Some(flush);
868                        return Poll::Pending;
869                    }
870                }
871            }
872
873            if this.interval.poll_tick(cx).is_ready() {
874                let api = this.api.clone();
875                let path = this.dump_state.clone().expect("exists; see above");
876                this.in_progress_dump =
877                    Some(Box::pin(Self::dump_state(api, path, this.preserve_historical_states)));
878            } else {
879                break;
880            }
881        }
882
883        Poll::Pending
884    }
885}
886
887/// Represents the --state flag and where to load from, or dump the state to
888#[derive(Clone, Debug)]
889pub struct StateFile {
890    pub path: PathBuf,
891    pub state: Option<SerializableState>,
892}
893
894impl StateFile {
895    /// This is used as the clap `value_parser` implementation to parse from file but only if it
896    /// exists
897    fn parse(path: &str) -> Result<Self, String> {
898        Self::parse_path(path)
899    }
900
901    /// Parse from file but only if it exists
902    pub fn parse_path(path: impl AsRef<Path>) -> Result<Self, String> {
903        let mut path = path.as_ref().to_path_buf();
904        if path.is_dir() {
905            path = path.join("state.json");
906        }
907        let mut state = Self { path, state: None };
908        if !state.path.exists() {
909            return Ok(state);
910        }
911
912        state.state = Some(SerializableState::load(&state.path).map_err(|err| err.to_string())?);
913
914        Ok(state)
915    }
916}
917
918/// Represents the input URL for a fork with an optional trailing block number:
919/// `http://localhost:8545@1000000`
920#[derive(Clone, Debug, PartialEq, Eq)]
921pub struct ForkUrl {
922    /// The endpoint url
923    pub url: String,
924    /// Optional trailing block
925    pub block: Option<u64>,
926}
927
928impl fmt::Display for ForkUrl {
929    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
930        self.url.fmt(f)?;
931        if let Some(block) = self.block {
932            write!(f, "@{block}")?;
933        }
934        Ok(())
935    }
936}
937
938impl FromStr for ForkUrl {
939    type Err = String;
940
941    fn from_str(s: &str) -> Result<Self, Self::Err> {
942        if let Some((url, block)) = s.rsplit_once('@') {
943            if block == "latest" {
944                return Ok(Self { url: url.to_string(), block: None });
945            }
946            // this will prevent false positives for auths `user:password@example.com`
947            if !block.is_empty() && !block.contains(':') && !block.contains('.') {
948                let block: u64 = block
949                    .parse()
950                    .map_err(|_| format!("Failed to parse block number: `{block}`"))?;
951                return Ok(Self { url: url.to_string(), block: Some(block) });
952            }
953        }
954        Ok(Self { url: s.to_string(), block: None })
955    }
956}
957
958/// Parses a hardfork string against the active network configuration.
959fn parse_hardfork(hf: &str, networks: &NetworkConfigs) -> eyre::Result<FoundryHardfork> {
960    if let Ok(hardfork) = FoundryHardfork::from_str(hf) {
961        networks.normalize_for_hardfork(hardfork).map_err(eyre::Report::msg)?;
962        return Ok(hardfork);
963    }
964
965    networks.execution_network().parse_hardfork(hf).map_err(eyre::Report::msg)
966}
967
968/// Clap's value parser for genesis. Loads a genesis.json file.
969fn read_genesis_file(path: &str) -> Result<Genesis, String> {
970    foundry_common::fs::read_json_file(path.as_ref()).map_err(|err| err.to_string())
971}
972
973fn duration_from_secs_f64(s: &str) -> Result<Duration, String> {
974    let s = s.parse::<f64>().map_err(|e| e.to_string())?;
975    if s == 0.0 {
976        return Err("Duration must be greater than 0".to_string());
977    }
978    Duration::try_from_secs_f64(s).map_err(|e| e.to_string())
979}
980
981#[cfg(test)]
982mod tests {
983    use super::*;
984    use foundry_evm::hardfork::EthereumHardfork;
985    use std::{env, net::Ipv4Addr};
986    use tempo_hardfork::TempoHardfork;
987
988    #[cfg(feature = "base")]
989    use foundry_evm::hardforks::BaseUpgrade;
990
991    #[cfg(feature = "optimism")]
992    use foundry_evm::hardfork::OpHardfork;
993
994    #[tokio::test]
995    async fn final_dump_cancels_suspended_periodic_dump() {
996        for queued_for_mining in [false, true] {
997            let (api, _handle) = crate::spawn(NodeConfig::test().with_no_mining(true)).await;
998            let dir = tempfile::tempdir().unwrap();
999            let path = dir.path().join("state.json");
1000            let mining =
1001                if queued_for_mining { Some(api.backend.lock_mining().await) } else { None };
1002            let db = api.backend.get_db().write().await;
1003            let mut dumper = PeriodicStateDumper::new(
1004                api.clone(),
1005                Some(path.clone()),
1006                Duration::from_secs(60),
1007                false,
1008            );
1009            dumper.in_progress_dump =
1010                Some(Box::pin(PeriodicStateDumper::dump_state(api.clone(), path.clone(), false)));
1011            assert!(futures::poll!(&mut dumper).is_pending());
1012
1013            // Shutdown stops polling the periodic dumper before starting the final dump.
1014            drop(db);
1015            drop(mining);
1016            tokio::time::timeout(Duration::from_secs(2), dumper.dump())
1017                .await
1018                .expect("final dump waited for a suspended periodic dump");
1019            let state = foundry_common::fs::read_json_file::<SerializableState>(&path).unwrap();
1020            assert_eq!(state.best_block_number, Some(0));
1021        }
1022    }
1023
1024    #[cfg(feature = "base")]
1025    #[test]
1026    fn base_chain_ids_select_native_base_unless_overridden() {
1027        for chain_id in ["8453", "84532"] {
1028            let config =
1029                NodeArgs::parse_from(["anvil", "--chain-id", chain_id]).into_node_config().unwrap();
1030            assert!(config.networks.is_base());
1031
1032            let config =
1033                NodeArgs::parse_from(["anvil", "--chain-id", chain_id, "--network", "ethereum"])
1034                    .into_node_config()
1035                    .unwrap();
1036            assert!(config.networks.execution_network().is_ethereum());
1037        }
1038    }
1039
1040    #[test]
1041    fn fork_bal_can_be_disabled() {
1042        let args =
1043            NodeArgs::try_parse_from(["anvil", "--fork-url", "http://localhost:8545", "--no-bal"]);
1044        let config = args.unwrap().into_node_config().unwrap();
1045        assert!(config.no_bal);
1046        assert!(!NodeArgs::parse_from(["anvil"]).into_node_config().unwrap().no_bal);
1047        assert!(NodeArgs::parse_from(["anvil", "--no-bal"]).into_node_config().unwrap().no_bal);
1048    }
1049
1050    #[test]
1051    fn test_parse_fork_url() {
1052        let fork: ForkUrl = "http://localhost:8545@1000000".parse().unwrap();
1053        assert_eq!(
1054            fork,
1055            ForkUrl { url: "http://localhost:8545".to_string(), block: Some(1000000) }
1056        );
1057
1058        let fork: ForkUrl = "http://localhost:8545".parse().unwrap();
1059        assert_eq!(fork, ForkUrl { url: "http://localhost:8545".to_string(), block: None });
1060
1061        let fork: ForkUrl = "wss://user:password@example.com/".parse().unwrap();
1062        assert_eq!(
1063            fork,
1064            ForkUrl { url: "wss://user:password@example.com/".to_string(), block: None }
1065        );
1066
1067        let fork: ForkUrl = "wss://user:password@example.com/@latest".parse().unwrap();
1068        assert_eq!(
1069            fork,
1070            ForkUrl { url: "wss://user:password@example.com/".to_string(), block: None }
1071        );
1072
1073        let fork: ForkUrl = "wss://user:password@example.com/@100000".parse().unwrap();
1074        assert_eq!(
1075            fork,
1076            ForkUrl { url: "wss://user:password@example.com/".to_string(), block: Some(100000) }
1077        );
1078    }
1079
1080    #[test]
1081    fn can_parse_ethereum_hardfork() {
1082        let args: NodeArgs = NodeArgs::parse_from(["anvil", "--hardfork", "berlin"]);
1083        let config = args.into_node_config().unwrap();
1084        assert_eq!(config.hardfork, Some(EthereumHardfork::Berlin.into()));
1085    }
1086
1087    #[cfg(feature = "optimism")]
1088    #[test]
1089    fn can_parse_optimism_hardfork() {
1090        let args: NodeArgs =
1091            NodeArgs::parse_from(["anvil", "--optimism", "--hardfork", "Regolith"]);
1092        let config = args.into_node_config().unwrap();
1093        assert_eq!(config.hardfork, Some(OpHardfork::Regolith.into()));
1094    }
1095
1096    #[test]
1097    fn can_parse_tempo_hardfork_from_network() {
1098        let args: NodeArgs =
1099            NodeArgs::parse_from(["anvil", "--network", "tempo", "--hardfork", "T5"]);
1100        let config = args.into_node_config().unwrap();
1101
1102        assert!(config.networks.is_tempo());
1103        assert_eq!(config.hardfork, Some(TempoHardfork::T5.into()));
1104    }
1105
1106    #[test]
1107    fn can_parse_namespaced_tempo_hardfork() {
1108        let args = NodeArgs::parse_from(["anvil", "--hardfork", "tempo:T5"]);
1109        let config = args.into_node_config().unwrap();
1110
1111        assert!(config.networks.is_tempo());
1112        assert_eq!(config.hardfork, Some(TempoHardfork::T5.into()));
1113    }
1114
1115    #[cfg(feature = "base")]
1116    #[test]
1117    fn can_parse_base_hardfork_from_network() {
1118        let args: NodeArgs =
1119            NodeArgs::parse_from(["anvil", "--network", "base", "--hardfork", "Beryl"]);
1120        let config = args.into_node_config().unwrap();
1121
1122        assert!(config.networks.is_base());
1123        assert_eq!(config.hardfork, Some(BaseUpgrade::Beryl.into()));
1124    }
1125
1126    #[cfg(feature = "base")]
1127    #[test]
1128    fn can_parse_namespaced_base_hardfork() {
1129        let args = NodeArgs::parse_from(["anvil", "--hardfork", "base:Beryl"]);
1130        let config = args.into_node_config().unwrap();
1131
1132        assert!(config.networks.is_base());
1133        assert_eq!(config.hardfork, Some(BaseUpgrade::Beryl.into()));
1134    }
1135
1136    #[cfg(feature = "base")]
1137    #[test]
1138    fn can_parse_base_activation_admin() {
1139        let admin = Address::repeat_byte(0xaa);
1140        let admin_arg = admin.to_string();
1141        let args = NodeArgs::parse_from([
1142            "anvil",
1143            "--network",
1144            "base",
1145            "--hardfork",
1146            "base:Beryl",
1147            "--base-activation-admin",
1148            admin_arg.as_str(),
1149        ]);
1150        let config = args.into_node_config().unwrap();
1151
1152        assert!(config.networks.is_base());
1153        assert_eq!(config.base_activation_admin, Some(admin));
1154    }
1155
1156    #[cfg(feature = "optimism")]
1157    #[test]
1158    fn chain_id_infers_optimism_network_in_node_config() {
1159        let args: NodeArgs = NodeArgs::parse_from(["anvil", "--chain-id", "10"]);
1160        let config = args.into_node_config().unwrap();
1161
1162        assert!(config.networks.is_optimism());
1163    }
1164
1165    #[test]
1166    #[cfg(not(feature = "optimism"))]
1167    fn chain_id_rejects_disabled_optimism_network() {
1168        let args = NodeArgs::parse_from(["anvil", "--chain-id", "10"]);
1169        let error = args.into_node_config().unwrap_err();
1170
1171        assert_eq!(
1172            error.to_string(),
1173            "cannot infer execution network from chain ID 10: network family `optimism` is not \
1174             enabled in this build"
1175        );
1176    }
1177
1178    #[test]
1179    #[cfg(feature = "base")]
1180    fn base_fork_chain_id_precedes_execution_chain_id_for_network() {
1181        let args = NodeArgs::parse_from([
1182            "anvil",
1183            "--fork-url",
1184            "http://localhost:8545",
1185            "--fork-block-number",
1186            "1",
1187            "--fork-chain-id",
1188            "8453",
1189            "--chain-id",
1190            "1",
1191        ]);
1192        let config = args.into_node_config().unwrap();
1193
1194        assert!(config.networks.is_base());
1195        assert_eq!(config.get_chain_id(), 1);
1196    }
1197
1198    /// `anvil --chain-id 8453` resolved to Optimism before Base support existed and must keep
1199    /// doing so in builds without the feature, which is what release binaries ship.
1200    #[test]
1201    #[cfg(all(not(feature = "base"), feature = "optimism"))]
1202    fn chain_id_without_base_still_resolves_to_optimism() {
1203        for chain_id in ["8453", "84532"] {
1204            let args = NodeArgs::parse_from(["anvil", "--chain-id", chain_id]);
1205            let config = args
1206                .into_node_config()
1207                .unwrap_or_else(|error| panic!("chain ID {chain_id} must still resolve: {error}"));
1208            assert!(config.networks.is_optimism(), "chain ID {chain_id} must resolve to Optimism");
1209        }
1210    }
1211
1212    #[test]
1213    #[cfg(not(feature = "monad"))]
1214    fn chain_id_rejects_disabled_monad_network() {
1215        for chain_id in ["143", "10143"] {
1216            let args = NodeArgs::parse_from(["anvil", "--chain-id", chain_id]);
1217            let error = args.into_node_config().unwrap_err();
1218
1219            assert_eq!(
1220                error.to_string(),
1221                format!(
1222                    "cannot infer execution network from chain ID {chain_id}: network family \
1223                     `monad` is not enabled in this build"
1224                )
1225            );
1226        }
1227    }
1228
1229    #[test]
1230    fn explicit_ethereum_allows_base_chain_id() {
1231        let args = NodeArgs::parse_from(["anvil", "--network", "ethereum", "--chain-id", "8453"]);
1232        let config = args.into_node_config().unwrap();
1233
1234        assert_eq!(config.networks, NetworkConfigs::with_ethereum());
1235        assert_eq!(config.get_chain_id(), 8453);
1236    }
1237
1238    #[test]
1239    #[cfg(not(feature = "monad"))]
1240    fn explicit_ethereum_allows_monad_chain_id() {
1241        let args = NodeArgs::parse_from(["anvil", "--network", "ethereum", "--chain-id", "143"]);
1242        let config = args.into_node_config().unwrap();
1243
1244        assert_eq!(config.networks, NetworkConfigs::with_ethereum());
1245        assert_eq!(config.get_chain_id(), 143);
1246    }
1247
1248    #[test]
1249    #[cfg(not(feature = "monad"))]
1250    fn genesis_chain_id_rejects_disabled_monad_network() {
1251        let mut args = NodeArgs::parse_from(["anvil"]);
1252        let mut genesis = Genesis::default();
1253        genesis.config.chain_id = 143;
1254        args.init = Some(genesis);
1255
1256        let error = args.into_node_config().unwrap_err();
1257
1258        assert_eq!(
1259            error.to_string(),
1260            "cannot infer execution network from chain ID 143: network family `monad` is not \
1261             enabled in this build"
1262        );
1263    }
1264
1265    #[test]
1266    #[cfg(feature = "base")]
1267    fn genesis_chain_id_infers_base_network() {
1268        let mut args = NodeArgs::parse_from(["anvil"]);
1269        let mut genesis = Genesis::default();
1270        genesis.config.chain_id = 8453;
1271        args.init = Some(genesis);
1272
1273        let config = args.into_node_config().unwrap();
1274        assert!(config.networks.is_base());
1275    }
1276
1277    /// Base chain IDs resolved to Optimism before Base support existed, so a build without the
1278    /// `base` feature must keep resolving them that way rather than erroring.
1279    #[test]
1280    #[cfg(all(not(feature = "base"), feature = "optimism"))]
1281    fn genesis_chain_id_without_base_still_infers_optimism() {
1282        let mut args = NodeArgs::parse_from(["anvil"]);
1283        let mut genesis = Genesis::default();
1284        genesis.config.chain_id = 8453;
1285        args.init = Some(genesis);
1286
1287        let config = args.into_node_config().unwrap();
1288        assert!(config.networks.is_optimism());
1289    }
1290
1291    #[test]
1292    #[cfg(not(feature = "monad"))]
1293    fn explicit_network_overrides_genesis_chain_id_inference() {
1294        let mut args = NodeArgs::parse_from(["anvil", "--network", "ethereum"]);
1295        let mut genesis = Genesis::default();
1296        genesis.config.chain_id = 143;
1297        args.init = Some(genesis);
1298
1299        let config = args.into_node_config().unwrap();
1300
1301        assert_eq!(config.networks, NetworkConfigs::with_ethereum());
1302        assert_eq!(config.get_chain_id(), 143);
1303    }
1304
1305    #[test]
1306    #[cfg(not(feature = "monad"))]
1307    fn explicit_chain_id_precedes_genesis_network_inference() {
1308        let mut args = NodeArgs::parse_from(["anvil", "--chain-id", "1"]);
1309        let mut genesis = Genesis::default();
1310        genesis.config.chain_id = 143;
1311        args.init = Some(genesis);
1312
1313        let config = args.into_node_config().unwrap();
1314
1315        assert!(!config.networks.has_network_selection());
1316        assert_eq!(config.get_chain_id(), 1);
1317    }
1318
1319    #[test]
1320    fn explicit_network_overrides_base_genesis_chain_id_inference() {
1321        let mut args = NodeArgs::parse_from(["anvil", "--network", "ethereum"]);
1322        let mut genesis = Genesis::default();
1323        genesis.config.chain_id = 8453;
1324        args.init = Some(genesis);
1325
1326        let config = args.into_node_config().unwrap();
1327        assert_eq!(config.networks, NetworkConfigs::with_ethereum());
1328        assert_eq!(config.get_chain_id(), 8453);
1329    }
1330
1331    #[test]
1332    fn explicit_chain_id_precedes_base_genesis_network_inference() {
1333        let mut args = NodeArgs::parse_from(["anvil", "--chain-id", "1"]);
1334        let mut genesis = Genesis::default();
1335        genesis.config.chain_id = 8453;
1336        args.init = Some(genesis);
1337
1338        let config = args.into_node_config().unwrap();
1339        assert!(!config.networks.has_network_selection());
1340        assert_eq!(config.get_chain_id(), 1);
1341    }
1342
1343    #[test]
1344    #[cfg(not(feature = "monad"))]
1345    fn disabled_fork_chain_id_precedes_execution_chain_id() {
1346        let args = NodeArgs::parse_from([
1347            "anvil",
1348            "--fork-url",
1349            "http://localhost:8545",
1350            "--fork-block-number",
1351            "1",
1352            "--fork-chain-id",
1353            "10143",
1354            "--chain-id",
1355            "1",
1356        ]);
1357        let error = args.into_node_config().unwrap_err();
1358
1359        assert!(error.to_string().contains(
1360            "cannot infer execution network from chain ID 10143: network family `monad` is not \
1361             enabled in this build"
1362        ));
1363    }
1364
1365    #[test]
1366    fn fork_state_by_number_flag_sets_config() {
1367        let args = NodeArgs::parse_from([
1368            "anvil",
1369            "--fork-url",
1370            "http://localhost:8545",
1371            "--fork-state-by-number",
1372        ]);
1373        let config = args.into_node_config().unwrap();
1374
1375        assert!(config.fork_state_by_number);
1376    }
1377
1378    #[test]
1379    fn unknown_chain_id_preserves_ethereum_fallback() {
1380        let args = NodeArgs::parse_from(["anvil", "--chain-id", "98765432"]);
1381        let config = args.into_node_config().unwrap();
1382
1383        assert!(!config.networks.has_network_selection());
1384        assert_eq!(config.get_chain_id(), 98_765_432);
1385    }
1386
1387    #[test]
1388    fn chain_id_infers_tempo_network_for_hardfork() {
1389        let args = NodeArgs::parse_from(["anvil", "--chain-id", "4217", "--hardfork", "T5"]);
1390        let config = args.into_node_config().unwrap();
1391
1392        assert!(config.networks.is_tempo());
1393        assert_eq!(config.hardfork, Some(TempoHardfork::T5.into()));
1394    }
1395
1396    #[test]
1397    fn fork_chain_id_infers_tempo_network_for_hardfork() {
1398        let args = NodeArgs::parse_from([
1399            "anvil",
1400            "--fork-url",
1401            "http://localhost:8545",
1402            "--fork-block-number",
1403            "1",
1404            "--fork-chain-id",
1405            "4217",
1406            "--hardfork",
1407            "T5",
1408        ]);
1409        let config = args.into_node_config().unwrap();
1410
1411        assert!(config.networks.is_tempo());
1412        assert_eq!(config.hardfork, Some(TempoHardfork::T5.into()));
1413    }
1414
1415    #[test]
1416    #[cfg(feature = "monad")]
1417    fn chain_id_infers_monad_network_in_node_config() {
1418        let args = NodeArgs::parse_from(["anvil", "--chain-id", "143"]);
1419        let config = args.into_node_config().unwrap();
1420
1421        assert!(config.networks.is_monad());
1422    }
1423
1424    #[test]
1425    #[cfg(feature = "monad")]
1426    fn fork_chain_id_precedes_execution_chain_id_for_network() {
1427        let args = NodeArgs::parse_from([
1428            "anvil",
1429            "--fork-url",
1430            "http://localhost:8545",
1431            "--fork-block-number",
1432            "1",
1433            "--fork-chain-id",
1434            "10143",
1435            "--chain-id",
1436            "1",
1437        ]);
1438        let config = args.into_node_config().unwrap();
1439
1440        assert!(config.networks.is_monad());
1441        assert_eq!(config.get_chain_id(), 1);
1442    }
1443
1444    #[test]
1445    fn fork_execution_chain_id_does_not_infer_source_network() {
1446        let args = NodeArgs::parse_from([
1447            "anvil",
1448            "--fork-url",
1449            "http://localhost:8545",
1450            "--chain-id",
1451            "4217",
1452        ]);
1453        let config = args.into_node_config().unwrap();
1454
1455        assert!(config.networks.resolved_network().is_none());
1456        assert_eq!(config.get_chain_id(), 4217);
1457    }
1458
1459    #[test]
1460    #[cfg(feature = "monad")]
1461    fn can_parse_monad_hardfork() {
1462        let args: NodeArgs =
1463            NodeArgs::parse_from(["anvil", "--network", "monad", "--hardfork", "MonadNine"]);
1464        let config = args.into_node_config().unwrap();
1465        assert_eq!(config.hardfork, Some(foundry_evm::hardfork::MonadHardfork::MonadNine.into()));
1466        assert!(config.networks.is_monad());
1467    }
1468
1469    #[test]
1470    #[cfg(feature = "monad")]
1471    fn monad_uses_monad_default_hardfork() {
1472        let args: NodeArgs = NodeArgs::parse_from(["anvil", "--network", "monad"]);
1473        let config = args.into_node_config().unwrap();
1474        assert_eq!(config.hardfork, None);
1475        assert_eq!(config.get_hardfork(), foundry_evm::hardfork::MonadHardfork::default().into());
1476        assert!(config.networks.is_monad());
1477    }
1478
1479    #[test]
1480    fn cant_parse_invalid_hardfork() {
1481        let args: NodeArgs = NodeArgs::parse_from(["anvil", "--hardfork", "Regolith"]);
1482        let config = args.into_node_config();
1483        assert!(config.is_err());
1484    }
1485
1486    #[test]
1487    fn can_parse_fork_headers() {
1488        let args: NodeArgs = NodeArgs::parse_from([
1489            "anvil",
1490            "--fork-url",
1491            "http,://localhost:8545",
1492            "--fork-header",
1493            "User-Agent: test-agent",
1494            "--fork-header",
1495            "Referrer: example.com",
1496        ]);
1497        assert_eq!(args.evm.fork_headers, vec!["User-Agent: test-agent", "Referrer: example.com"]);
1498    }
1499
1500    #[test]
1501    fn can_parse_prune_config() {
1502        let args: NodeArgs = NodeArgs::parse_from(["anvil", "--prune-history"]);
1503        assert!(args.prune_history.is_some());
1504
1505        let args: NodeArgs = NodeArgs::parse_from(["anvil", "--prune-history", "100"]);
1506        assert_eq!(args.prune_history, Some(Some(100)));
1507    }
1508
1509    #[test]
1510    fn can_parse_max_persisted_states_config() {
1511        let args: NodeArgs = NodeArgs::parse_from(["anvil", "--max-persisted-states", "500"]);
1512        assert_eq!(args.max_persisted_states, (Some(500)));
1513    }
1514
1515    #[test]
1516    fn can_parse_disable_block_gas_limit() {
1517        let args: NodeArgs = NodeArgs::parse_from(["anvil", "--disable-block-gas-limit"]);
1518        assert!(args.evm.disable_block_gas_limit);
1519
1520        let args =
1521            NodeArgs::try_parse_from(["anvil", "--disable-block-gas-limit", "--gas-limit", "100"]);
1522        assert!(args.is_err());
1523    }
1524
1525    #[test]
1526    fn can_parse_enable_tx_gas_limit() {
1527        let args: NodeArgs = NodeArgs::parse_from(["anvil", "--enable-tx-gas-limit"]);
1528        assert!(args.evm.enable_tx_gas_limit);
1529
1530        // Also test the alias
1531        let args: NodeArgs = NodeArgs::parse_from(["anvil", "--tx-gas-limit"]);
1532        assert!(args.evm.enable_tx_gas_limit);
1533    }
1534
1535    #[test]
1536    fn can_parse_disable_code_size_limit() {
1537        let args: NodeArgs = NodeArgs::parse_from(["anvil", "--disable-code-size-limit"]);
1538        assert!(args.evm.disable_code_size_limit);
1539
1540        let args = NodeArgs::try_parse_from([
1541            "anvil",
1542            "--disable-code-size-limit",
1543            "--code-size-limit",
1544            "100",
1545        ]);
1546        // can't be used together
1547        assert!(args.is_err());
1548    }
1549
1550    #[test]
1551    fn can_parse_host() {
1552        let args = NodeArgs::parse_from(["anvil"]);
1553        assert_eq!(args.host, vec![IpAddr::V4(Ipv4Addr::LOCALHOST)]);
1554
1555        let args = NodeArgs::parse_from([
1556            "anvil", "--host", "::1", "--host", "1.1.1.1", "--host", "2.2.2.2",
1557        ]);
1558        assert_eq!(
1559            args.host,
1560            ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::<IpAddr>().unwrap()).to_vec()
1561        );
1562
1563        let args = NodeArgs::parse_from(["anvil", "--host", "::1,1.1.1.1,2.2.2.2"]);
1564        assert_eq!(
1565            args.host,
1566            ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::<IpAddr>().unwrap()).to_vec()
1567        );
1568
1569        unsafe { env::set_var("ANVIL_IP_ADDR", "1.1.1.1") };
1570        let args = NodeArgs::parse_from(["anvil"]);
1571        assert_eq!(args.host, vec!["1.1.1.1".parse::<IpAddr>().unwrap()]);
1572
1573        unsafe { env::set_var("ANVIL_IP_ADDR", "::1,1.1.1.1,2.2.2.2") };
1574        let args = NodeArgs::parse_from(["anvil"]);
1575        assert_eq!(
1576            args.host,
1577            ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::<IpAddr>().unwrap()).to_vec()
1578        );
1579    }
1580
1581    #[test]
1582    fn can_parse_no_fork_node_info() {
1583        let args = NodeArgs::parse_from([
1584            "anvil",
1585            "--fork-url",
1586            "http://localhost:8545",
1587            "--no-fork-node-info",
1588        ]);
1589        assert!(args.evm.no_fork_node_info);
1590        let config = args.into_node_config().unwrap();
1591        assert!(config.no_fork_node_info);
1592    }
1593
1594    #[test]
1595    fn can_parse_multiple_fork_urls() {
1596        let args: NodeArgs = NodeArgs::parse_from([
1597            "anvil",
1598            "--fork-url",
1599            "http://localhost:8545",
1600            "--fork-url",
1601            "http://localhost:8546",
1602            "--fork-url",
1603            "http://localhost:8547",
1604        ]);
1605        assert_eq!(args.evm.fork_url.len(), 3);
1606        assert_eq!(args.evm.fork_url[0].url, "http://localhost:8545");
1607        assert_eq!(args.evm.fork_url[1].url, "http://localhost:8546");
1608        assert_eq!(args.evm.fork_url[2].url, "http://localhost:8547");
1609
1610        // Block suffix on first URL should work
1611        let args: NodeArgs = NodeArgs::parse_from([
1612            "anvil",
1613            "--fork-url",
1614            "http://localhost:8545@1000000",
1615            "--fork-url",
1616            "http://localhost:8546",
1617        ]);
1618        assert_eq!(args.evm.fork_url[0].block, Some(1000000));
1619        assert_eq!(args.evm.fork_url[1].block, None);
1620    }
1621
1622    #[test]
1623    fn rejects_block_suffix_on_secondary_fork_urls() {
1624        let args: NodeArgs = NodeArgs::parse_from([
1625            "anvil",
1626            "--fork-url",
1627            "http://localhost:8545@1000000",
1628            "--fork-url",
1629            "http://localhost:8546@2000000",
1630        ]);
1631        let result = args.into_node_config();
1632        assert!(result.is_err());
1633        assert!(
1634            result.unwrap_err().to_string().contains("Block number suffixes"),
1635            "should reject block suffix on secondary fork URL"
1636        );
1637    }
1638
1639    #[test]
1640    fn fork_dependent_args_require_fork_url() {
1641        // All these args have `requires = "fork_url"` — they should fail without --fork-url
1642        let cases = [
1643            vec!["anvil", "--fork-header", "X-Api-Key: test"],
1644            vec!["anvil", "--timeout", "5000"],
1645            vec!["anvil", "--retries", "3"],
1646            vec!["anvil", "--fork-block-number", "100"],
1647            vec!["anvil", "--fork-retry-backoff", "500"],
1648            vec!["anvil", "--no-fork-node-info"],
1649        ];
1650        for args in &cases {
1651            let result = NodeArgs::try_parse_from(args);
1652            assert!(result.is_err(), "expected error when using {:?} without --fork-url", args[1]);
1653        }
1654    }
1655
1656    #[test]
1657    fn transaction_coalescing_window_cli() {
1658        for (args, expected) in [
1659            (vec!["anvil"], 5),
1660            (vec!["anvil", "--transaction-coalescing-window", "0"], 0),
1661            (vec!["anvil", "--transaction-coalescing-window", "20"], 20),
1662        ] {
1663            let config = NodeArgs::parse_from(args).into_node_config().unwrap();
1664            assert_eq!(config.transaction_coalescing_window, Duration::from_millis(expected));
1665        }
1666        assert!(
1667            NodeArgs::try_parse_from(["anvil", "--transaction-coalescing-window", "-1"]).is_err()
1668        );
1669    }
1670}