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