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