anvil_zksync/
cli.rs

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