use crate::{ AccountGenerator, CHAIN_ID, NodeConfig, config::{DEFAULT_MNEMONIC, DEFAULT_SLOTS_IN_AN_EPOCH, ForkChoice}, eth::{ EthApi, backend::db::SerializableState, miner::INSTANT_COALESCE_WINDOW, pool::transactions::TransactionOrder, }, }; use alloy_genesis::Genesis; use alloy_network::Network; use alloy_primitives::{Address, B256, U256, map::HashMap, utils::Unit}; use alloy_signer_local::coins_bip39::{English, Mnemonic}; use anvil_server::ServerConfig; use clap::Parser; use core::fmt; use foundry_common::shell; use foundry_config::{Chain, Config, FigmentProviders}; use foundry_evm_hardforks::FoundryHardfork; use foundry_evm_networks::NetworkConfigs; use foundry_primitives::FoundryReceiptEnvelope; use futures::FutureExt; use rand_08::{SeedableRng, rngs::StdRng}; use std::{ net::IpAddr, num::NonZeroU64, path::{Path, PathBuf}, pin::Pin, str::FromStr, sync::{ Arc, atomic::{AtomicUsize, Ordering}, }, task::{Context, Poll}, time::Duration, }; use tokio::time::{Instant, Interval}; #[derive(Clone, Debug, Parser)] pub struct NodeArgs { /// Port number to listen on. #[arg(long, short, default_value = "8545", value_name = "NUM")] pub port: u16, /// Number of dev accounts to generate and configure. #[arg(long, short, default_value = "10", value_name = "NUM")] pub accounts: u64, /// The balance of every dev account in Ether. #[arg(long, default_value = "10000", value_name = "NUM")] pub balance: u64, /// The timestamp of the genesis block. #[arg(long, value_name = "NUM")] pub timestamp: Option, /// The number of the genesis block. #[arg(long, value_name = "NUM")] pub number: Option, /// BIP39 mnemonic phrase used for generating accounts. /// Cannot be used if `mnemonic_random` or `mnemonic_seed` are used. #[arg(long, short, conflicts_with_all = &["mnemonic_seed", "mnemonic_random"])] pub mnemonic: Option, /// Automatically generates a BIP39 mnemonic phrase, and derives accounts from it. /// Cannot be used with other `mnemonic` options. /// You can specify the number of words you want in the mnemonic. /// [default: 12] #[arg(long, conflicts_with_all = &["mnemonic", "mnemonic_seed"], default_missing_value = "12", num_args(0..=1))] pub mnemonic_random: Option, /// Generates a BIP39 mnemonic phrase from a given seed /// Cannot be used with other `mnemonic` options. /// /// CAREFUL: This is NOT SAFE and should only be used for testing. /// Never use the private keys generated in production. #[arg(long = "mnemonic-seed-unsafe", conflicts_with_all = &["mnemonic", "mnemonic_random"])] pub mnemonic_seed: Option, /// Sets the derivation path of the child key to be derived. /// /// [default: m/44'/60'/0'/0/] #[arg(long)] pub derivation_path: Option, /// The account used to sponsor Tempo fee-payer requests (`eth_signRawTransaction` and raw /// transactions carrying the sponsorship placeholder). /// /// Must be an unlocked account. Only used on Tempo networks; defaults to the last dev /// account. #[arg(long = "tempo.fee-payer", value_name = "ADDRESS")] pub tempo_fee_payer: Option
, /// Number of block heights in a Tempo consensus epoch. Only used on Tempo networks. /// /// Defaults to the fork source's epoch length when it reports one, otherwise 21600. #[arg(long = "tempo.epoch-length", value_name = "BLOCKS")] pub tempo_epoch_length: Option, /// Development consensus seed for Tempo, as 32 bytes of hex. /// /// Defaults to a domain-separated hash of the dev-account mnemonic. Only used on Tempo /// networks. #[arg(long = "tempo.consensus-seed", value_name = "SEED")] pub tempo_consensus_seed: Option, /// The EVM hardfork to use. /// /// Choose the hardfork by name, e.g. `prague`, `cancun`, `shanghai`, `paris`, `london`, etc... /// [default: latest] #[arg(long)] pub hardfork: Option, /// Override the Base activation-registry administrator. #[cfg(feature = "base")] #[arg(long, value_name = "ADDRESS")] pub base_activation_admin: Option
, /// Block time in seconds for interval mining. #[arg(short, long, visible_alias = "blockTime", value_name = "SECONDS", value_parser = duration_from_secs_f64)] pub block_time: Option, /// Time in milliseconds to group ready transactions into one auto-mined block. /// Defaults to 0 (no delay). Set a positive value to enable transaction coalescing. #[arg(long, value_name = "MILLISECONDS", default_value_t = INSTANT_COALESCE_WINDOW.as_millis() as u64)] pub transaction_coalescing_window: u64, /// Slots in an epoch #[arg(long, value_name = "SLOTS_IN_AN_EPOCH", default_value_t = DEFAULT_SLOTS_IN_AN_EPOCH)] pub slots_in_an_epoch: u64, /// Writes output of `anvil` as json to user-specified file. #[arg(long, value_name = "FILE", value_hint = clap::ValueHint::FilePath)] pub config_out: Option, /// Disable auto and interval mining, and mine on demand instead. #[arg(long, visible_alias = "no-mine", conflicts_with = "block_time")] pub no_mining: bool, /// Enable mixed mining mode. Blocks are mined on a timer (set by `--block-time`), /// but also whenever a transaction is submitted. Requires `--block-time` to be set. #[arg(long, requires = "block_time")] pub mixed_mining: bool, /// The hosts the server will listen on. #[arg( long, value_name = "IP_ADDR", env = "ANVIL_IP_ADDR", default_value = "127.0.0.1", help_heading = "Server options", value_delimiter = ',' )] pub host: Vec, /// How transactions are sorted in the mempool. #[arg(long, default_value = "fees")] pub order: TransactionOrder, /// Initialize the genesis block with the given `genesis.json` file. #[arg(long, value_name = "PATH", value_parser= read_genesis_file)] pub init: Option, /// This is an alias for both --load-state and --dump-state. /// /// It initializes the chain with the state and block environment stored at the file, if it /// exists, and dumps the chain's state on exit. #[arg( long, value_name = "PATH", value_parser = StateFile::parse, conflicts_with_all = &[ "init", "dump_state", "load_state" ] )] pub state: Option, /// Interval in seconds at which the state and block environment is to be dumped to disk. /// /// See --state and --dump-state #[arg(short, long, value_name = "SECONDS")] pub state_interval: Option, /// Dump the state and block environment of chain on exit to the given file. /// /// If the value is a directory, the state will be written to `/state.json`. #[arg(long, value_name = "PATH", conflicts_with = "init")] pub dump_state: Option, /// Preserve historical state snapshots when dumping the state. /// /// This will save the in-memory states of the chain at particular block hashes. /// /// These historical states will be loaded into the memory when `--load-state` / `--state`, and /// aids in RPC calls beyond the block at which state was dumped. #[arg(long, conflicts_with = "init", default_value = "false")] pub preserve_historical_states: bool, /// Initialize the chain from a previously saved state snapshot. #[arg( long, value_name = "PATH", value_parser = SerializableState::parse, conflicts_with = "init" )] pub load_state: Option, /// Fund specific accounts with custom balances on startup. /// /// Accepts multiple address:balance pairs where balance is in ETH. /// Example: --fund-accounts 0x1234...5678:1000 0xabcd...ef01:5000 #[arg(long, value_name = "ADDRESS:AMOUNT", value_delimiter = ' ', num_args = 1..)] pub fund_accounts: Vec, #[arg(long, help = IPC_HELP, value_name = "PATH", visible_alias = "ipcpath")] pub ipc: Option>, /// Don't keep full chain history. /// If a number argument is specified, at most this number of states is kept in memory. /// /// If enabled, no state will be persisted on disk, so `max_persisted_states` will be 0. #[arg(long)] pub prune_history: Option>, /// Max number of states to persist on disk. /// /// Note that `prune_history` will overwrite `max_persisted_states` to 0. #[arg(long, conflicts_with = "prune_history")] pub max_persisted_states: Option, /// Number of blocks with transactions to keep in memory. #[arg(long)] pub transaction_block_keeper: Option, /// Maximum number of transactions in a block. #[arg(long)] pub max_transactions: Option, #[command(flatten)] pub evm: AnvilEvmArgs, #[command(flatten)] pub server_config: ServerConfig, /// Path to the cache directory where persisted states are stored (see /// `--max-persisted-states`). /// /// Note: This does not affect the fork RPC cache location, which uses endpoint-specific files /// under `~/.foundry/cache/rpc///`. #[arg(long, value_name = "PATH")] pub cache_path: Option, } #[cfg(windows)] const IPC_HELP: &str = "Launch an ipc server at the given path or default path = `\\.\\pipe\\anvil.ipc`"; /// The default IPC endpoint #[cfg(not(windows))] const IPC_HELP: &str = "Launch an ipc server at the given path or default path = `/tmp/anvil.ipc`"; /// Default interval for periodically dumping the state. const DEFAULT_DUMP_INTERVAL: Duration = Duration::from_secs(60); impl NodeArgs { pub fn into_node_config(self) -> eyre::Result { let genesis_balance = Unit::ETHER.wei().saturating_mul(U256::from(self.balance)); let compute_units_per_second = if self.evm.no_rate_limit { Some(u64::MAX) } else { self.evm.compute_units_per_second }; // Validate that secondary fork URLs don't have conflicting block number suffixes if self.evm.fork_url.len() > 1 { for fork in &self.evm.fork_url[1..] { if fork.block.is_some() { eyre::bail!( "Block number suffixes (@block) on secondary --fork-url values are not supported. \ Use --fork-block-number to set the fork block for all endpoints." ); } } } let funded_accounts = self.parse_funded_accounts()?; #[cfg(feature = "base")] let base_activation_admin = self.base_activation_admin; let local_chain_id = self .evm .chain_id .map(u64::from) .or_else(|| self.init.as_ref().map(|genesis| genesis.config.chain_id)); let inferred_network_chain_id = self .evm .fork_chain_id .map(u64::from) .or(if self.evm.fork_url.is_empty() { local_chain_id } else { None }); let networks = if let Some(chain_id) = inferred_network_chain_id { self.evm.networks.try_with_chain_id(chain_id).map_err(eyre::Report::msg)? } else { self.evm.networks }; let hardfork = match &self.hardfork { Some(hf) => Some(parse_hardfork(hf, &networks)?), None => None, }; let networks = if let Some(hardfork) = hardfork { networks.normalize_for_hardfork(hardfork).map_err(eyre::Report::msg)? } else { networks }; let config = NodeConfig::default() .with_gas_limit(self.evm.gas_limit) .disable_block_gas_limit(self.evm.disable_block_gas_limit) .enable_tx_gas_limit(self.evm.enable_tx_gas_limit) .with_gas_price(self.evm.gas_price) .with_hardfork(hardfork) .with_blocktime(self.block_time) .with_transaction_coalescing_window(Duration::from_millis( self.transaction_coalescing_window, )) .with_no_mining(self.no_mining) .with_mixed_mining(self.mixed_mining, self.block_time) .with_account_generator(self.account_generator())? .with_genesis_balance(genesis_balance) .with_genesis_timestamp(self.timestamp) .with_genesis_block_number(self.number) .with_port(self.port) .with_fork_choice(match (self.evm.fork_block_number, self.evm.fork_transaction_hash) { (Some(block), None) => Some(ForkChoice::Block(block)), (None, Some(hash)) => Some(ForkChoice::Transaction(hash)), _ => self .evm .fork_url .first() .and_then(|f| f.block) .map(|num| ForkChoice::Block(num as i128)), }) .with_fork_headers(self.evm.fork_headers) .with_fork_chain_id(self.evm.fork_chain_id.map(u64::from).map(U256::from)) .with_no_fork_node_info(self.evm.no_fork_node_info) .with_no_bal(self.evm.no_bal) .with_fork_state_by_number(self.evm.fork_state_by_number) .fork_request_timeout(self.evm.fork_request_timeout.map(Duration::from_millis)) .fork_request_retries(self.evm.fork_request_retries) .fork_retry_backoff(self.evm.fork_retry_backoff.map(Duration::from_millis)) .fork_compute_units_per_second(compute_units_per_second) .with_fork_urls(self.evm.fork_url.into_iter().map(|f| f.url).collect()) .with_base_fee(self.evm.block_base_fee_per_gas) .disable_min_priority_fee(self.evm.disable_min_priority_fee) .with_no_storage_caching(self.evm.no_storage_caching) .with_server_config(self.server_config) .with_host(self.host) .set_silent(shell::is_quiet()) .set_config_out(self.config_out) .with_transaction_order(self.order) .with_genesis(self.init) .with_steps_tracing(self.evm.steps_tracing) .with_print_logs(!self.evm.disable_console_log) .with_print_traces(self.evm.print_traces) .with_auto_impersonate(self.evm.auto_impersonate) .with_ipc(self.ipc) .with_code_size_limit(self.evm.code_size_limit) .disable_code_size_limit(self.evm.disable_code_size_limit) .set_pruned_history(self.prune_history) .with_init_state(self.load_state.or_else(|| self.state.and_then(|s| s.state))) .with_transaction_block_keeper(self.transaction_block_keeper) .with_max_transactions(self.max_transactions) .with_max_persisted_states(self.max_persisted_states) .with_chain_id(self.evm.chain_id) // Restore the source-derived or explicitly selected network after applying the // execution chain ID. Fork source discovery can refine an unresolved network later. .with_networks(networks) .with_tempo_fee_payer(self.tempo_fee_payer) .with_tempo_epoch_length(self.tempo_epoch_length) .with_tempo_consensus_seed(self.tempo_consensus_seed) .with_disable_default_create2_deployer(self.evm.disable_default_create2_deployer) .with_disable_pool_balance_checks(self.evm.disable_pool_balance_checks) .with_slots_in_an_epoch(self.slots_in_an_epoch) .with_memory_limit(self.evm.memory_limit) .with_cache_path(self.cache_path) .with_funded_accounts(funded_accounts); #[cfg(feature = "base")] let config = config.with_base_activation_admin(base_activation_admin); Ok(config) } fn parse_funded_accounts(&self) -> eyre::Result> { let mut accounts = HashMap::default(); for entry in &self.fund_accounts { let parts: Vec<&str> = entry.split(':').collect(); if parts.len() != 2 { eyre::bail!( "Invalid fund-accounts entry '{}'. Expected format: ADDRESS:AMOUNT", entry ); } let address = parts[0] .parse::
() .map_err(|e| eyre::eyre!("Invalid address '{}': {}", parts[0], e))?; let amount: u64 = parts[1] .parse() .map_err(|e| eyre::eyre!("Invalid amount '{}': {}", parts[1], e))?; let balance = Unit::ETHER.wei().saturating_mul(U256::from(amount)); accounts.insert(address, balance); } Ok(accounts) } fn account_generator(&self) -> AccountGenerator { let mut generator = AccountGenerator::new(self.accounts as usize) .phrase(DEFAULT_MNEMONIC) .chain_id(self.evm.chain_id.unwrap_or(CHAIN_ID.into())); if let Some(ref mnemonic) = self.mnemonic { generator = generator.phrase(mnemonic); } else if let Some(count) = self.mnemonic_random { let mut rng = rand_08::thread_rng(); let mnemonic = match Mnemonic::::new_with_count(&mut rng, count) { Ok(mnemonic) => mnemonic.to_phrase(), Err(err) => { warn!(target: "node", ?count, %err, "failed to generate mnemonic, falling back to 12-word random mnemonic"); // Fallback: generate a valid 12-word random mnemonic instead of using // DEFAULT_MNEMONIC Mnemonic::::new_with_count(&mut rng, 12) .expect("valid default word count") .to_phrase() } }; generator = generator.phrase(mnemonic); } else if let Some(seed) = self.mnemonic_seed { let mut seed = StdRng::seed_from_u64(seed); let mnemonic = Mnemonic::::new(&mut seed).to_phrase(); generator = generator.phrase(mnemonic); } if let Some(ref derivation) = self.derivation_path { generator = generator.derivation_path(derivation); } generator } /// Starts the node /// /// See also [crate::spawn()] pub async fn run(self) -> eyre::Result<()> { let dump_state = self.dump_state.as_ref().or_else(|| self.state.as_ref().map(|s| &s.path)).cloned(); let dump_interval = self.state_interval.map(Duration::from_secs).unwrap_or(DEFAULT_DUMP_INTERVAL); let preserve_historical_states = self.preserve_historical_states; let (api, mut handle) = crate::try_spawn(self.into_node_config()?).await?; // sets the signal handler to gracefully shutdown. let mut fork = api.get_fork(); let running = Arc::new(AtomicUsize::new(0)); // handle for the currently running rt, this must be obtained before setting the crtlc // handler, See [Handle::current] let mut signal = handle.shutdown_signal_mut().take(); let task_manager = handle.task_manager(); let mut on_shutdown = task_manager.on_shutdown(); let mut state_dumper = PeriodicStateDumper::new(api, dump_state, dump_interval, preserve_historical_states); task_manager.spawn(async move { // wait for the SIGTERM signal on unix systems #[cfg(unix)] let mut sigterm = Box::pin(async { if let Ok(mut stream) = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) { stream.recv().await; } else { futures::future::pending::<()>().await; } }); // On windows, this will never fire. #[cfg(not(unix))] let mut sigterm = Box::pin(futures::future::pending::<()>()); // await shutdown signal but also periodically flush state tokio::select! { _ = &mut sigterm => { trace!("received sigterm signal, shutting down"); } _ = &mut on_shutdown => {} _ = &mut state_dumper => {} } // shutdown received state_dumper.dump().await; // cleaning up and shutting down // this will make sure that the fork RPC cache is flushed if caching is configured if let Some(fork) = fork.take() { trace!("flushing cache on shutdown"); fork.database .read() .await .maybe_flush_cache() .expect("Could not flush cache on fork DB"); // cleaning up and shutting down // this will make sure that the fork RPC cache is flushed if caching is configured } std::process::exit(0); }); ctrlc::set_handler(move || { let prev = running.fetch_add(1, Ordering::SeqCst); if prev == 0 { trace!("received shutdown signal, shutting down"); let _ = signal.take(); } }) .expect("Error setting Ctrl-C handler"); Ok(handle.await??) } } /// Anvil's EVM related arguments. #[derive(Clone, Debug, Parser)] #[command(next_help_heading = "EVM options")] pub struct AnvilEvmArgs { /// Fetch state over a remote endpoint instead of starting from an empty state. /// /// 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. /// /// Multiple `--fork-url` flags can be provided to distribute requests across endpoints /// using round-robin load balancing. On failure, the retry layer rotates to the next /// endpoint. #[arg( long, short, visible_alias = "rpc-url", value_name = "URL", help_heading = "Fork config" )] pub fork_url: Vec, /// Headers to use for the rpc client, e.g. "User-Agent: test-agent" /// /// See --fork-url. #[arg( long = "fork-header", value_name = "HEADERS", help_heading = "Fork config", requires = "fork_url" )] pub fork_headers: Vec, /// Timeout in ms for requests sent to remote JSON-RPC server in forking mode. /// /// Default value 45000 #[arg(id = "timeout", long = "timeout", help_heading = "Fork config", requires = "fork_url")] pub fork_request_timeout: Option, /// Number of retry requests for spurious networks (timed out requests) /// /// Default value 5 #[arg(id = "retries", long = "retries", help_heading = "Fork config", requires = "fork_url")] pub fork_request_retries: Option, /// Fetch state from a specific block number over a remote endpoint. /// /// If negative, the given value is subtracted from the `latest` block number. /// /// See --fork-url. #[arg( long, requires = "fork_url", value_name = "BLOCK", help_heading = "Fork config", allow_hyphen_values = true )] pub fork_block_number: Option, /// Fetch state from after a specific transaction hash has been applied over a remote endpoint. /// /// See --fork-url. #[arg( long, requires = "fork_url", value_name = "TRANSACTION", help_heading = "Fork config", conflicts_with = "fork_block_number" )] pub fork_transaction_hash: Option, /// Initial retry backoff on encountering errors. /// /// See --fork-url. #[arg(long, requires = "fork_url", value_name = "BACKOFF", help_heading = "Fork config")] pub fork_retry_backoff: Option, /// Specify chain id to skip fetching it from remote endpoint. This enables offline-start mode. /// /// You still must pass both `--fork-url` and `--fork-block-number`, and already have your /// required state cached on disk, anything missing locally would be fetched from the /// remote. #[arg( long, help_heading = "Fork config", value_name = "CHAIN", requires = "fork_block_number" )] pub fork_chain_id: Option, /// Do not probe the fork endpoint with `anvil_nodeInfo` / `anvil_metadata`. /// /// Those calls detect a nested Anvil. Some public RPCs retry unknown methods for tens of /// seconds instead of returning method-not-found, which delays listen until the probe /// finishes. #[arg(long, requires = "fork_url", help_heading = "Fork config")] pub no_fork_node_info: bool, /// Disable pre-filling the fork cache with block access list (BAL) post-state. /// /// By default, eligible Ethereum forks briefly try fetching a BAL at the fork block. /// Missing or invalid BALs fall back to ordinary state reads. This setting also applies /// to subsequent resets and does not disable serving BAL RPC methods. #[arg(long, help_heading = "Fork config")] pub no_bal: bool, /// Read fork state by block number instead of by block hash. /// /// Use this for RPCs that cannot serve `eth_getBalance`, `eth_getCode`, `eth_getStorageAt` or /// `eth_getTransactionCount` for a block hash. Block hash ancestry is still validated against /// the fork block. /// /// See --fork-url. #[arg(long, requires = "fork_url", help_heading = "Fork config")] pub fork_state_by_number: bool, /// Sets the number of assumed available compute units per second for this provider /// /// default value: 330 /// /// See also --fork-url and #[arg( long, requires = "fork_url", alias = "cups", value_name = "CUPS", help_heading = "Fork config" )] pub compute_units_per_second: Option, /// Disables rate limiting for this node's provider. /// /// default value: false /// /// See also --fork-url and #[arg( long, requires = "fork_url", value_name = "NO_RATE_LIMITS", help_heading = "Fork config", visible_alias = "no-rpc-rate-limit" )] pub no_rate_limit: bool, /// Explicitly disables the use of RPC caching. /// /// All storage slots are read entirely from the endpoint. /// /// This flag overrides the project's configuration file. /// /// See --fork-url. #[arg(long, requires = "fork_url", help_heading = "Fork config")] pub no_storage_caching: bool, /// The block gas limit. #[arg(long, alias = "block-gas-limit", help_heading = "Environment config")] pub gas_limit: Option, /// Disable the `call.gas_limit <= block.gas_limit` constraint. #[arg( long, value_name = "DISABLE_GAS_LIMIT", help_heading = "Environment config", alias = "disable-gas-limit", conflicts_with = "gas_limit" )] pub disable_block_gas_limit: bool, /// Enable the transaction gas limit check as imposed by EIP-7825 (Osaka hardfork). /// /// From Amsterdam, the cap always applies to regular gas only, and gas above it becomes the /// EIP-8037 state gas reservoir. #[arg(long, visible_alias = "tx-gas-limit", help_heading = "Environment config")] pub enable_tx_gas_limit: bool, /// EIP-170: Contract code size limit in bytes. Useful to increase this because of tests. To /// disable entirely, use `--disable-code-size-limit`. By default, it is 0x6000 (~25kb). #[arg(long, value_name = "CODE_SIZE", help_heading = "Environment config")] pub code_size_limit: Option, /// Disable EIP-170: Contract code size limit. #[arg( long, value_name = "DISABLE_CODE_SIZE_LIMIT", conflicts_with = "code_size_limit", help_heading = "Environment config" )] pub disable_code_size_limit: bool, /// The gas price. #[arg(long, help_heading = "Environment config")] pub gas_price: Option, /// The base fee in a block. #[arg( long, visible_alias = "base-fee", value_name = "FEE", help_heading = "Environment config" )] pub block_base_fee_per_gas: Option, /// Disable the enforcement of a minimum suggested priority fee. #[arg(long, visible_alias = "no-priority-fee", help_heading = "Environment config")] pub disable_min_priority_fee: bool, /// The chain ID. #[arg(long, alias = "chain", help_heading = "Environment config")] pub chain_id: Option, /// Enable steps tracing used for debug calls returning geth-style traces /// /// Steps are recorded by replaying the transaction from its parent block's state, so they /// are unavailable once that state is pruned or when loading a state dump created without /// `--preserve-historical-states`. #[arg(long, visible_alias = "tracing")] pub steps_tracing: bool, /// Disable printing of `console.log` invocations to stdout. #[arg(long, visible_alias = "no-console-log")] pub disable_console_log: bool, /// Enable printing of traces for executed transactions and `eth_call` to stdout. #[arg(long, visible_alias = "enable-trace-printing")] pub print_traces: bool, /// Enables automatic impersonation on startup. This allows any transaction sender to be /// simulated as different accounts, which is useful for testing contract behavior. #[arg(long, visible_alias = "auto-unlock")] pub auto_impersonate: bool, /// Disable the default create2 deployer #[arg(long, visible_alias = "no-create2")] pub disable_default_create2_deployer: bool, /// Disable pool balance checks #[arg(long)] pub disable_pool_balance_checks: bool, /// The memory limit per EVM execution in bytes. #[arg(long)] pub memory_limit: Option, #[command(flatten)] pub networks: NetworkConfigs, } /// Resolves an alias passed as fork-url to the matching url defined in the rpc_endpoints section /// of the project configuration file. /// Does nothing if the fork-url is not a configured alias. /// /// When an alias maps to an `RpcEndpoint` with multiple `endpoints`, all URLs are expanded /// into additional `--fork-url` entries for multi-endpoint load balancing. impl AnvilEvmArgs { pub fn resolve_rpc_alias(&mut self) { if let Ok(config) = Config::load_with_providers(FigmentProviders::Anvil) { let mut resolved_urls = Vec::new(); for fork_url in &self.fork_url { let mut endpoints = config.rpc_endpoints.clone().resolved(); if let Some(endpoint) = endpoints.remove(&fork_url.url) { // Alias matched — expand all URLs from the endpoint config match endpoint.all_urls() { Ok(urls) => { for (i, url) in urls.into_iter().enumerate() { resolved_urls.push(ForkUrl { url, // Only the first URL inherits the block suffix block: if i == 0 { fork_url.block } else { None }, }); } } Err(e) => { warn!(target: "node", alias=%fork_url.url, %e, "could not resolve all endpoints, using primary endpoint only"); if let Ok(url) = endpoint.url() { resolved_urls.push(ForkUrl { url, block: fork_url.block }); } else { resolved_urls.push(fork_url.clone()); } } } } else if let Some(Ok(url)) = config.get_rpc_url_with_alias(&fork_url.url) { // Try mesc or other resolution resolved_urls.push(ForkUrl { url: url.to_string(), block: fork_url.block }); } else { // Not an alias — keep as-is resolved_urls.push(fork_url.clone()); } } self.fork_url = resolved_urls; } } } /// Helper type to periodically dump the state of the chain to disk struct PeriodicStateDumper { in_progress_dump: Option + Send + Sync + 'static>>>, api: EthApi, dump_state: Option, preserve_historical_states: bool, interval: Interval, } impl> PeriodicStateDumper { fn new( api: EthApi, dump_state: Option, interval: Duration, preserve_historical_states: bool, ) -> Self { let dump_state = dump_state.map(|mut dump_state| { if dump_state.is_dir() { dump_state = dump_state.join("state.json"); } dump_state }); // periodically flush the state let interval = tokio::time::interval_at(Instant::now() + interval, interval); Self { in_progress_dump: None, api, dump_state, preserve_historical_states, interval } } async fn dump(mut self) { // Shutdown no longer polls the periodic future, so release its locks first. self.in_progress_dump = None; if let Some(state) = self.dump_state { Self::dump_state(self.api, state, self.preserve_historical_states).await } } /// Infallible state dump async fn dump_state(api: EthApi, dump_state: PathBuf, preserve_historical_states: bool) { trace!(path=?dump_state, "Dumping state on shutdown"); match api.serialized_state(preserve_historical_states).await { Ok(state) => { if let Err(err) = foundry_common::fs::write_json_file(&dump_state, &state) { error!(?err, "Failed to dump state"); } else { trace!(path=?dump_state, "Dumped state on shutdown"); } } Err(err) => { error!(?err, "Failed to extract state"); } } } } // An endless future that periodically dumps the state to disk if configured. impl> Future for PeriodicStateDumper { type Output = (); fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.get_mut(); if this.dump_state.is_none() { return Poll::Pending; } loop { if let Some(mut flush) = this.in_progress_dump.take() { match flush.poll_unpin(cx) { Poll::Ready(_) => { this.interval.reset(); } Poll::Pending => { this.in_progress_dump = Some(flush); return Poll::Pending; } } } if this.interval.poll_tick(cx).is_ready() { let api = this.api.clone(); let path = this.dump_state.clone().expect("exists; see above"); this.in_progress_dump = Some(Box::pin(Self::dump_state(api, path, this.preserve_historical_states))); } else { break; } } Poll::Pending } } /// Represents the --state flag and where to load from, or dump the state to #[derive(Clone, Debug)] pub struct StateFile { pub path: PathBuf, pub state: Option, } impl StateFile { /// This is used as the clap `value_parser` implementation to parse from file but only if it /// exists fn parse(path: &str) -> Result { Self::parse_path(path) } /// Parse from file but only if it exists pub fn parse_path(path: impl AsRef) -> Result { let mut path = path.as_ref().to_path_buf(); if path.is_dir() { path = path.join("state.json"); } let mut state = Self { path, state: None }; if !state.path.exists() { return Ok(state); } state.state = Some(SerializableState::load(&state.path).map_err(|err| err.to_string())?); Ok(state) } } /// Represents the input URL for a fork with an optional trailing block number: /// `http://localhost:8545@1000000` #[derive(Clone, Debug, PartialEq, Eq)] pub struct ForkUrl { /// The endpoint url pub url: String, /// Optional trailing block pub block: Option, } impl fmt::Display for ForkUrl { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.url.fmt(f)?; if let Some(block) = self.block { write!(f, "@{block}")?; } Ok(()) } } impl FromStr for ForkUrl { type Err = String; fn from_str(s: &str) -> Result { if let Some((url, block)) = s.rsplit_once('@') { if block == "latest" { return Ok(Self { url: url.to_string(), block: None }); } // this will prevent false positives for auths `user:password@example.com` if !block.is_empty() && !block.contains(':') && !block.contains('.') { let block: u64 = block .parse() .map_err(|_| format!("Failed to parse block number: `{block}`"))?; return Ok(Self { url: url.to_string(), block: Some(block) }); } } Ok(Self { url: s.to_string(), block: None }) } } /// Parses a hardfork string against the active network configuration. fn parse_hardfork(hf: &str, networks: &NetworkConfigs) -> eyre::Result { if let Ok(hardfork) = FoundryHardfork::from_str(hf) { networks.normalize_for_hardfork(hardfork).map_err(eyre::Report::msg)?; return Ok(hardfork); } networks.execution_network().parse_hardfork(hf).map_err(eyre::Report::msg) } /// Clap's value parser for genesis. Loads a genesis.json file. fn read_genesis_file(path: &str) -> Result { foundry_common::fs::read_json_file(path.as_ref()).map_err(|err| err.to_string()) } fn duration_from_secs_f64(s: &str) -> Result { let s = s.parse::().map_err(|e| e.to_string())?; if s == 0.0 { return Err("Duration must be greater than 0".to_string()); } Duration::try_from_secs_f64(s).map_err(|e| e.to_string()) } #[cfg(test)] mod tests { use super::*; use foundry_evm_hardforks::EthereumHardfork; use std::{env, net::Ipv4Addr}; use tempo_hardfork::TempoHardfork; #[cfg(feature = "base")] use foundry_evm_hardforks::BaseUpgrade; #[cfg(feature = "optimism")] use foundry_evm_hardforks::OpHardfork; #[tokio::test] async fn final_dump_cancels_suspended_periodic_dump() { for queued_for_mining in [false, true] { let (api, _handle) = crate::spawn(NodeConfig::test().with_no_mining(true)).await; let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("state.json"); let mining = if queued_for_mining { Some(api.backend.lock_mining().await) } else { None }; let db = api.backend.get_db().write().await; let mut dumper = PeriodicStateDumper::new( api.clone(), Some(path.clone()), Duration::from_secs(60), false, ); dumper.in_progress_dump = Some(Box::pin(PeriodicStateDumper::dump_state(api.clone(), path.clone(), false))); assert!(futures::poll!(&mut dumper).is_pending()); // Shutdown stops polling the periodic dumper before starting the final dump. drop(db); drop(mining); tokio::time::timeout(Duration::from_secs(2), dumper.dump()) .await .expect("final dump waited for a suspended periodic dump"); let state = foundry_common::fs::read_json_file::(&path).unwrap(); assert_eq!(state.best_block_number, Some(0)); } } #[cfg(feature = "base")] #[test] fn base_chain_ids_select_native_base_unless_overridden() { for chain_id in ["8453", "84532"] { let config = NodeArgs::parse_from(["anvil", "--chain-id", chain_id]).into_node_config().unwrap(); assert!(config.networks.is_base()); let config = NodeArgs::parse_from(["anvil", "--chain-id", chain_id, "--network", "ethereum"]) .into_node_config() .unwrap(); assert!(config.networks.execution_network().is_ethereum()); } } #[test] fn fork_bal_can_be_disabled() { let args = NodeArgs::try_parse_from(["anvil", "--fork-url", "http://localhost:8545", "--no-bal"]); let config = args.unwrap().into_node_config().unwrap(); assert!(config.no_bal); assert!(!NodeArgs::parse_from(["anvil"]).into_node_config().unwrap().no_bal); assert!(NodeArgs::parse_from(["anvil", "--no-bal"]).into_node_config().unwrap().no_bal); } #[test] fn test_parse_fork_url() { let fork: ForkUrl = "http://localhost:8545@1000000".parse().unwrap(); assert_eq!( fork, ForkUrl { url: "http://localhost:8545".to_string(), block: Some(1000000) } ); let fork: ForkUrl = "http://localhost:8545".parse().unwrap(); assert_eq!(fork, ForkUrl { url: "http://localhost:8545".to_string(), block: None }); let fork: ForkUrl = "wss://user:password@example.com/".parse().unwrap(); assert_eq!( fork, ForkUrl { url: "wss://user:password@example.com/".to_string(), block: None } ); let fork: ForkUrl = "wss://user:password@example.com/@latest".parse().unwrap(); assert_eq!( fork, ForkUrl { url: "wss://user:password@example.com/".to_string(), block: None } ); let fork: ForkUrl = "wss://user:password@example.com/@100000".parse().unwrap(); assert_eq!( fork, ForkUrl { url: "wss://user:password@example.com/".to_string(), block: Some(100000) } ); } #[test] fn can_parse_ethereum_hardfork() { let args: NodeArgs = NodeArgs::parse_from(["anvil", "--hardfork", "berlin"]); let config = args.into_node_config().unwrap(); assert_eq!(config.hardfork, Some(EthereumHardfork::Berlin.into())); } #[cfg(feature = "optimism")] #[test] fn can_parse_optimism_hardfork() { let args: NodeArgs = NodeArgs::parse_from(["anvil", "--optimism", "--hardfork", "Regolith"]); let config = args.into_node_config().unwrap(); assert_eq!(config.hardfork, Some(OpHardfork::Regolith.into())); } #[test] fn can_parse_tempo_hardfork_from_network() { let args: NodeArgs = NodeArgs::parse_from(["anvil", "--network", "tempo", "--hardfork", "T5"]); let config = args.into_node_config().unwrap(); assert!(config.networks.is_tempo()); assert_eq!(config.hardfork, Some(TempoHardfork::T5.into())); } #[test] fn can_parse_namespaced_tempo_hardfork() { let args = NodeArgs::parse_from(["anvil", "--hardfork", "tempo:T5"]); let config = args.into_node_config().unwrap(); assert!(config.networks.is_tempo()); assert_eq!(config.hardfork, Some(TempoHardfork::T5.into())); } #[cfg(feature = "base")] #[test] fn can_parse_base_hardfork_from_network() { let args: NodeArgs = NodeArgs::parse_from(["anvil", "--network", "base", "--hardfork", "Beryl"]); let config = args.into_node_config().unwrap(); assert!(config.networks.is_base()); assert_eq!(config.hardfork, Some(BaseUpgrade::Beryl.into())); } #[cfg(feature = "base")] #[test] fn can_parse_namespaced_base_hardfork() { let args = NodeArgs::parse_from(["anvil", "--hardfork", "base:Beryl"]); let config = args.into_node_config().unwrap(); assert!(config.networks.is_base()); assert_eq!(config.hardfork, Some(BaseUpgrade::Beryl.into())); } #[cfg(feature = "base")] #[test] fn can_parse_base_activation_admin() { let admin = Address::repeat_byte(0xaa); let admin_arg = admin.to_string(); let args = NodeArgs::parse_from([ "anvil", "--network", "base", "--hardfork", "base:Beryl", "--base-activation-admin", admin_arg.as_str(), ]); let config = args.into_node_config().unwrap(); assert!(config.networks.is_base()); assert_eq!(config.base_activation_admin, Some(admin)); } #[cfg(feature = "optimism")] #[test] fn chain_id_infers_optimism_network_in_node_config() { let args: NodeArgs = NodeArgs::parse_from(["anvil", "--chain-id", "10"]); let config = args.into_node_config().unwrap(); assert!(config.networks.is_optimism()); } #[test] #[cfg(not(feature = "optimism"))] fn chain_id_rejects_disabled_optimism_network() { let args = NodeArgs::parse_from(["anvil", "--chain-id", "10"]); let error = args.into_node_config().unwrap_err(); assert_eq!( error.to_string(), "cannot infer execution network from chain ID 10: network family `optimism` is not \ enabled in this build" ); } #[test] #[cfg(feature = "base")] fn base_fork_chain_id_precedes_execution_chain_id_for_network() { let args = NodeArgs::parse_from([ "anvil", "--fork-url", "http://localhost:8545", "--fork-block-number", "1", "--fork-chain-id", "8453", "--chain-id", "1", ]); let config = args.into_node_config().unwrap(); assert!(config.networks.is_base()); assert_eq!(config.get_chain_id(), 1); } /// `anvil --chain-id 8453` resolved to Optimism before Base support existed and must keep /// doing so in builds without the feature, which is what release binaries ship. #[test] #[cfg(all(not(feature = "base"), feature = "optimism"))] fn chain_id_without_base_still_resolves_to_optimism() { for chain_id in ["8453", "84532"] { let args = NodeArgs::parse_from(["anvil", "--chain-id", chain_id]); let config = args .into_node_config() .unwrap_or_else(|error| panic!("chain ID {chain_id} must still resolve: {error}")); assert!(config.networks.is_optimism(), "chain ID {chain_id} must resolve to Optimism"); } } #[test] #[cfg(not(feature = "monad"))] fn chain_id_rejects_disabled_monad_network() { for chain_id in ["143", "10143"] { let args = NodeArgs::parse_from(["anvil", "--chain-id", chain_id]); let error = args.into_node_config().unwrap_err(); assert_eq!( error.to_string(), format!( "cannot infer execution network from chain ID {chain_id}: network family \ `monad` is not enabled in this build" ) ); } } #[test] fn explicit_ethereum_allows_base_chain_id() { let args = NodeArgs::parse_from(["anvil", "--network", "ethereum", "--chain-id", "8453"]); let config = args.into_node_config().unwrap(); assert_eq!(config.networks, NetworkConfigs::with_ethereum()); assert_eq!(config.get_chain_id(), 8453); } #[test] #[cfg(not(feature = "monad"))] fn explicit_ethereum_allows_monad_chain_id() { let args = NodeArgs::parse_from(["anvil", "--network", "ethereum", "--chain-id", "143"]); let config = args.into_node_config().unwrap(); assert_eq!(config.networks, NetworkConfigs::with_ethereum()); assert_eq!(config.get_chain_id(), 143); } #[test] #[cfg(not(feature = "monad"))] fn genesis_chain_id_rejects_disabled_monad_network() { let mut args = NodeArgs::parse_from(["anvil"]); let mut genesis = Genesis::default(); genesis.config.chain_id = 143; args.init = Some(genesis); let error = args.into_node_config().unwrap_err(); assert_eq!( error.to_string(), "cannot infer execution network from chain ID 143: network family `monad` is not \ enabled in this build" ); } #[test] #[cfg(feature = "base")] fn genesis_chain_id_infers_base_network() { let mut args = NodeArgs::parse_from(["anvil"]); let mut genesis = Genesis::default(); genesis.config.chain_id = 8453; args.init = Some(genesis); let config = args.into_node_config().unwrap(); assert!(config.networks.is_base()); } /// Base chain IDs resolved to Optimism before Base support existed, so a build without the /// `base` feature must keep resolving them that way rather than erroring. #[test] #[cfg(all(not(feature = "base"), feature = "optimism"))] fn genesis_chain_id_without_base_still_infers_optimism() { let mut args = NodeArgs::parse_from(["anvil"]); let mut genesis = Genesis::default(); genesis.config.chain_id = 8453; args.init = Some(genesis); let config = args.into_node_config().unwrap(); assert!(config.networks.is_optimism()); } #[test] #[cfg(not(feature = "monad"))] fn explicit_network_overrides_genesis_chain_id_inference() { let mut args = NodeArgs::parse_from(["anvil", "--network", "ethereum"]); let mut genesis = Genesis::default(); genesis.config.chain_id = 143; args.init = Some(genesis); let config = args.into_node_config().unwrap(); assert_eq!(config.networks, NetworkConfigs::with_ethereum()); assert_eq!(config.get_chain_id(), 143); } #[test] #[cfg(not(feature = "monad"))] fn explicit_chain_id_precedes_genesis_network_inference() { let mut args = NodeArgs::parse_from(["anvil", "--chain-id", "1"]); let mut genesis = Genesis::default(); genesis.config.chain_id = 143; args.init = Some(genesis); let config = args.into_node_config().unwrap(); assert!(!config.networks.has_network_selection()); assert_eq!(config.get_chain_id(), 1); } #[test] fn explicit_network_overrides_base_genesis_chain_id_inference() { let mut args = NodeArgs::parse_from(["anvil", "--network", "ethereum"]); let mut genesis = Genesis::default(); genesis.config.chain_id = 8453; args.init = Some(genesis); let config = args.into_node_config().unwrap(); assert_eq!(config.networks, NetworkConfigs::with_ethereum()); assert_eq!(config.get_chain_id(), 8453); } #[test] fn explicit_chain_id_precedes_base_genesis_network_inference() { let mut args = NodeArgs::parse_from(["anvil", "--chain-id", "1"]); let mut genesis = Genesis::default(); genesis.config.chain_id = 8453; args.init = Some(genesis); let config = args.into_node_config().unwrap(); assert!(!config.networks.has_network_selection()); assert_eq!(config.get_chain_id(), 1); } #[test] #[cfg(not(feature = "monad"))] fn disabled_fork_chain_id_precedes_execution_chain_id() { let args = NodeArgs::parse_from([ "anvil", "--fork-url", "http://localhost:8545", "--fork-block-number", "1", "--fork-chain-id", "10143", "--chain-id", "1", ]); let error = args.into_node_config().unwrap_err(); assert!(error.to_string().contains( "cannot infer execution network from chain ID 10143: network family `monad` is not \ enabled in this build" )); } #[test] fn fork_state_by_number_flag_sets_config() { let args = NodeArgs::parse_from([ "anvil", "--fork-url", "http://localhost:8545", "--fork-state-by-number", ]); let config = args.into_node_config().unwrap(); assert!(config.fork_state_by_number); } #[test] fn unknown_chain_id_preserves_ethereum_fallback() { let args = NodeArgs::parse_from(["anvil", "--chain-id", "98765432"]); let config = args.into_node_config().unwrap(); assert!(!config.networks.has_network_selection()); assert_eq!(config.get_chain_id(), 98_765_432); } #[test] fn chain_id_infers_tempo_network_for_hardfork() { let args = NodeArgs::parse_from(["anvil", "--chain-id", "4217", "--hardfork", "T5"]); let config = args.into_node_config().unwrap(); assert!(config.networks.is_tempo()); assert_eq!(config.hardfork, Some(TempoHardfork::T5.into())); } #[test] fn fork_chain_id_infers_tempo_network_for_hardfork() { let args = NodeArgs::parse_from([ "anvil", "--fork-url", "http://localhost:8545", "--fork-block-number", "1", "--fork-chain-id", "4217", "--hardfork", "T5", ]); let config = args.into_node_config().unwrap(); assert!(config.networks.is_tempo()); assert_eq!(config.hardfork, Some(TempoHardfork::T5.into())); } #[test] #[cfg(feature = "monad")] fn chain_id_infers_monad_network_in_node_config() { let args = NodeArgs::parse_from(["anvil", "--chain-id", "143"]); let config = args.into_node_config().unwrap(); assert!(config.networks.is_monad()); } #[test] #[cfg(feature = "monad")] fn fork_chain_id_precedes_execution_chain_id_for_network() { let args = NodeArgs::parse_from([ "anvil", "--fork-url", "http://localhost:8545", "--fork-block-number", "1", "--fork-chain-id", "10143", "--chain-id", "1", ]); let config = args.into_node_config().unwrap(); assert!(config.networks.is_monad()); assert_eq!(config.get_chain_id(), 1); } #[test] fn fork_execution_chain_id_does_not_infer_source_network() { let args = NodeArgs::parse_from([ "anvil", "--fork-url", "http://localhost:8545", "--chain-id", "4217", ]); let config = args.into_node_config().unwrap(); assert!(config.networks.resolved_network().is_none()); assert_eq!(config.get_chain_id(), 4217); } #[test] #[cfg(feature = "monad")] fn can_parse_monad_hardfork() { let args: NodeArgs = NodeArgs::parse_from(["anvil", "--network", "monad", "--hardfork", "MonadNine"]); let config = args.into_node_config().unwrap(); assert_eq!(config.hardfork, Some(foundry_evm_hardforks::MonadHardfork::MonadNine.into())); assert!(config.networks.is_monad()); } #[test] #[cfg(feature = "monad")] fn monad_uses_monad_default_hardfork() { let args: NodeArgs = NodeArgs::parse_from(["anvil", "--network", "monad"]); let config = args.into_node_config().unwrap(); assert_eq!(config.hardfork, None); assert_eq!(config.get_hardfork(), foundry_evm_hardforks::MonadHardfork::default().into()); assert!(config.networks.is_monad()); } #[test] fn cant_parse_invalid_hardfork() { let args: NodeArgs = NodeArgs::parse_from(["anvil", "--hardfork", "Regolith"]); let config = args.into_node_config(); assert!(config.is_err()); } #[test] fn can_parse_fork_headers() { let args: NodeArgs = NodeArgs::parse_from([ "anvil", "--fork-url", "http,://localhost:8545", "--fork-header", "User-Agent: test-agent", "--fork-header", "Referrer: example.com", ]); assert_eq!(args.evm.fork_headers, vec!["User-Agent: test-agent", "Referrer: example.com"]); } #[test] fn can_parse_prune_config() { let args: NodeArgs = NodeArgs::parse_from(["anvil", "--prune-history"]); assert!(args.prune_history.is_some()); let args: NodeArgs = NodeArgs::parse_from(["anvil", "--prune-history", "100"]); assert_eq!(args.prune_history, Some(Some(100))); } #[test] fn can_parse_max_persisted_states_config() { let args: NodeArgs = NodeArgs::parse_from(["anvil", "--max-persisted-states", "500"]); assert_eq!(args.max_persisted_states, (Some(500))); } #[test] fn can_parse_disable_block_gas_limit() { let args: NodeArgs = NodeArgs::parse_from(["anvil", "--disable-block-gas-limit"]); assert!(args.evm.disable_block_gas_limit); let args = NodeArgs::try_parse_from(["anvil", "--disable-block-gas-limit", "--gas-limit", "100"]); assert!(args.is_err()); } #[test] fn can_parse_enable_tx_gas_limit() { let args: NodeArgs = NodeArgs::parse_from(["anvil", "--enable-tx-gas-limit"]); assert!(args.evm.enable_tx_gas_limit); // Also test the alias let args: NodeArgs = NodeArgs::parse_from(["anvil", "--tx-gas-limit"]); assert!(args.evm.enable_tx_gas_limit); } #[test] fn can_parse_disable_code_size_limit() { let args: NodeArgs = NodeArgs::parse_from(["anvil", "--disable-code-size-limit"]); assert!(args.evm.disable_code_size_limit); let args = NodeArgs::try_parse_from([ "anvil", "--disable-code-size-limit", "--code-size-limit", "100", ]); // can't be used together assert!(args.is_err()); } #[test] fn can_parse_host() { let args = NodeArgs::parse_from(["anvil"]); assert_eq!(args.host, vec![IpAddr::V4(Ipv4Addr::LOCALHOST)]); let args = NodeArgs::parse_from([ "anvil", "--host", "::1", "--host", "1.1.1.1", "--host", "2.2.2.2", ]); assert_eq!( args.host, ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::().unwrap()).to_vec() ); let args = NodeArgs::parse_from(["anvil", "--host", "::1,1.1.1.1,2.2.2.2"]); assert_eq!( args.host, ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::().unwrap()).to_vec() ); unsafe { env::set_var("ANVIL_IP_ADDR", "1.1.1.1") }; let args = NodeArgs::parse_from(["anvil"]); assert_eq!(args.host, vec!["1.1.1.1".parse::().unwrap()]); unsafe { env::set_var("ANVIL_IP_ADDR", "::1,1.1.1.1,2.2.2.2") }; let args = NodeArgs::parse_from(["anvil"]); assert_eq!( args.host, ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::().unwrap()).to_vec() ); } #[test] fn can_parse_no_fork_node_info() { let args = NodeArgs::parse_from([ "anvil", "--fork-url", "http://localhost:8545", "--no-fork-node-info", ]); assert!(args.evm.no_fork_node_info); let config = args.into_node_config().unwrap(); assert!(config.no_fork_node_info); } #[test] fn can_parse_multiple_fork_urls() { let args: NodeArgs = NodeArgs::parse_from([ "anvil", "--fork-url", "http://localhost:8545", "--fork-url", "http://localhost:8546", "--fork-url", "http://localhost:8547", ]); assert_eq!(args.evm.fork_url.len(), 3); assert_eq!(args.evm.fork_url[0].url, "http://localhost:8545"); assert_eq!(args.evm.fork_url[1].url, "http://localhost:8546"); assert_eq!(args.evm.fork_url[2].url, "http://localhost:8547"); // Block suffix on first URL should work let args: NodeArgs = NodeArgs::parse_from([ "anvil", "--fork-url", "http://localhost:8545@1000000", "--fork-url", "http://localhost:8546", ]); assert_eq!(args.evm.fork_url[0].block, Some(1000000)); assert_eq!(args.evm.fork_url[1].block, None); } #[test] fn rejects_block_suffix_on_secondary_fork_urls() { let args: NodeArgs = NodeArgs::parse_from([ "anvil", "--fork-url", "http://localhost:8545@1000000", "--fork-url", "http://localhost:8546@2000000", ]); let result = args.into_node_config(); assert!(result.is_err()); assert!( result.unwrap_err().to_string().contains("Block number suffixes"), "should reject block suffix on secondary fork URL" ); } #[test] fn fork_dependent_args_require_fork_url() { // All these args have `requires = "fork_url"` — they should fail without --fork-url let cases = [ vec!["anvil", "--fork-header", "X-Api-Key: test"], vec!["anvil", "--timeout", "5000"], vec!["anvil", "--retries", "3"], vec!["anvil", "--fork-block-number", "100"], vec!["anvil", "--fork-retry-backoff", "500"], vec!["anvil", "--no-fork-node-info"], ]; for args in &cases { let result = NodeArgs::try_parse_from(args); assert!(result.is_err(), "expected error when using {:?} without --fork-url", args[1]); } } #[test] fn transaction_coalescing_window_cli() { for (args, expected) in [ (vec!["anvil"], 0), (vec!["anvil", "--transaction-coalescing-window", "0"], 0), (vec!["anvil", "--transaction-coalescing-window", "20"], 20), ] { let config = NodeArgs::parse_from(args).into_node_config().unwrap(); assert_eq!(config.transaction_coalescing_window, Duration::from_millis(expected)); } assert!( NodeArgs::try_parse_from(["anvil", "--transaction-coalescing-window", "-1"]).is_err() ); } #[test] fn tempo_consensus_arguments() { for epoch_length in ["1", "21600", "18446744073709551615"] { let config = NodeArgs::try_parse_from([ "anvil", "--network", "tempo", "--tempo.epoch-length", epoch_length, "--tempo.consensus-seed", "0x0101010101010101010101010101010101010101010101010101010101010101", ]) .unwrap() .into_node_config() .unwrap(); assert_eq!(config.tempo_epoch_length, epoch_length.parse().ok()); assert_eq!(config.tempo_consensus_seed(), B256::repeat_byte(1)); } for epoch_length in ["0", "-1", "18446744073709551616"] { assert!( NodeArgs::try_parse_from(["anvil", "--tempo.epoch-length", epoch_length,]).is_err() ); } for seed in ["0x01".to_owned(), "11".repeat(33), "zz".repeat(32)] { assert!(NodeArgs::try_parse_from(["anvil", "--tempo.consensus-seed", &seed]).is_err()); } } #[test] fn tempo_consensus_seed_uses_effective_mnemonic() { let default = NodeArgs::parse_from(["anvil"]).into_node_config().unwrap(); assert_eq!(default.tempo_epoch_length, None); assert_eq!(default.tempo_consensus_seed(), NodeConfig::default().tempo_consensus_seed()); let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; let custom = NodeArgs::parse_from(["anvil", "--mnemonic", mnemonic]).into_node_config().unwrap(); assert_ne!(custom.tempo_consensus_seed(), default.tempo_consensus_seed()); let seed = B256::repeat_byte(7); let explicit = NodeArgs::parse_from([ "anvil", "--mnemonic", mnemonic, "--tempo.consensus-seed", &seed.to_string(), ]) .into_node_config() .unwrap(); assert_eq!(explicit.tempo_consensus_seed(), seed); assert_eq!(explicit.genesis_accounts, custom.genesis_accounts); let seeded = NodeArgs::parse_from(["anvil", "--mnemonic-seed-unsafe", "42"]) .into_node_config() .unwrap(); let repeated = NodeArgs::parse_from(["anvil", "--mnemonic-seed-unsafe", "42"]) .into_node_config() .unwrap(); assert_ne!(seeded.tempo_consensus_seed(), default.tempo_consensus_seed()); assert_eq!(seeded.tempo_consensus_seed(), repeated.tempo_consensus_seed()); } }