anvil_zksync/
cli.rs

1use crate::utils::{
2    TELEMETRY_SENSITIVE_VALUE, get_cli_command_telemetry_props, parse_genesis_file,
3};
4use alloy::signers::local::coins_bip39::{English, Mnemonic};
5use anvil_zksync_common::{
6    cache::{CacheConfig, CacheType, DEFAULT_DISK_CACHE_DIR},
7    sh_err, sh_warn,
8    utils::io::write_json_file,
9};
10use anvil_zksync_config::types::{AccountGenerator, Genesis, SystemContractsOptions};
11use anvil_zksync_config::{BaseTokenConfig, L1Config, TestNodeConfig};
12use anvil_zksync_config::{
13    DebugTraceConfig,
14    constants::{DEFAULT_MNEMONIC, TEST_NODE_NETWORK_ID},
15    types::ZKsyncOsConfig,
16};
17use anvil_zksync_core::node::fork::ForkConfig;
18use anvil_zksync_core::node::{InMemoryNode, VersionedState};
19use anvil_zksync_types::{
20    LogLevel, ShowGasDetails, ShowStorageLogs, ShowVMDetails, TransactionOrder,
21};
22use clap::{ArgAction, Parser, Subcommand, ValueEnum};
23use flate2::read::GzDecoder;
24use futures::FutureExt;
25use num::rational::Ratio;
26use rand::{SeedableRng, rngs::StdRng};
27use std::collections::HashMap;
28use std::env;
29use std::io::Read;
30use std::net::IpAddr;
31use std::num::NonZeroU64;
32use std::path::PathBuf;
33use std::str::FromStr;
34use std::time::Duration;
35use std::{
36    future::Future,
37    pin::Pin,
38    task::{Context, Poll},
39};
40use tokio::time::{Instant, Interval};
41use url::Url;
42use zksync_telemetry::TelemetryProps;
43use zksync_types::fee_model::{BaseTokenConversionRatio, ConversionRatio};
44use zksync_types::{H256, ProtocolVersionId, U256};
45
46const DEFAULT_PORT: &str = "8011";
47const DEFAULT_HOST: &str = "0.0.0.0";
48const DEFAULT_ACCOUNTS: &str = "10";
49const DEFAULT_BALANCE: &str = "10000";
50const DEFAULT_ALLOW_ORIGIN: &str = "*";
51const DEFAULT_TX_ORDER: &str = "fifo";
52
53#[derive(Debug, Parser, Clone)]
54#[command(
55    author = "Matter Labs",
56    version,
57    about = "A fast and extensible local ZKsync test node.",
58    long_about = "anvil-zksync\n\nA developer-friendly ZKsync local node for testing."
59)]
60pub struct Cli {
61    #[command(subcommand)]
62    pub command: Option<Command>,
63
64    // General Options
65    #[arg(long, help_heading = "General Options")]
66    /// Run in offline mode (disables all network requests).
67    pub offline: bool,
68
69    #[arg(long, help_heading = "General Options")]
70    /// Enable health check endpoint.
71    /// It will be available for GET requests at /health.
72    /// The endpoint will return 200 OK if the node is healthy.
73    pub health_check_endpoint: bool,
74
75    /// Writes output of `anvil-zksync` as json to user-specified file.
76    #[arg(long, value_name = "OUT_FILE", help_heading = "General Options")]
77    pub config_out: Option<String>,
78
79    #[arg(long, default_value = DEFAULT_PORT, help_heading = "Network Options")]
80    /// Port to listen on (default: 8011).
81    pub port: Option<u16>,
82
83    /// The hosts the server will listen on.
84    #[arg(
85        long,
86        value_name = "IP_ADDR",
87        env = "ANVIL_ZKSYNC_IP_ADDR",
88        default_value = DEFAULT_HOST,
89        value_delimiter = ',',
90        help_heading = "Network Options"
91    )]
92    pub host: Vec<IpAddr>,
93
94    #[arg(long, help_heading = "Network Options")]
95    /// Specify chain ID (default: 260).
96    pub chain_id: Option<u32>,
97
98    #[arg(long, default_value = "true", default_missing_value = "true", num_args(0..=1), help_heading = "Debugging Options")]
99    /// If true, prints node config on startup.
100    pub show_node_config: Option<bool>,
101
102    // Debugging Options
103    #[arg(long, help_heading = "Debugging Options")]
104    /// Show storage log information.
105    pub show_storage_logs: Option<ShowStorageLogs>,
106
107    #[arg(long, help_heading = "Debugging Options")]
108    /// Show VM details information.
109    pub show_vm_details: Option<ShowVMDetails>,
110
111    #[arg(long, help_heading = "Debugging Options")]
112    /// Show gas details information.
113    pub show_gas_details: Option<ShowGasDetails>,
114
115    /// Increments verbosity each time it is used. (-vv, -vvv)
116    ///
117    /// Example usage:
118    ///   - `-vv` => verbosity level 2 (includes user calls, user L1-L2 logs, and event calls)
119    ///   - `-vvv` => level 3 (includes system calls, system L1-L2 logs, and system event calls)
120    ///   - `-vvvv` => level 4 (includes system calls, system event calls, and precompiles)
121    ///   - `-vvvvv` => level 5 (includes everything)
122    #[arg(short = 'v', long = "verbosity", action = ArgAction::Count, help_heading = "Debugging Options")]
123    pub verbosity: u8,
124
125    // Gas Configuration
126    #[arg(long, help_heading = "Gas Configuration")]
127    /// Custom L1 gas price (in wei).
128    pub l1_gas_price: Option<u64>,
129
130    #[arg(long, alias = "gas-price", help_heading = "Gas Configuration")]
131    /// Custom L2 gas price (in wei).
132    pub l2_gas_price: Option<u64>,
133
134    #[arg(long, help_heading = "Gas Configuration")]
135    /// Custom L1 pubdata price (in wei).
136    pub l1_pubdata_price: Option<u64>,
137
138    #[arg(long, help_heading = "Gas Configuration")]
139    /// Gas price estimation scale factor.
140    pub price_scale_factor: Option<f64>,
141
142    #[arg(long, help_heading = "Gas Configuration")]
143    /// Gas limit estimation scale factor.
144    pub limit_scale_factor: Option<f32>,
145
146    #[arg(long, help_heading = "System Configuration")]
147    /// Directory to override bytecodes.
148    pub override_bytecodes_dir: Option<String>,
149
150    #[arg(long, help_heading = "System Configuration")]
151    /// Enforces bytecode compression (default: false).
152    pub enforce_bytecode_compression: Option<bool>,
153
154    // System Configuration
155    #[arg(long, help_heading = "System Configuration")]
156    /// Option for system contracts (default: built-in).
157    pub dev_system_contracts: Option<SystemContractsOptions>,
158
159    /// Override the location of the compiled system contracts.
160    #[arg(
161        long,
162        help_heading = "System Configuration",
163        value_parser = clap::value_parser!(PathBuf),
164    )]
165    pub system_contracts_path: Option<PathBuf>,
166
167    /// Override the EVM emulator bytecode with a compiled artifact, independently of
168    /// --dev-system-contracts. Useful for testing a locally built emulator.
169    #[arg(
170        long,
171        help_heading = "System Configuration",
172        value_parser = clap::value_parser!(PathBuf),
173    )]
174    pub evm_emulator_path: Option<PathBuf>,
175
176    #[arg(long, value_parser = protocol_version_from_str, help_heading = "System Configuration")]
177    /// Protocol version to use for new blocks (default: 26). Also affects revision of built-in
178    /// contracts that will get deployed (if applicable).
179    pub protocol_version: Option<ProtocolVersionId>,
180
181    #[arg(long, help_heading = "System Configuration")]
182    /// Enables EVM interpreter.
183    pub evm_interpreter: bool,
184
185    #[clap(flatten)]
186    /// ZKsync OS detailed config.
187    pub zksync_os_group: ZKsyncOsGroup,
188
189    // Logging Configuration
190    #[arg(long, help_heading = "Logging Configuration")]
191    /// Log level (default: info).
192    pub log: Option<LogLevel>,
193
194    #[arg(long, help_heading = "Logging Configuration")]
195    /// Log file path (default: anvil-zksync.log).
196    pub log_file_path: Option<String>,
197
198    #[arg(long, alias = "quiet", default_missing_value = "true", num_args(0..=1), help_heading = "Logging Configuration")]
199    /// If true, the tool will not print anything on startup.
200    pub silent: Option<bool>,
201
202    // Cache Options
203    #[arg(long, help_heading = "Cache Options")]
204    /// Cache type (none, memory, or disk). Default: "disk".
205    pub cache: Option<CacheType>,
206
207    #[arg(long, help_heading = "Cache Options")]
208    /// Reset the local disk cache.
209    pub reset_cache: Option<bool>,
210
211    #[arg(long, help_heading = "Cache Options")]
212    /// Cache directory location for disk cache (default: .cache).
213    pub cache_dir: Option<String>,
214
215    /// Number of dev accounts to generate and configure.
216    #[arg(
217        long,
218        short,
219        default_value = DEFAULT_ACCOUNTS,
220        value_name = "NUM",
221        help_heading = "Account Configuration"
222    )]
223    pub accounts: u64,
224
225    /// The balance of every dev account in Ether.
226    #[arg(
227        long,
228        default_value = DEFAULT_BALANCE,
229        value_name = "NUM",
230        help_heading = "Account Configuration"
231    )]
232    pub balance: u64,
233
234    /// The timestamp of the genesis block.
235    #[arg(long, value_name = "NUM")]
236    pub timestamp: Option<u64>,
237
238    /// Initialize the genesis block with the given `genesis.json` file.
239    #[arg(long, value_name = "PATH", value_parser= parse_genesis_file)]
240    pub init: Option<Genesis>,
241
242    /// This is an alias for both --load-state and --dump-state.
243    ///
244    /// It initializes the chain with the state and block environment stored at the file, if it
245    /// exists, and dumps the chain's state on exit.
246    #[arg(
247        long,
248        value_name = "PATH",
249        conflicts_with_all = &[
250            "init",
251            "dump_state",
252            "load_state"
253        ]
254    )]
255    pub state: Option<PathBuf>,
256
257    /// Interval in seconds at which the state and block environment is to be dumped to disk.
258    ///
259    /// See --state and --dump-state
260    #[arg(short, long, value_name = "SECONDS")]
261    pub state_interval: Option<u64>,
262
263    /// Dump the state and block environment of chain on exit to the given file.
264    ///
265    /// If the value is a directory, the state will be written to `<VALUE>/state.json`.
266    #[arg(long, value_name = "PATH", conflicts_with = "init")]
267    pub dump_state: Option<PathBuf>,
268
269    /// Preserve historical state snapshots when dumping the state.
270    ///
271    /// This will save the in-memory states of the chain at particular block hashes.
272    ///
273    /// These historical states will be loaded into the memory when `--load-state` / `--state`, and
274    /// aids in RPC calls beyond the block at which state was dumped.
275    #[arg(long, conflicts_with = "init", default_value = "false")]
276    pub preserve_historical_states: bool,
277
278    /// Initialize the chain from a previously saved state snapshot.
279    #[arg(long, value_name = "PATH", conflicts_with = "init")]
280    pub load_state: Option<PathBuf>,
281
282    /// BIP39 mnemonic phrase used for generating accounts.
283    /// Cannot be used if `mnemonic_random` or `mnemonic_seed` are used.
284    #[arg(long, short, conflicts_with_all = &["mnemonic_seed", "mnemonic_random"], help_heading = "Account Configuration")]
285    pub mnemonic: Option<String>,
286
287    /// Automatically generates a BIP39 mnemonic phrase and derives accounts from it.
288    /// Cannot be used with other `mnemonic` options.
289    /// You can specify the number of words you want in the mnemonic.
290    /// [default: 12]
291    #[arg(long, conflicts_with_all = &["mnemonic", "mnemonic_seed"], default_missing_value = "12", num_args(0..=1), help_heading = "Account Configuration")]
292    pub mnemonic_random: Option<usize>,
293
294    /// Generates a BIP39 mnemonic phrase from a given seed.
295    /// Cannot be used with other `mnemonic` options.
296    /// CAREFUL: This is NOT SAFE and should only be used for testing.
297    /// Never use the private keys generated in production.
298    #[arg(long = "mnemonic-seed-unsafe", conflicts_with_all = &["mnemonic", "mnemonic_random"],  help_heading = "Account Configuration")]
299    pub mnemonic_seed: Option<u64>,
300
301    /// Sets the derivation path of the child key to be derived.
302    /// [default: m/44'/60'/0'/0/]
303    #[arg(long, help_heading = "Account Configuration")]
304    pub derivation_path: Option<String>,
305
306    /// Enables automatic impersonation on startup. This allows any transaction sender to be
307    /// simulated as different accounts, which is useful for testing contract behavior.
308    #[arg(
309        long,
310        visible_alias = "auto-unlock",
311        help_heading = "Account Configuration"
312    )]
313    pub auto_impersonate: bool,
314
315    /// Block time in seconds for interval sealing.
316    /// If unset, node seals a new block as soon as there is at least one transaction.
317    #[arg(short, long, value_name = "SECONDS", value_parser = duration_from_secs_f64, help_heading = "Block Sealing")]
318    pub block_time: Option<Duration>,
319
320    /// Disable auto and interval mining, and mine on demand instead.
321    #[arg(long, visible_alias = "no-mine", conflicts_with = "block_time")]
322    pub no_mining: bool,
323
324    /// The cors `allow_origin` header
325    #[arg(long, default_value = DEFAULT_ALLOW_ORIGIN, help_heading = "Server options")]
326    pub allow_origin: String,
327
328    /// Disable CORS.
329    #[arg(long, conflicts_with = "allow_origin", help_heading = "Server options")]
330    pub no_cors: bool,
331
332    /// Transaction ordering in the mempool.
333    #[arg(long, default_value = DEFAULT_TX_ORDER)]
334    pub order: TransactionOrder,
335
336    #[clap(flatten)]
337    pub l1_group: Option<L1Group>,
338
339    /// Enable automatic execution of L1 batches
340    #[arg(long, requires = "l1_group", default_missing_value = "true", num_args(0..=1), help_heading = "UNSTABLE - L1")]
341    pub auto_execute_l1: Option<bool>,
342
343    /// Base token symbol to use instead of 'ETH'.
344    #[arg(long, help_heading = "Custom Base Token")]
345    pub base_token_symbol: Option<String>,
346
347    /// Base token conversion ratio (e.g., '40000', '628/17').
348    #[arg(long, help_heading = "Custom Base Token")]
349    pub base_token_ratio: Option<Ratio<u64>>,
350}
351
352#[derive(Clone, Debug, clap::Args)]
353pub struct ZKsyncOsGroup {
354    /// Enables ZKsync OS.
355    #[arg(long, help_heading = "UNSTABLE - ZKsync OS")]
356    pub zksync_os: bool,
357
358    /// Path to ZKsync OS binary (if you need to compute witnesses).
359    #[arg(long, requires = "zksync_os", help_heading = "UNSTABLE - ZKsync OS")]
360    pub zksync_os_bin_path: Option<String>,
361}
362
363impl From<ZKsyncOsGroup> for ZKsyncOsConfig {
364    fn from(group: ZKsyncOsGroup) -> Self {
365        ZKsyncOsConfig {
366            zksync_os: group.zksync_os,
367            zksync_os_bin_path: group.zksync_os_bin_path,
368        }
369    }
370}
371
372#[derive(Debug, Clone, clap::Args)]
373#[group(id = "l1_group", multiple = false)]
374pub struct L1Group {
375    /// Enable L1 support and spawn L1 anvil node on the provided port (defaults to 8012).
376    #[arg(long, conflicts_with = "external_l1", default_missing_value = "8012", num_args(0..=1), help_heading = "UNSTABLE - L1")]
377    pub spawn_l1: Option<u16>,
378
379    /// Enable L1 support and use provided address as L1 JSON-RPC endpoint.
380    #[arg(long, conflicts_with = "spawn_l1", help_heading = "UNSTABLE - L1")]
381    pub external_l1: Option<String>,
382}
383
384#[derive(Debug, Subcommand, Clone)]
385pub enum Command {
386    /// Starts a new empty local network.
387    #[command(name = "run")]
388    Run,
389    /// Starts a local network that is a fork of another network.
390    #[command(name = "fork")]
391    Fork(ForkArgs),
392    /// Starts a local network that is a fork of another network, and replays a given TX on it.
393    #[command(name = "replay_tx")]
394    ReplayTx(ReplayArgs),
395    /// Fetches debug_traceTransaction for a TX and prints formatted traces (respects -v).
396    #[command(name = "debug-trace")]
397    DebugTrace(DebugTxArgs),
398}
399
400#[derive(Debug, Parser, Clone)]
401pub struct ForkArgs {
402    /// Whether to fork from existing network.
403    /// If not set - will start a new network from genesis.
404    /// If set - will try to fork a remote network. Possible values:
405    /// Possible values:
406    ///   • `era` / `mainnet`
407    ///   • `era-testnet` / `sepolia-testnet`
408    ///   • `abstract` / `abstract-testnet`
409    ///   • `sophon` / `sophon-testnet`
410    ///   • `cronos` / `cronos-testnet`
411    ///   • `lens` / `lens-testnet`
412    ///   • `openzk` / `openzk-testnet`
413    ///   • `zkcandy`
414    ///  - http://XXX:YY
415    #[arg(
416        long,
417        alias = "network",
418        value_enum,
419        help = "Which network to fork (builtins) or an HTTP(S) URL"
420    )]
421    pub fork_url: ForkUrl,
422    // Fork at a given L2 miniblock height.
423    // If not set - will use the current finalized block from the network.
424    #[arg(
425        long,
426        value_name = "BLOCK",
427        long_help = "Fetch state from a specific block number over a remote endpoint.",
428        alias = "fork-at"
429    )]
430    pub fork_block_number: Option<u64>,
431
432    /// Fetch state from a specific transaction hash over a remote endpoint.
433    ///
434    /// See --fork-url.
435    #[arg(
436        long,
437        requires = "fork_url",
438        value_name = "TRANSACTION",
439        conflicts_with = "fork_block_number"
440    )]
441    pub fork_transaction_hash: Option<H256>,
442}
443
444#[derive(Debug, Parser, Clone)]
445pub struct ReplayArgs {
446    /// Whether to fork from existing network.
447    /// If not set - will start a new network from genesis.
448    /// If set - will try to fork a remote network. Possible values:
449    /// Possible values:
450    ///   • `era` / `mainnet`
451    ///   • `era-testnet` / `sepolia-testnet`
452    ///   • `abstract` / `abstract-testnet`
453    ///   • `sophon` / `sophon-testnet`
454    ///   • `cronos` / `cronos-testnet`
455    ///   • `lens` / `lens-testnet`
456    ///   • `openzk` / `openzk-testnet`
457    ///   • `zkcandy`
458    ///   • custom HTTP(S) URL
459    ///  - http://XXX:YY
460    #[arg(
461        long,
462        alias = "network",
463        value_enum,
464        help = "Which network to fork (builtins) or an HTTP(S) URL"
465    )]
466    pub fork_url: ForkUrl,
467    /// Transaction hash to replay.
468    #[arg(help = "Transaction hash to replay.")]
469    pub tx: H256,
470}
471
472#[derive(Debug, Parser, Clone)]
473pub struct DebugTxArgs {
474    #[arg(
475        long,
476        alias = "network",
477        value_enum,
478        help = "Which network to fork (builtins) or an HTTP(S) URL"
479    )]
480    pub fork_url: ForkUrl,
481
482    /// Transaction hash to debug.
483    pub tx: H256,
484
485    /// Only top-level call from debug API (passes through to `only_top_call`).
486    #[arg(long)]
487    pub only_top: bool,
488}
489
490// Elastic Network ZK Chains
491#[derive(Debug, Clone, ValueEnum)]
492#[value(rename_all = "kebab-case")]
493pub enum BuiltinNetwork {
494    #[value(alias = "mainnet")]
495    Era,
496    #[value(alias = "sepolia-testnet")]
497    EraTestnet,
498    Abstract,
499    AbstractTestnet,
500    #[value(alias = "sophon-mainnet")]
501    Sophon,
502    SophonTestnet,
503    Cronos,
504    CronosTestnet,
505    Lens,
506    LensTestnet,
507    Openzk,
508    OpenzkTestnet,
509    Zkcandy,
510}
511
512/// ForkUrl is used to specify the URL of the forked network.
513#[derive(Debug, Clone)]
514pub enum ForkUrl {
515    Builtin(BuiltinNetwork),
516    Custom(Url),
517}
518
519impl BuiltinNetwork {
520    /// Converts the BuiltinNetwork to a ForkConfig.
521    pub fn to_fork_config(&self) -> ForkConfig {
522        match self {
523            BuiltinNetwork::Era => ForkConfig {
524                url: "https://mainnet.era.zksync.io".parse().unwrap(),
525                estimate_gas_price_scale_factor: 1.5,
526                estimate_gas_scale_factor: 1.3,
527            },
528            BuiltinNetwork::EraTestnet => ForkConfig {
529                url: "https://sepolia.era.zksync.dev".parse().unwrap(),
530                estimate_gas_price_scale_factor: 2.0,
531                estimate_gas_scale_factor: 1.3,
532            },
533            BuiltinNetwork::Abstract => ForkConfig {
534                url: "https://api.mainnet.abs.xyz".parse().unwrap(),
535                estimate_gas_price_scale_factor: 1.5,
536                estimate_gas_scale_factor: 1.3,
537            },
538            BuiltinNetwork::AbstractTestnet => ForkConfig {
539                url: "https://api.testnet.abs.xyz".parse().unwrap(),
540                estimate_gas_price_scale_factor: 1.5,
541                estimate_gas_scale_factor: 1.3,
542            },
543            BuiltinNetwork::Sophon => ForkConfig {
544                url: "https://rpc.sophon.xyz".parse().unwrap(),
545                estimate_gas_price_scale_factor: 1.5,
546                estimate_gas_scale_factor: 1.3,
547            },
548            BuiltinNetwork::SophonTestnet => ForkConfig {
549                url: "https://rpc.testnet.sophon.xyz".parse().unwrap(),
550                estimate_gas_price_scale_factor: 1.5,
551                estimate_gas_scale_factor: 1.3,
552            },
553            BuiltinNetwork::Cronos => ForkConfig {
554                url: "https://mainnet.zkevm.cronos.org".parse().unwrap(),
555                estimate_gas_price_scale_factor: 1.5,
556                estimate_gas_scale_factor: 1.3,
557            },
558            BuiltinNetwork::CronosTestnet => ForkConfig {
559                url: "https://testnet.zkevm.cronos.org".parse().unwrap(),
560                estimate_gas_price_scale_factor: 1.5,
561                estimate_gas_scale_factor: 1.3,
562            },
563            BuiltinNetwork::Lens => ForkConfig {
564                url: "https://rpc.lens.xyz".parse().unwrap(),
565                estimate_gas_price_scale_factor: 1.5,
566                estimate_gas_scale_factor: 1.3,
567            },
568            BuiltinNetwork::LensTestnet => ForkConfig {
569                url: "https://rpc.testnet.lens.xyz".parse().unwrap(),
570                estimate_gas_price_scale_factor: 1.5,
571                estimate_gas_scale_factor: 1.3,
572            },
573            BuiltinNetwork::Openzk => ForkConfig {
574                url: "https://rpc.openzk.net".parse().unwrap(),
575                estimate_gas_price_scale_factor: 1.5,
576                estimate_gas_scale_factor: 1.3,
577            },
578            BuiltinNetwork::OpenzkTestnet => ForkConfig {
579                url: "https://openzk-testnet.rpc.caldera.xyz/http"
580                    .parse()
581                    .unwrap(),
582                estimate_gas_price_scale_factor: 1.5,
583                estimate_gas_scale_factor: 1.3,
584            },
585            BuiltinNetwork::Zkcandy => ForkConfig {
586                url: "https://rpc.zkcandy.io".parse().unwrap(),
587                estimate_gas_price_scale_factor: 1.5,
588                estimate_gas_scale_factor: 1.3,
589            },
590        }
591    }
592}
593
594impl ForkUrl {
595    /// Converts the ForkUrl to a ForkConfig.
596    pub fn to_config(&self) -> ForkConfig {
597        match self {
598            ForkUrl::Builtin(net) => net.to_fork_config(),
599            ForkUrl::Custom(url) => ForkConfig::unknown(url.clone()),
600        }
601    }
602}
603
604impl FromStr for ForkUrl {
605    type Err = String;
606
607    fn from_str(s: &str) -> Result<Self, Self::Err> {
608        if let Ok(net) = BuiltinNetwork::from_str(s, true) {
609            return Ok(ForkUrl::Builtin(net));
610        }
611        Url::parse(s)
612            .map(ForkUrl::Custom)
613            .map_err(|e| format!("`{s}` is neither a known network nor a valid URL: {e}"))
614    }
615}
616
617impl Cli {
618    /// Checks for deprecated options and warns users.
619    pub fn deprecated_config_option() {
620        let args: Vec<String> = env::args().collect();
621
622        let deprecated_flags: HashMap<&str, &str> = [
623        ("--config",
624            "⚠ The '--config' option has been removed. Please migrate to using other configuration options or defaults."),
625
626        ("--show-calls",
627            "⚠ The '--show-calls' option is deprecated. Use verbosity levels instead:\n\
628             -vv  → Show user calls\n\
629             -vvv → Show system calls"),
630
631        ("--show-event-logs",
632            "⚠ The '--show-event-logs' option is deprecated. Event logs are now included in traces by default.\n\
633             Use verbosity levels instead:\n\
634             -vv  → Show user calls\n\
635             -vvv → Show system calls"),
636
637        ("--resolve-hashes",
638            "⚠ The '--resolve-hashes' option is deprecated. Automatic decoding of function and event selectors\n\
639             using OpenChain is now enabled by default, unless running in offline mode.\n\
640             If needed, disable it explicitly with `--offline`."),
641
642        ("--show-outputs",
643            "⚠ The '--show-outputs' option has been deprecated. Output logs are now included in traces by default."),
644
645        ("--debug",
646            "⚠ The '--debug' (or '-d') option is deprecated. Use verbosity levels instead:\n\
647             -vv  → Show user calls\n\
648             -vvv → Show system calls"),
649
650        ("-d",
651            "⚠ The '-d' option is deprecated. Use verbosity levels instead:\n\
652             -vv  → Show user calls\n\
653             -vvv → Show system calls"),
654    ]
655    .iter()
656    .copied()
657    .collect();
658
659        // Prefix flags that may have values assigned (e.g., --show-calls=system)
660        let prefix_flags = [
661            "--config=",
662            "--show-calls=",
663            "--resolve-hashes=",
664            "--show-outputs=",
665            "--show-event-logs=",
666        ];
667
668        let mut detected = false;
669
670        for arg in &args {
671            if let Some(warning) = deprecated_flags.get(arg.as_str()) {
672                if !detected {
673                    sh_warn!("⚠ Deprecated CLI Options Detected (as of v0.4.0):\n");
674                    sh_warn!("[Options below will be removed in v0.4.1]\n");
675                    detected = true;
676                }
677                sh_warn!("{}", warning);
678            } else if let Some(base_flag) =
679                prefix_flags.iter().find(|&&prefix| arg.starts_with(prefix))
680            {
681                let warning = deprecated_flags
682                    .get(base_flag.trim_end_matches("="))
683                    .unwrap_or(&"⚠ Unknown deprecated option.");
684                if !detected {
685                    sh_warn!("⚠ Deprecated CLI Options Detected (as of v0.4.0):\n");
686                    sh_warn!("[Options below will be removed in v0.4.1]\n");
687                    detected = true;
688                }
689                sh_warn!("{}", warning);
690            }
691        }
692    }
693
694    /// Converts the CLI arguments to a `TestNodeConfig`.
695    pub fn into_test_node_config(
696        self,
697    ) -> Result<TestNodeConfig, zksync_error::anvil_zksync::env::AnvilEnvironmentError> {
698        // We keep a serialized version of the provided arguments to communicate them later if the arguments were incorrect.
699        let debug_self_repr = format!("{self:#?}");
700
701        let genesis_balance = U256::from(self.balance as u128 * 10u128.pow(18));
702        let mut config = TestNodeConfig::default()
703            .with_port(self.port)
704            .with_offline(if self.offline { Some(true) } else { None })
705            .with_l1_gas_price(self.l1_gas_price)
706            .with_l2_gas_price(self.l2_gas_price)
707            .with_l1_pubdata_price(self.l1_pubdata_price)
708            .with_vm_log_detail(self.show_vm_details)
709            .with_show_storage_logs(self.show_storage_logs)
710            .with_show_gas_details(self.show_gas_details)
711            .with_gas_limit_scale(self.limit_scale_factor)
712            .with_price_scale(self.price_scale_factor)
713            .with_verbosity_level(self.verbosity)
714            .with_show_node_config(self.show_node_config)
715            .with_silent(self.silent)
716            .with_system_contracts(self.dev_system_contracts)
717            .with_system_contracts_path(self.system_contracts_path.clone())
718            .with_evm_emulator_path(self.evm_emulator_path.clone())
719            .with_protocol_version(self.protocol_version)
720            .with_override_bytecodes_dir(self.override_bytecodes_dir.clone())
721            .with_enforce_bytecode_compression(self.enforce_bytecode_compression)
722            .with_log_level(self.log)
723            .with_log_file_path(self.log_file_path.clone())
724            .with_account_generator(self.account_generator())
725            .with_auto_impersonate(self.auto_impersonate)
726            .with_genesis_balance(genesis_balance)
727            .with_cache_dir(self.cache_dir.clone())
728            .with_cache_config(self.cache.map(|cache_type| {
729                match cache_type {
730                    CacheType::None => CacheConfig::None,
731                    CacheType::Memory => CacheConfig::Memory,
732                    CacheType::Disk => CacheConfig::Disk {
733                        dir: self
734                            .cache_dir
735                            .unwrap_or_else(|| DEFAULT_DISK_CACHE_DIR.to_string()),
736                        reset: self.reset_cache.unwrap_or(false),
737                    },
738                }
739            }))
740            .with_genesis_timestamp(self.timestamp)
741            .with_genesis(self.init)
742            .with_chain_id(self.chain_id)
743            .set_config_out(self.config_out)
744            .with_host(self.host)
745            .with_evm_interpreter(if self.evm_interpreter {
746                Some(true)
747            } else {
748                None
749            })
750            .with_zksync_os(self.zksync_os_group.into())
751            .with_health_check_endpoint(if self.health_check_endpoint {
752                Some(true)
753            } else {
754                None
755            })
756            .with_block_time(self.block_time)
757            .with_no_mining(self.no_mining)
758            .with_allow_origin(self.allow_origin)
759            .with_no_cors(self.no_cors)
760            .with_transaction_order(self.order)
761            .with_state(self.state)
762            .with_state_interval(self.state_interval)
763            .with_dump_state(self.dump_state)
764            .with_preserve_historical_states(self.preserve_historical_states)
765            .with_load_state(self.load_state)
766            .with_l1_config(self.l1_group.and_then(|group| {
767                group.spawn_l1.map(|port| L1Config::Spawn { port }).or(group
768                    .external_l1
769                    .map(|address| L1Config::External { address }))
770            }))
771            .with_auto_execute_l1(self.auto_execute_l1)
772            .with_base_token_config({
773                let ratio = self.base_token_ratio.unwrap_or(Ratio::ONE);
774                BaseTokenConfig {
775                    symbol: self.base_token_symbol.unwrap_or("ETH".to_string()),
776                    ratio: BaseTokenConversionRatio::new_simple(ConversionRatio {
777                        numerator: NonZeroU64::new(*ratio.numer())
778                            .expect("base token conversion ratio cannot have 0 as numerator"),
779                        denominator: NonZeroU64::new(*ratio.denom())
780                            .expect("base token conversion ratio cannot have 0 as denominator"),
781                    }),
782                }
783            });
784
785        if self.evm_interpreter && config.protocol_version() < ProtocolVersionId::Version27 {
786            return Err(zksync_error::anvil_zksync::env::InvalidArguments {
787                details: "EVM interpreter requires protocol version 27 or higher".into(),
788                arguments: debug_self_repr,
789            });
790        }
791
792        if let Some(Command::DebugTrace(args)) = &self.command {
793            let dt = DebugTraceConfig {
794                fork_url: args.fork_url.to_config().url.to_string(),
795                tx: args.tx,
796                only_top: args.only_top,
797            };
798            config = config.with_debug_trace(Some(dt));
799        }
800
801        Ok(config)
802    }
803
804    /// Converts the CLI arguments to a `TelemetryProps` to be used as event props.
805    pub fn into_telemetry_props(self) -> TelemetryProps {
806        TelemetryProps::new()
807            .insert("command", get_cli_command_telemetry_props(self.command))
808            .insert_with("offline", self.offline, |v| v.then_some(v))
809            .insert_with("health_check_endpoint", self.health_check_endpoint, |v| {
810                v.then_some(v)
811            })
812            .insert_with("config_out", self.config_out, |v| {
813                v.map(|_| TELEMETRY_SENSITIVE_VALUE)
814            })
815            .insert_with("port", self.port, |v| {
816                v.filter(|&p| p.to_string() != DEFAULT_PORT)
817                    .map(|_| TELEMETRY_SENSITIVE_VALUE)
818            })
819            .insert_with("host", &self.host, |v| {
820                v.first()
821                    .filter(|&h| h.to_string() != DEFAULT_HOST || self.host.len() != 1)
822                    .map(|_| TELEMETRY_SENSITIVE_VALUE)
823            })
824            .insert_with("chain_id", self.chain_id, |v| {
825                v.map(|_| TELEMETRY_SENSITIVE_VALUE)
826            })
827            .insert_with("show_node_config", self.show_node_config, |v| {
828                (!v.unwrap_or(false)).then_some(false)
829            })
830            .insert(
831                "show_storage_logs",
832                self.show_storage_logs.map(|v| v.to_string()),
833            )
834            .insert(
835                "show_vm_details",
836                self.show_vm_details.map(|v| v.to_string()),
837            )
838            .insert(
839                "show_gas_details",
840                self.show_gas_details.map(|v| v.to_string()),
841            )
842            .insert(
843                "l1_gas_price",
844                self.l1_gas_price.map(serde_json::Number::from),
845            )
846            .insert(
847                "l2_gas_price",
848                self.l2_gas_price.map(serde_json::Number::from),
849            )
850            .insert(
851                "l1_pubdata_price",
852                self.l1_pubdata_price.map(serde_json::Number::from),
853            )
854            .insert(
855                "price_scale_factor",
856                self.price_scale_factor.map(|v| {
857                    serde_json::Number::from_f64(v).unwrap_or(serde_json::Number::from(0))
858                }),
859            )
860            .insert(
861                "limit_scale_factor",
862                self.limit_scale_factor.map(|v| {
863                    serde_json::Number::from_f64(v as f64).unwrap_or(serde_json::Number::from(0))
864                }),
865            )
866            .insert_with("override_bytecodes_dir", self.override_bytecodes_dir, |v| {
867                v.map(|_| TELEMETRY_SENSITIVE_VALUE)
868            })
869            .insert(
870                "dev_system_contracts",
871                self.dev_system_contracts.map(|v| format!("{v:?}")),
872            )
873            .insert(
874                "protocol_version",
875                self.protocol_version.map(|v| v.to_string()),
876            )
877            .insert_with("evm_interpreter", self.evm_interpreter, |v| v.then_some(v))
878            .insert("log", self.log.map(|v| v.to_string()))
879            .insert_with("log_file_path", self.log_file_path, |v| {
880                v.map(|_| TELEMETRY_SENSITIVE_VALUE)
881            })
882            .insert("silent", self.silent)
883            .insert("cache", self.cache.map(|v| format!("{v:?}")))
884            .insert("reset_cache", self.reset_cache)
885            .insert_with("cache_dir", self.cache_dir, |v| {
886                v.map(|_| TELEMETRY_SENSITIVE_VALUE)
887            })
888            .insert_with("accounts", self.accounts, |v| {
889                (v.to_string() != DEFAULT_ACCOUNTS).then_some(serde_json::Number::from(v))
890            })
891            .insert_with("balance", self.balance, |v| {
892                (v.to_string() != DEFAULT_BALANCE).then_some(serde_json::Number::from(v))
893            })
894            .insert("timestamp", self.timestamp.map(serde_json::Number::from))
895            .insert_with("init", self.init, |v| v.map(|_| TELEMETRY_SENSITIVE_VALUE))
896            .insert_with("state", self.state, |v| {
897                v.map(|_| TELEMETRY_SENSITIVE_VALUE)
898            })
899            .insert(
900                "state_interval",
901                self.state_interval.map(serde_json::Number::from),
902            )
903            .insert_with("dump_state", self.dump_state, |v| {
904                v.map(|_| TELEMETRY_SENSITIVE_VALUE)
905            })
906            .insert_with(
907                "preserve_historical_states",
908                self.preserve_historical_states,
909                |v| v.then_some(v),
910            )
911            .insert_with("load_state", self.load_state, |v| {
912                v.map(|_| TELEMETRY_SENSITIVE_VALUE)
913            })
914            .insert_with("mnemonic", self.mnemonic, |v| {
915                v.map(|_| TELEMETRY_SENSITIVE_VALUE)
916            })
917            .insert_with("mnemonic_random", self.mnemonic_random, |v| {
918                v.map(|_| TELEMETRY_SENSITIVE_VALUE)
919            })
920            .insert_with("mnemonic_seed", self.mnemonic_seed, |v| {
921                v.map(|_| TELEMETRY_SENSITIVE_VALUE)
922            })
923            .insert_with("derivation_path", self.derivation_path, |v| {
924                v.map(|_| TELEMETRY_SENSITIVE_VALUE)
925            })
926            .insert_with("auto_impersonate", self.auto_impersonate, |v| {
927                v.then_some(v)
928            })
929            .insert("block_time", self.block_time.map(|v| format!("{v:?}")))
930            .insert_with("no_mining", self.no_mining, |v| v.then_some(v))
931            .insert_with("allow_origin", self.allow_origin, |v| {
932                (v != DEFAULT_ALLOW_ORIGIN).then_some(TELEMETRY_SENSITIVE_VALUE)
933            })
934            .insert_with("no_cors", self.no_cors, |v| v.then_some(v))
935            .insert_with("order", self.order, |v| {
936                (v.to_string() != DEFAULT_TX_ORDER).then_some(v.to_string())
937            })
938            .take()
939    }
940
941    fn account_generator(&self) -> AccountGenerator {
942        let mut generator = AccountGenerator::new(self.accounts as usize)
943            .phrase(DEFAULT_MNEMONIC)
944            .chain_id(self.chain_id.unwrap_or(TEST_NODE_NETWORK_ID));
945        if let Some(ref mnemonic) = self.mnemonic {
946            generator = generator.phrase(mnemonic);
947        } else if let Some(count) = self.mnemonic_random {
948            let mut rng = rand::thread_rng();
949            let mnemonic = match Mnemonic::<English>::new_with_count(&mut rng, count) {
950                Ok(mnemonic) => mnemonic.to_phrase(),
951                Err(_) => DEFAULT_MNEMONIC.to_string(),
952            };
953            generator = generator.phrase(mnemonic);
954        } else if let Some(seed) = self.mnemonic_seed {
955            let mut seed = StdRng::seed_from_u64(seed);
956            let mnemonic = Mnemonic::<English>::new(&mut seed).to_phrase();
957            generator = generator.phrase(mnemonic);
958        }
959        if let Some(ref derivation) = self.derivation_path {
960            generator = generator.derivation_path(derivation);
961        }
962        generator
963    }
964}
965
966fn duration_from_secs_f64(s: &str) -> Result<Duration, String> {
967    let s = s.parse::<f64>().map_err(|e| e.to_string())?;
968    if s == 0.0 {
969        return Err("Duration must be greater than 0".to_string());
970    }
971    Duration::try_from_secs_f64(s).map_err(|e| e.to_string())
972}
973
974fn protocol_version_from_str(s: &str) -> anyhow::Result<ProtocolVersionId> {
975    let version = s.parse::<u16>()?;
976    Ok(ProtocolVersionId::try_from(version)?)
977}
978
979// Implementation adapted from: https://github.com/foundry-rs/foundry/blob/206dab285437bd6889463ab006b6a5fb984079d8/crates/anvil/src/cmd.rs#L606
980/// Helper type to periodically dump the state of the chain to disk
981pub struct PeriodicStateDumper {
982    in_progress_dump: Option<Pin<Box<dyn Future<Output = ()> + Send + Sync + 'static>>>,
983    node: InMemoryNode,
984    dump_state: Option<PathBuf>,
985    preserve_historical_states: bool,
986    interval: Interval,
987}
988
989impl PeriodicStateDumper {
990    pub fn new(
991        node: InMemoryNode,
992        dump_state: Option<PathBuf>,
993        interval: Duration,
994        preserve_historical_states: bool,
995    ) -> Self {
996        let dump_state = dump_state.map(|mut dump_state| {
997            if dump_state.is_dir() {
998                dump_state = dump_state.join("state.json");
999            }
1000            dump_state
1001        });
1002
1003        let interval = tokio::time::interval_at(Instant::now() + interval, interval);
1004        Self {
1005            in_progress_dump: None,
1006            node,
1007            dump_state,
1008            preserve_historical_states,
1009            interval,
1010        }
1011    }
1012
1013    #[allow(dead_code)] // TODO: Remove this once the method is used
1014    pub async fn dump(&self) {
1015        if let Some(state) = self.dump_state.clone() {
1016            Self::dump_state(self.node.clone(), state, self.preserve_historical_states).await
1017        }
1018    }
1019
1020    /// Infallible state dump
1021    async fn dump_state(node: InMemoryNode, dump_path: PathBuf, preserve_historical_states: bool) {
1022        tracing::trace!(path=?dump_path, "Dumping state");
1023
1024        // Spawn a blocking task for state dumping
1025        let state_bytes = match node.dump_state(preserve_historical_states).await {
1026            Ok(bytes) => bytes,
1027            Err(err) => {
1028                sh_err!("Failed to dump state: {:?}", err);
1029                return;
1030            }
1031        };
1032
1033        let mut decoder = GzDecoder::new(&state_bytes.0[..]);
1034        let mut json_str = String::new();
1035        if let Err(err) = decoder.read_to_string(&mut json_str) {
1036            sh_err!("Failed to decompress state bytes: {}", err);
1037            return;
1038        }
1039
1040        let state = match serde_json::from_str::<VersionedState>(&json_str) {
1041            Ok(state) => state,
1042            Err(err) => {
1043                sh_err!("Failed to parse state JSON: {}", err);
1044                return;
1045            }
1046        };
1047
1048        if let Err(err) = write_json_file(&dump_path, &state) {
1049            sh_err!("Failed to write state to file: {}", err);
1050        } else {
1051            tracing::trace!(path = ?dump_path, "Dumped state successfully");
1052        }
1053    }
1054}
1055
1056// An endless future that periodically dumps the state to disk if configured.
1057// Implementation adapted from: https://github.com/foundry-rs/foundry/blob/206dab285437bd6889463ab006b6a5fb984079d8/crates/anvil/src/cmd.rs#L658
1058impl Future for PeriodicStateDumper {
1059    type Output = anyhow::Result<()>;
1060
1061    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
1062        let this = self.get_mut();
1063        if this.dump_state.is_none() {
1064            return Poll::Pending;
1065        }
1066
1067        loop {
1068            if let Some(mut flush) = this.in_progress_dump.take() {
1069                match flush.poll_unpin(cx) {
1070                    Poll::Ready(_) => {
1071                        this.interval.reset();
1072                    }
1073                    Poll::Pending => {
1074                        this.in_progress_dump = Some(flush);
1075                        return Poll::Pending;
1076                    }
1077                }
1078            }
1079
1080            if this.interval.poll_tick(cx).is_ready() {
1081                let api = this.node.clone();
1082                let path = this.dump_state.clone().expect("exists; see above");
1083                this.in_progress_dump = Some(Box::pin(Self::dump_state(
1084                    api,
1085                    path,
1086                    this.preserve_historical_states,
1087                )));
1088            } else {
1089                break;
1090            }
1091        }
1092
1093        Poll::Pending
1094    }
1095}
1096
1097#[cfg(test)]
1098mod tests {
1099    use crate::cli::PeriodicStateDumper;
1100
1101    use super::Cli;
1102    use anvil_zksync_core::node::InMemoryNode;
1103    use clap::Parser;
1104    use serde_json::{Value, json};
1105    use std::{
1106        env,
1107        net::{IpAddr, Ipv4Addr},
1108    };
1109    use zksync_types::{H160, U256};
1110
1111    #[test]
1112    fn can_parse_host() {
1113        // Test adapted from https://github.com/foundry-rs/foundry/blob/398ef4a3d55d8dd769ce86cada5ec845e805188b/crates/anvil/src/cmd.rs#L895
1114        let args = Cli::parse_from(["anvil-zksync"]);
1115        assert_eq!(args.host, vec![IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))]);
1116
1117        let args = Cli::parse_from([
1118            "anvil-zksync",
1119            "--host",
1120            "::1",
1121            "--host",
1122            "1.1.1.1",
1123            "--host",
1124            "2.2.2.2",
1125        ]);
1126        assert_eq!(
1127            args.host,
1128            ["::1", "1.1.1.1", "2.2.2.2"]
1129                .map(|ip| ip.parse::<IpAddr>().unwrap())
1130                .to_vec()
1131        );
1132
1133        let args = Cli::parse_from(["anvil-zksync", "--host", "::1,1.1.1.1,2.2.2.2"]);
1134        assert_eq!(
1135            args.host,
1136            ["::1", "1.1.1.1", "2.2.2.2"]
1137                .map(|ip| ip.parse::<IpAddr>().unwrap())
1138                .to_vec()
1139        );
1140
1141        unsafe {
1142            env::set_var("ANVIL_ZKSYNC_IP_ADDR", "1.1.1.1");
1143        }
1144        let args = Cli::parse_from(["anvil-zksync"]);
1145        assert_eq!(args.host, vec!["1.1.1.1".parse::<IpAddr>().unwrap()]);
1146
1147        unsafe {
1148            env::set_var("ANVIL_ZKSYNC_IP_ADDR", "::1,1.1.1.1,2.2.2.2");
1149        }
1150        let args = Cli::parse_from(["anvil-zksync"]);
1151        assert_eq!(
1152            args.host,
1153            ["::1", "1.1.1.1", "2.2.2.2"]
1154                .map(|ip| ip.parse::<IpAddr>().unwrap())
1155                .to_vec()
1156        );
1157    }
1158
1159    #[tokio::test]
1160    async fn test_dump_state() -> anyhow::Result<()> {
1161        let temp_dir = tempfile::Builder::new()
1162            .prefix("state-test")
1163            .tempdir()
1164            .expect("failed creating temporary dir");
1165        let dump_path = temp_dir.path().join("state.json");
1166
1167        let config = anvil_zksync_config::TestNodeConfig {
1168            dump_state: Some(dump_path.clone()),
1169            state_interval: Some(1),
1170            preserve_historical_states: true,
1171            ..Default::default()
1172        };
1173
1174        let node = InMemoryNode::test_config(None, config.clone());
1175
1176        let mut state_dumper = PeriodicStateDumper::new(
1177            node.clone(),
1178            config.dump_state.clone(),
1179            std::time::Duration::from_secs(1),
1180            config.preserve_historical_states,
1181        );
1182
1183        // Spawn the state dumper as a task:
1184        let dumper_handle = tokio::spawn(async move {
1185            tokio::select! {
1186                _ = &mut state_dumper => {}
1187            }
1188            state_dumper.dump().await;
1189        });
1190        tokio::time::sleep(std::time::Duration::from_secs(2)).await;
1191
1192        dumper_handle.abort();
1193        let _ = dumper_handle.await;
1194
1195        let dumped_data =
1196            std::fs::read_to_string(&dump_path).expect("Expected state file to be created");
1197        let _: Value =
1198            serde_json::from_str(&dumped_data).expect("Failed to parse dumped state as JSON");
1199
1200        Ok(())
1201    }
1202
1203    #[tokio::test]
1204    async fn test_load_state() -> anyhow::Result<()> {
1205        let temp_dir = tempfile::Builder::new()
1206            .prefix("state-load-test")
1207            .tempdir()
1208            .expect("failed creating temporary dir");
1209        let state_path = temp_dir.path().join("state.json");
1210
1211        let config = anvil_zksync_config::TestNodeConfig {
1212            dump_state: Some(state_path.clone()),
1213            state_interval: Some(1),
1214            preserve_historical_states: true,
1215            ..Default::default()
1216        };
1217
1218        let node = InMemoryNode::test_config(None, config.clone());
1219        let test_address = H160::from_low_u64_be(12345);
1220        node.set_rich_account(test_address, U256::from(1000000u64))
1221            .await;
1222
1223        let mut state_dumper = PeriodicStateDumper::new(
1224            node.clone(),
1225            config.dump_state.clone(),
1226            std::time::Duration::from_secs(1),
1227            config.preserve_historical_states,
1228        );
1229
1230        let dumper_handle = tokio::spawn(async move {
1231            tokio::select! {
1232                _ = &mut state_dumper => {}
1233            }
1234            state_dumper.dump().await;
1235        });
1236
1237        tokio::time::sleep(std::time::Duration::from_secs(2)).await;
1238
1239        dumper_handle.abort();
1240        let _ = dumper_handle.await;
1241
1242        // assert the state json file was created
1243        std::fs::read_to_string(&state_path).expect("Expected state file to be created");
1244
1245        let new_config = anvil_zksync_config::TestNodeConfig::default();
1246        let new_node = InMemoryNode::test_config(None, new_config.clone());
1247
1248        new_node
1249            .load_state(zksync_types::web3::Bytes(std::fs::read(&state_path)?))
1250            .await?;
1251
1252        // assert the balance from the loaded state is correctly applied
1253        let balance = new_node.get_balance_impl(test_address, None).await?;
1254        assert_eq!(balance, U256::from(1000000u64));
1255
1256        Ok(())
1257    }
1258
1259    #[tokio::test]
1260    async fn test_cli_telemetry_data_skips_missing_args() -> anyhow::Result<()> {
1261        let args = Cli::parse_from(["anvil-zksync"]).into_telemetry_props();
1262        let json = args.to_inner();
1263        let expected_json: serde_json::Value = json!({});
1264        assert_eq!(json, expected_json);
1265        Ok(())
1266    }
1267
1268    #[tokio::test]
1269    async fn test_cli_telemetry_data_hides_sensitive_data() -> anyhow::Result<()> {
1270        let args = Cli::parse_from([
1271            "anvil-zksync",
1272            "--offline",
1273            "--host",
1274            "100.100.100.100",
1275            "--port",
1276            "3333",
1277            "--chain-id",
1278            "123",
1279            "--config-out",
1280            "/some/path",
1281            "fork",
1282            "--fork-url",
1283            "era",
1284        ])
1285        .into_telemetry_props();
1286        let json = args.to_inner();
1287        let expected_json: serde_json::Value = json!({
1288            "command": {
1289                "args": {
1290                    "fork_url": "Builtin(Era)"
1291                },
1292                "name": "fork"
1293            },
1294            "offline": true,
1295            "config_out": "***",
1296            "port": "***",
1297            "host": "***",
1298            "chain_id": "***"
1299        });
1300        assert_eq!(json, expected_json);
1301        Ok(())
1302    }
1303}