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