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