Repository: anza-xyz/agave
Pull request #5196: use clap v4 for vortexor
Author: login: lijunwangs
Created: 2025-03-07T21:24:30Z | Merged: 2025-03-13T18:18:47Z

#### Problem

Use clap v4 for vortexor to take advantage of some of improvements in clap lib.

#### Summary of Changes

Key changes:

Use derive feature to define the CLI arguments declaratively.


Fixes #
<!-- OPTIONAL: Feature Gate Issue: # -->
<!-- Don't forget to add the "feature-gate" label -->



Diff:
diff --git a/Cargo.lock b/Cargo.lock
index e25823229935a5..585a0c5e7ea51a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -498,12 +498,55 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "anstream"
+version = "0.6.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
 [[package]]
 name = "anstyle"
 version = "1.0.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
 
+[[package]]
+name = "anstyle-parse"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
+dependencies = [
+ "anstyle",
+ "windows-sys 0.59.0",
+]
+
 [[package]]
 name = "anyhow"
 version = "1.0.96"
@@ -1526,7 +1569,7 @@ checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5"
 dependencies = [
  "atty",
  "bitflags 1.3.2",
- "clap_derive",
+ "clap_derive 3.2.18",
  "clap_lex 0.2.4",
  "indexmap 1.9.3",
  "once_cell",
@@ -1537,21 +1580,24 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.3.21"
+version = "4.5.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c27cdf28c0f604ba3f512b0c9a409f8de8513e4816705deb0498b627e7c3a3fd"
+checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767"
 dependencies = [
  "clap_builder",
+ "clap_derive 4.5.28",
 ]
 
 [[package]]
 name = "clap_builder"
-version = "4.3.21"
+version = "4.5.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08a9f1ab5e9f01a9b81f202e8562eb9a10de70abf9eaeac1be465c28b75aa4aa"
+checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863"
 dependencies = [
+ "anstream",
  "anstyle",
- "clap_lex 0.5.0",
+ "clap_lex 0.7.4",
+ "strsim 0.11.1",
 ]
 
 [[package]]
@@ -1560,13 +1606,25 @@ version = "3.2.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65"
 dependencies = [
- "heck",
+ "heck 0.4.0",
  "proc-macro-error",
  "proc-macro2",
  "quote",
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "clap_derive"
+version = "4.5.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.99",
+]
+
 [[package]]
 name = "clap_lex"
 version = "0.2.4"
@@ -1578,9 +1636,15 @@ dependencies = [
 
 [[package]]
 name = "clap_lex"
-version = "0.5.0"
+version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b"
+checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
 
 [[package]]
 name = "combine"
@@ -1743,7 +1807,7 @@ dependencies = [
  "anes",
  "cast 0.3.0",
  "ciborium",
- "clap 4.3.21",
+ "clap 4.5.31",
  "criterion-plot",
  "is-terminal",
  "itertools 0.10.5",
@@ -2920,6 +2984,12 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
 
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
 [[package]]
 name = "hermit-abi"
 version = "0.1.19"
@@ -3481,6 +3551,12 @@ dependencies = [
  "windows-sys 0.48.0",
 ]
 
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+
 [[package]]
 name = "itertools"
 version = "0.10.5"
@@ -4834,7 +4910,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "119533552c9a7ffacc21e099c24a0ac8bb19c2a2a3f363de84cd9b844feab270"
 dependencies = [
  "bytes",
- "heck",
+ "heck 0.4.0",
  "itertools 0.10.5",
  "lazy_static",
  "log",
@@ -10796,7 +10872,7 @@ dependencies = [
  "assert_matches",
  "async-channel",
  "bytes",
- "clap 2.33.3",
+ "clap 4.5.31",
  "crossbeam-channel",
  "dashmap",
  "futures 0.3.31",
@@ -11531,6 +11607,12 @@ version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
 
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
 [[package]]
 name = "strum"
 version = "0.24.1"
@@ -11546,7 +11628,7 @@ version = "0.24.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
 dependencies = [
- "heck",
+ "heck 0.4.0",
  "proc-macro2",
  "quote",
  "rustversion",
@@ -12526,6 +12608,12 @@ version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
 
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
 [[package]]
 name = "valuable"
 version = "0.1.0"
diff --git a/vortexor/Cargo.toml b/vortexor/Cargo.toml
index d4ed73a30e388b..8c546d1a006ac9 100644
--- a/vortexor/Cargo.toml
+++ b/vortexor/Cargo.toml
@@ -15,7 +15,7 @@ edition = { workspace = true }
 agave-banking-stage-ingress-types = { workspace = true }
 async-channel = { workspace = true }
 bytes = { workspace = true }
-clap = { workspace = true }
+clap = { version = "4.5.31", features = ["cargo", "derive", "error-context"] }
 crossbeam-channel = { workspace = true }
 dashmap = { workspace = true }
 futures  = { workspace = true }
diff --git a/vortexor/src/cli.rs b/vortexor/src/cli.rs
index 9603df51c306de..5bfd1aeb5ed7a3 100644
--- a/vortexor/src/cli.rs
+++ b/vortexor/src/cli.rs
@@ -1,52 +1,22 @@
 use {
-    clap::{crate_description, crate_name, App, AppSettings, Arg},
-    solana_clap_utils::input_validators::{is_keypair_or_ask_keyword, is_parsable},
+    clap::{crate_description, crate_name, ArgAction, ColorChoice, Parser},
     solana_net_utils::{MINIMUM_VALIDATOR_PORT_RANGE_WIDTH, VALIDATOR_PORT_RANGE},
-    solana_sdk::quic::QUIC_PORT_OFFSET,
+    solana_sdk::{net::DEFAULT_TPU_COALESCE, quic::QUIC_PORT_OFFSET},
     solana_streamer::quic::{
         DEFAULT_MAX_CONNECTIONS_PER_IPADDR_PER_MINUTE, DEFAULT_MAX_STAKED_CONNECTIONS,
         DEFAULT_MAX_STREAMS_PER_MS, DEFAULT_MAX_UNSTAKED_CONNECTIONS,
     },
+    std::{
+        net::{IpAddr, SocketAddr},
+        path::PathBuf,
+    },
 };
 
 pub const DEFAULT_MAX_QUIC_CONNECTIONS_PER_PEER: usize = 8;
 pub const DEFAULT_NUM_QUIC_ENDPOINTS: usize = 8;
 
-pub struct DefaultArgs {
-    pub bind_address: String,
-    pub dynamic_port_range: String,
-    pub max_connections_per_peer: String,
-    pub max_tpu_staked_connections: String,
-    pub max_tpu_unstaked_connections: String,
-    pub max_fwd_staked_connections: String,
-    pub max_fwd_unstaked_connections: String,
-    pub max_streams_per_ms: String,
-    pub max_connections_per_ipaddr_per_min: String,
-    pub num_quic_endpoints: String,
-}
-
-impl Default for DefaultArgs {
-    fn default() -> Self {
-        Self {
-            bind_address: "0.0.0.0".to_string(),
-            dynamic_port_range: format!("{}-{}", VALIDATOR_PORT_RANGE.0, VALIDATOR_PORT_RANGE.1),
-            max_connections_per_peer: DEFAULT_MAX_QUIC_CONNECTIONS_PER_PEER.to_string(),
-            max_tpu_staked_connections: DEFAULT_MAX_STAKED_CONNECTIONS.to_string(),
-            max_tpu_unstaked_connections: DEFAULT_MAX_UNSTAKED_CONNECTIONS.to_string(),
-            max_fwd_staked_connections: DEFAULT_MAX_STAKED_CONNECTIONS
-                .saturating_add(DEFAULT_MAX_UNSTAKED_CONNECTIONS)
-                .to_string(),
-            max_fwd_unstaked_connections: 0.to_string(),
-            max_streams_per_ms: DEFAULT_MAX_STREAMS_PER_MS.to_string(),
-            max_connections_per_ipaddr_per_min: DEFAULT_MAX_CONNECTIONS_PER_IPADDR_PER_MINUTE
-                .to_string(),
-            num_quic_endpoints: DEFAULT_NUM_QUIC_ENDPOINTS.to_string(),
-        }
-    }
-}
-
-fn port_range_validator(port_range: String) -> Result<(), String> {
-    if let Some((start, end)) = solana_net_utils::parse_port_range(&port_range) {
+fn parse_port_range(port_range: &str) -> Result<(u16, u16), String> {
+    if let Some((start, end)) = solana_net_utils::parse_port_range(port_range) {
         if end.saturating_sub(start) < MINIMUM_VALIDATOR_PORT_RANGE_WIDTH {
             Err(format!(
                 "Port range is too small.  Try --dynamic-port-range {}-{}",
@@ -56,139 +26,91 @@ fn port_range_validator(port_range: String) -> Result<(), String> {
         } else if end.checked_add(QUIC_PORT_OFFSET).is_none() {
             Err("Invalid dynamic_port_range.".to_string())
         } else {
-            Ok(())
+            Ok((start, end))
         }
     } else {
         Err("Invalid port range".to_string())
     }
 }
 
-pub fn app<'a>(version: &'a str, default_args: &'a DefaultArgs) -> App<'a, 'a> {
-    App::new(crate_name!())
-        .about(crate_description!())
-        .version(version)
-        .global_setting(AppSettings::ColoredHelp)
-        .global_setting(AppSettings::InferSubcommands)
-        .global_setting(AppSettings::UnifiedHelpMessage)
-        .global_setting(AppSettings::VersionlessSubcommands)
-        .arg(
-            Arg::with_name("identity")
-                .short("i")
-                .long("identity")
-                .value_name("KEYPAIR")
-                .takes_value(true)
-                .validator(is_keypair_or_ask_keyword)
-                .help("Vortexor identity keypair"),
-        )
-        .arg(
-            Arg::with_name("bind_address")
-                .long("bind-address")
-                .value_name("HOST")
-                .takes_value(true)
-                .validator(solana_net_utils::is_host)
-                .default_value(&default_args.bind_address)
-                .help("IP address to bind the validator ports"),
-        )
-        .arg(
-            Arg::with_name("dynamic_port_range")
-                .long("dynamic-port-range")
-                .value_name("MIN_PORT-MAX_PORT")
-                .takes_value(true)
-                .default_value(&default_args.dynamic_port_range)
-                .validator(port_range_validator)
-                .help("Range to use for dynamically assigned ports"),
-        )
-        .arg(
-            Arg::with_name("max_connections_per_peer")
-                .long("max-connections-per-peer")
-                .takes_value(true)
-                .default_value(&default_args.max_connections_per_peer)
-                .validator(is_parsable::<u32>)
-                .help("Controls the max concurrent connections per IpAddr."),
-        )
-        .arg(
-            Arg::with_name("max_tpu_staked_connections")
-                .long("max-tpu-staked-connections")
-                .takes_value(true)
-                .default_value(&default_args.max_tpu_staked_connections)
-                .validator(is_parsable::<u32>)
-                .help("Controls the max concurrent connections for TPU from staked nodes."),
-        )
-        .arg(
-            Arg::with_name("max_tpu_unstaked_connections")
-                .long("max-tpu-unstaked-connections")
-                .takes_value(true)
-                .default_value(&default_args.max_tpu_unstaked_connections)
-                .validator(is_parsable::<u32>)
-                .help("Controls the max concurrent connections fort TPU from unstaked nodes."),
-        )
-        .arg(
-            Arg::with_name("max_fwd_staked_connections")
-                .long("max-fwd-staked-connections")
-                .takes_value(true)
-                .default_value(&default_args.max_fwd_staked_connections)
-                .validator(is_parsable::<u32>)
-                .help("Controls the max concurrent connections for TPU-forward from staked nodes."),
-        )
-        .arg(
-            Arg::with_name("max_fwd_unstaked_connections")
-                .long("max-fwd-unstaked-connections")
-                .takes_value(true)
-                .default_value(&default_args.max_fwd_unstaked_connections)
-                .validator(is_parsable::<u32>)
-                .help("Controls the max concurrent connections for TPU-forward from unstaked nodes."),
-        )
-        .arg(
-            Arg::with_name("max_connections_per_ipaddr_per_minute")
-                .long("max-connections-per-ipaddr-per-minute")
-                .takes_value(true)
-                .default_value(&default_args.max_connections_per_ipaddr_per_min)
-                .validator(is_parsable::<u32>)
-                .help("Controls the rate of the clients connections per IpAddr per minute."),
-        )
-        .arg(
-            Arg::with_name("num_quic_endpoints")
-                .long("num-quic-endpoints")
-                .takes_value(true)
-                .default_value(&default_args.num_quic_endpoints)
-                .validator(is_parsable::<usize>)
-                .help("The number of QUIC endpoints used for TPU and TPU-Forward. It can be increased to \
-                       increase network ingest throughput, at the expense of higher CPU and general \
-                       validator load."),
-        )
-        .arg(
-            Arg::with_name("max_streams_per_ms")
-                .long("max-streams-per-ms")
-                .takes_value(true)
-                .default_value(&default_args.max_streams_per_ms)
-                .validator(is_parsable::<usize>)
-                .help("Max streams per second for a streamer."),
-        )
-        .arg(
-            Arg::with_name("tpu_coalesce_ms")
-                .long("tpu-coalesce-ms")
-                .value_name("MILLISECS")
-                .takes_value(true)
-                .validator(is_parsable::<u64>)
-                .help("Milliseconds to wait in the TPU receiver for packet coalescing."),
-        )
-        .arg(
-            Arg::with_name("logfile")
-                .long("log")
-                .value_name("FILE")
-                .takes_value(true)
-                .help(
-                    "Redirect logging to the specified file, '-' for standard error. Sending the \
-                     SIGUSR1 signal to the vortexor process will cause it to re-open the log file.",
-                ),
-        )
-        .arg(
-            Arg::with_name("destination")
-                .long("destination")
-                .value_name("HOST:PORT")
-                .takes_value(true)
-                .multiple(true)
-                .validator(solana_net_utils::is_host_port)
-                .help("The destination validator address to which the vortexor will forward transactions."),
-        )
+fn get_version() -> &'static str {
+    let version = solana_version::version!();
+    let version_static: &'static str = Box::leak(version.to_string().into_boxed_str());
+    version_static
+}
+
+fn get_default_port_range() -> &'static str {
+    let range = format!("{}-{}", VALIDATOR_PORT_RANGE.0, VALIDATOR_PORT_RANGE.1);
+    let range: &'static str = Box::leak(range.into_boxed_str());
+    range
+}
+
+fn get_default_tpu_coalesce_ms() -> &'static str {
+    let coalesce = DEFAULT_TPU_COALESCE.as_millis().to_string();
+    let coalesce: &'static str = Box::leak(coalesce.into_boxed_str());
+    coalesce
+}
+
+#[derive(Parser)]
+#[command(name=crate_name!(),version=get_version(), about=crate_description!(),
+    long_about = None, color=ColorChoice::Auto)]
+pub struct Cli {
+    /// Vortexor identity keypair
+    #[arg(long, value_name = "KEYPAIR")]
+    pub identity: PathBuf,
+
+    /// IP address to bind the vortexor ports
+    #[arg(long, value_parser = solana_net_utils::parse_host, default_value = "0.0.0.0", value_name = "HOST")]
+    pub bind_address: IpAddr,
+
+    /// The destination validator address to which the vortexor will forward transactions.
+    #[arg(long, value_parser = solana_net_utils::parse_host_port, value_name = "HOST:PORT", action = ArgAction::Append)]
+    pub destination: Vec<SocketAddr>,
+
+    /// Range to use for dynamically assigned ports
+    #[arg(long, value_parser = parse_port_range, value_name = "MIN_PORT-MAX_PORT", default_value = get_default_port_range())]
+    pub dynamic_port_range: (u16, u16),
+
+    /// Controls the max concurrent connections per IpAddr.
+    #[arg(long, default_value_t = DEFAULT_MAX_QUIC_CONNECTIONS_PER_PEER)]
+    pub max_connections_per_peer: usize,
+
+    /// Controls the max concurrent connections for TPU from staked nodes.
+    #[arg(long, default_value_t = DEFAULT_MAX_STAKED_CONNECTIONS)]
+    pub max_tpu_staked_connections: usize,
+
+    /// Controls the max concurrent connections fort TPU from unstaked nodes.
+    #[arg(long, default_value_t = DEFAULT_MAX_UNSTAKED_CONNECTIONS)]
+    pub max_tpu_unstaked_connections: usize,
+
+    /// Controls the max concurrent connections for TPU-forward from staked nodes.
+    #[arg(long, default_value_t = DEFAULT_MAX_STAKED_CONNECTIONS)]
+    pub max_fwd_staked_connections: usize,
+
+    /// Controls the max concurrent connections for TPU-forward from unstaked nodes.
+    #[arg(long, default_value_t = 0)]
+    pub max_fwd_unstaked_connections: usize,
+
+    /// Controls the rate of the clients connections per IpAddr per minute.
+    #[arg(long, default_value_t = DEFAULT_MAX_CONNECTIONS_PER_IPADDR_PER_MINUTE)]
+    pub max_connections_per_ipaddr_per_minute: u64,
+
+    /// The number of QUIC endpoints used for TPU and TPU-Forward. It can be increased to
+    /// increase network ingest throughput, at the expense of higher CPU and general
+    /// validator load.
+    #[arg(long, default_value_t = DEFAULT_NUM_QUIC_ENDPOINTS)]
+    pub num_quic_endpoints: usize,
+
+    /// Max streams per second for a streamer.
+    #[arg(long, default_value_t = DEFAULT_MAX_STREAMS_PER_MS)]
+    pub max_streams_per_ms: u64,
+
+    /// Milliseconds to wait in the TPU receiver for packet coalescing.
+    #[arg(long, default_value = get_default_tpu_coalesce_ms())]
+    pub tpu_coalesce_ms: u64,
+
+    /// Redirect logging to the specified file, '-' for standard error. Sending the
+    /// SIGUSR1 signal to the vortexor process will cause it to re-open the log file.
+    #[arg(long="log", value_name = "FILE", value_parser = clap::value_parser!(String))]
+    pub logfile: Option<String>,
 }
diff --git a/vortexor/src/main.rs b/vortexor/src/main.rs
index 824fe6af311612..087d1bb2898163 100644
--- a/vortexor/src/main.rs
+++ b/vortexor/src/main.rs
@@ -1,15 +1,14 @@
 use {
-    clap::{crate_name, value_t, value_t_or_exit, values_t},
+    clap::{crate_name, Parser},
     crossbeam_channel::bounded,
     log::*,
-    solana_clap_utils::input_parsers::keypair_of,
     solana_core::banking_trace::BankingTracer,
     solana_logger::redirect_stderr_to_file,
     solana_net_utils::{bind_in_range_with_config, SocketConfig},
-    solana_sdk::{net::DEFAULT_TPU_COALESCE, signer::Signer},
+    solana_sdk::{signature::read_keypair_file, signer::Signer},
     solana_streamer::streamer::StakedNodes,
     solana_vortexor::{
-        cli::{app, DefaultArgs},
+        cli::Cli,
         sender::{
             PacketBatchSender, DEFAULT_BATCH_SIZE, DEFAULT_RECV_TIMEOUT,
             DEFAULT_SENDER_THREADS_COUNT,
@@ -17,8 +16,8 @@ use {
         vortexor::Vortexor,
     },
     std::{
-        collections::HashSet,
         env,
+        net::IpAddr,
         sync::{atomic::AtomicBool, Arc, RwLock},
         time::Duration,
     },
@@ -29,23 +28,21 @@ const DEFAULT_CHANNEL_SIZE: usize = 100_000;
 pub fn main() {
     solana_logger::setup();
 
-    let default_args = DefaultArgs::default();
+    let args = Cli::parse();
     let solana_version = solana_version::version!();
-    let cli_app = app(solana_version, &default_args);
-    let matches = cli_app.get_matches();
+    let identity = args.identity;
 
-    let identity_keypair = keypair_of(&matches, "identity").unwrap_or_else(|| {
-        clap::Error::with_description(
-            "The --identity <KEYPAIR> argument is required",
-            clap::ErrorKind::ArgumentNotFound,
+    let identity_keypair = read_keypair_file(identity).unwrap_or_else(|error| {
+        clap::Error::raw(
+            clap::error::ErrorKind::InvalidValue,
+            format!("The --identity <KEYPAIR> argument is required, error: {error}"),
         )
         .exit();
     });
 
     let logfile = {
-        let logfile = matches
-            .value_of("logfile")
-            .map(|s| s.into())
+        let logfile = args
+            .logfile
             .unwrap_or_else(|| format!("solana-vortexor-{}.log", identity_keypair.pubkey()));
 
         if logfile == "-" {
@@ -64,36 +61,27 @@ pub fn main() {
         std::env::args_os()
     );
 
-    let bind_address = solana_net_utils::parse_host(matches.value_of("bind_address").unwrap())
-        .expect("invalid bind_address");
-    let max_connections_per_peer = value_t_or_exit!(matches, "max_connections_per_peer", u64);
-    let max_tpu_staked_connections = value_t_or_exit!(matches, "max_tpu_staked_connections", u64);
-    let max_fwd_staked_connections = value_t_or_exit!(matches, "max_fwd_staked_connections", u64);
-    let max_fwd_unstaked_connections =
-        value_t_or_exit!(matches, "max_fwd_unstaked_connections", u64);
-
-    let max_tpu_unstaked_connections =
-        value_t_or_exit!(matches, "max_tpu_unstaked_connections", u64);
-
-    let max_connections_per_ipaddr_per_min =
-        value_t_or_exit!(matches, "max_connections_per_ipaddr_per_minute", u64);
-    let num_quic_endpoints = value_t_or_exit!(matches, "num_quic_endpoints", u64);
-    let tpu_coalesce = value_t!(matches, "tpu_coalesce_ms", u64)
-        .map(Duration::from_millis)
-        .unwrap_or(DEFAULT_TPU_COALESCE);
-
-    let dynamic_port_range =
-        solana_net_utils::parse_port_range(matches.value_of("dynamic_port_range").unwrap())
-            .expect("invalid dynamic_port_range");
-
-    let max_streams_per_ms = value_t_or_exit!(matches, "max_streams_per_ms", u64);
+    let bind_address: &IpAddr = &args.bind_address;
+    let max_connections_per_peer = args.max_connections_per_peer;
+    let max_tpu_staked_connections = args.max_tpu_staked_connections;
+    let max_fwd_staked_connections = args.max_fwd_staked_connections;
+    let max_fwd_unstaked_connections = args.max_fwd_unstaked_connections;
+
+    let max_tpu_unstaked_connections = args.max_tpu_unstaked_connections;
+
+    let max_connections_per_ipaddr_per_min = args.max_connections_per_ipaddr_per_minute;
+    let num_quic_endpoints = args.num_quic_endpoints;
+    let tpu_coalesce = Duration::from_millis(args.tpu_coalesce_ms);
+    let dynamic_port_range = args.dynamic_port_range;
+
+    let max_streams_per_ms = args.max_streams_per_ms;
     let exit = Arc::new(AtomicBool::new(false));
     // To be linked with the Tpu sigverify and forwarder service
     let (tpu_sender, tpu_receiver) = bounded(DEFAULT_CHANNEL_SIZE);
     let (tpu_fwd_sender, _tpu_fwd_receiver) = bounded(DEFAULT_CHANNEL_SIZE);
 
     let tpu_sockets =
-        Vortexor::create_tpu_sockets(bind_address, dynamic_port_range, num_quic_endpoints);
+        Vortexor::create_tpu_sockets(*bind_address, dynamic_port_range, num_quic_endpoints);
 
     let (banking_tracer, _) = BankingTracer::new(
         None, // Not interesed in banking tracing
@@ -103,22 +91,12 @@ pub fn main() {
     let config = SocketConfig::default().reuseport(false);
 
     let sender_socket =
-        bind_in_range_with_config(bind_address, dynamic_port_range, config).unwrap();
+        bind_in_range_with_config(*bind_address, dynamic_port_range, config).unwrap();
 
     // The non_vote_receiver will forward the verified transactions to its configured validator
     let (non_vote_sender, non_vote_receiver) = banking_tracer.create_channel_non_vote();
 
-    let destinations = values_t!(matches, "destination", String)
-        .unwrap_or_default()
-        .into_iter()
-        .map(|destination| {
-            solana_net_utils::parse_host_port(&destination).unwrap_or_else(|e| {
-                panic!("Failed to parse destination address: {e}");
-            })
-        })
-        .collect::<HashSet<_>>()
-        .into_iter()
-        .collect::<Vec<_>>();
+    let destinations = args.destination;
 
     info!("Creating the PacketBatchSender: at address: {:?} for the following initial destinations: {destinations:?}",
         sender_socket.1.local_addr());
diff --git a/vortexor/src/vortexor.rs b/vortexor/src/vortexor.rs
index 8b6208b42ecde5..b2230bc1915d54 100644
--- a/vortexor/src/vortexor.rs
+++ b/vortexor/src/vortexor.rs
@@ -56,7 +56,7 @@ impl Vortexor {
     pub fn create_tpu_sockets(
         bind_address: std::net::IpAddr,
         dynamic_port_range: (u16, u16),
-        num_quic_endpoints: u64,
+        num_quic_endpoints: usize,
     ) -> TpuSockets {
         let quic_config = SocketConfig::default().reuseport(true);
 
@@ -65,12 +65,7 @@ impl Vortexor {
                 .expect("expected bind to succeed");
 
         let tpu_quic_port = tpu_quic.local_addr().unwrap().port();
-        let tpu_quic = bind_more_with_config(
-            tpu_quic,
-            num_quic_endpoints.try_into().unwrap(),
-            quic_config,
-        )
-        .unwrap();
+        let tpu_quic = bind_more_with_config(tpu_quic, num_quic_endpoints, quic_config).unwrap();
 
         let (_, tpu_quic_fwd) = bind_in_range_with_config(
             bind_address,
@@ -79,12 +74,8 @@ impl Vortexor {
         )
         .expect("expected bind to succeed");
 
-        let tpu_quic_fwd = bind_more_with_config(
-            tpu_quic_fwd,
-            num_quic_endpoints.try_into().unwrap(),
-            quic_config,
-        )
-        .unwrap();
+        let tpu_quic_fwd =
+            bind_more_with_config(tpu_quic_fwd, num_quic_endpoints, quic_config).unwrap();
 
         TpuSockets {
             tpu_quic,
@@ -111,11 +102,11 @@ impl Vortexor {
         staked_nodes: Arc<RwLock<StakedNodes>>,
         tpu_sender: Sender<PacketBatch>,
         tpu_fwd_sender: Sender<PacketBatch>,
-        max_connections_per_peer: u64,
-        max_tpu_staked_connections: u64,
-        max_tpu_unstaked_connections: u64,
-        max_fwd_staked_connections: u64,
-        max_fwd_unstaked_connections: u64,
+        max_connections_per_peer: usize,
+        max_tpu_staked_connections: usize,
+        max_tpu_unstaked_connections: usize,
+        max_fwd_staked_connections: usize,
+        max_fwd_unstaked_connections: usize,
         max_streams_per_ms: u64,
         max_connections_per_ipaddr_per_min: u64,
         tpu_coalesce: Duration,
@@ -123,9 +114,9 @@ impl Vortexor {
         exit: Arc<AtomicBool>,
     ) -> Self {
         let mut quic_server_params = QuicServerParams {
-            max_connections_per_peer: max_connections_per_peer.try_into().unwrap(),
-            max_staked_connections: max_tpu_staked_connections.try_into().unwrap(),
-            max_unstaked_connections: max_tpu_unstaked_connections.try_into().unwrap(),
+            max_connections_per_peer,
+            max_staked_connections: max_tpu_staked_connections,
+            max_unstaked_connections: max_tpu_unstaked_connections,
             max_streams_per_ms,
             max_connections_per_ipaddr_per_min,
             wait_for_chunk_timeout: DEFAULT_WAIT_FOR_CHUNK_TIMEOUT,
@@ -152,9 +143,8 @@ impl Vortexor {
 
         // Fot TPU forward -- we disallow unstaked connections. Allocate all connection resources
         // for staked connections:
-        quic_server_params.max_staked_connections = max_fwd_staked_connections.try_into().unwrap();
-        quic_server_params.max_unstaked_connections =
-            max_fwd_unstaked_connections.try_into().unwrap();
+        quic_server_params.max_staked_connections = max_fwd_staked_connections;
+        quic_server_params.max_unstaked_connections = max_fwd_unstaked_connections;
         let tpu_fwd_result = spawn_server_multi(
             "solVtxTpuFwd",
             "quic_vortexor_tpu_forwards",
diff --git a/vortexor/tests/vortexor.rs b/vortexor/tests/vortexor.rs
index 17ab106c7753e2..b0f67d3a2b687a 100644
--- a/vortexor/tests/vortexor.rs
+++ b/vortexor/tests/vortexor.rs
@@ -36,7 +36,7 @@ async fn test_vortexor() {
     let tpu_sockets = Vortexor::create_tpu_sockets(
         bind_address,
         VALIDATOR_PORT_RANGE,
-        DEFAULT_NUM_QUIC_ENDPOINTS.try_into().unwrap(),
+        DEFAULT_NUM_QUIC_ENDPOINTS,
     );
 
     let tpu_address = tpu_sockets.tpu_quic[0].local_addr().unwrap();
@@ -53,13 +53,10 @@ async fn test_vortexor() {
         staked_nodes,
         tpu_sender,
         tpu_fwd_sender,
-        DEFAULT_MAX_QUIC_CONNECTIONS_PER_PEER.try_into().unwrap(),
-        DEFAULT_MAX_STAKED_CONNECTIONS.try_into().unwrap(),
-        DEFAULT_MAX_UNSTAKED_CONNECTIONS.try_into().unwrap(),
-        DEFAULT_MAX_STAKED_CONNECTIONS
-            .saturating_add(DEFAULT_MAX_UNSTAKED_CONNECTIONS)
-            .try_into()
-            .unwrap(), // max_fwd_staked_connections
+        DEFAULT_MAX_QUIC_CONNECTIONS_PER_PEER,
+        DEFAULT_MAX_STAKED_CONNECTIONS,
+        DEFAULT_MAX_UNSTAKED_CONNECTIONS,
+        DEFAULT_MAX_STAKED_CONNECTIONS.saturating_add(DEFAULT_MAX_UNSTAKED_CONNECTIONS), // max_fwd_staked_connections
         0, // max_fwd_unstaked_connections
         DEFAULT_MAX_STREAMS_PER_MS,
         DEFAULT_MAX_CONNECTIONS_PER_IPADDR_PER_MINUTE,


Reviews:
user: login: lijunwangs
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-07T21:28:46Z
user: login: alexpyattaev
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-11T21:10:54Z
user: login: lijunwangs
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-12T19:48:27Z
user: login: alexpyattaev
body: LGTM!
state: APPROVED
author_association: NONE
submitted_at: 2025-03-13T14:40:19Z
user: login: lijunwangs
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-13T18:17:01Z
user: login: lijunwangs
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-13T18:17:15Z
user: login: alexpyattaev
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-14T06:42:31Z

Review comments:
diff_hunk: @@ -15,7 +15,8 @@ edition = { workspace = true }
 agave-banking-stage-ingress-types = { workspace = true }
 async-channel = { workspace = true }
 bytes = { workspace = true }
-clap = { workspace = true }
+clap = { version="4.5.31", features = ["cargo", "error-context"] }
+#clap_builder = { version="4.5.31", features = ["cargo"] }
path: vortexor/Cargo.toml
user: login: lijunwangs
body: remove
created_at: 2025-03-07T21:28:46Z
author_association: NONE
diff_hunk: @@ -63,155 +33,84 @@ fn port_range_validator(port_range: &str) -> Result<(u16, u16), String> {
     }
 }
 
-pub fn command(version: &str, default_args: DefaultArgs) -> Command {
-    // The default values need to be static:
+fn get_version() -> &'static str {
+    let version = solana_version::version!();
     let version_static: &'static str = Box::leak(version.to_string().into_boxed_str());
-    let bind_address_static: &'static str = Box::leak(default_args.bind_address.into_boxed_str());
-    let port_range_static: &'static str =
-        Box::leak(default_args.dynamic_port_range.into_boxed_str());
-    let max_connections_per_peer_static: &'static str =
-        Box::leak(default_args.max_connections_per_peer.into_boxed_str());
-    let max_tpu_staked_connections_static: &'static str =
-        Box::leak(default_args.max_tpu_staked_connections.into_boxed_str());
-    let max_tpu_unstaked_connections_static: &'static str =
-        Box::leak(default_args.max_tpu_unstaked_connections.into_boxed_str());
-    let max_fwd_staked_connections_static: &'static str =
-        Box::leak(default_args.max_fwd_staked_connections.into_boxed_str());
-    let max_fwd_unstaked_connections_static: &'static str =
-        Box::leak(default_args.max_fwd_unstaked_connections.into_boxed_str());
-    let max_connections_per_ipaddr_per_min_static: &'static str = Box::leak(
-        default_args
-            .max_connections_per_ipaddr_per_min
-            .into_boxed_str(),
-    );
-    let num_quic_endpoints_static: &'static str =
-        Box::leak(default_args.num_quic_endpoints.into_boxed_str());
-    let max_streams_per_ms_static: &'static str =
-        Box::leak(default_args.max_streams_per_ms.into_boxed_str());
-
-    Command::new(crate_name!())
-        .about(crate_description!())
-        .version(version_static)
-        .infer_subcommands(true)
-        .color(ColorChoice::Auto)
-        .arg(
-            Arg::new("identity")
-                .long("identity")
-                .value_name("KEYPAIR")
-                .num_args(1)
-                .required(true)
-                .value_parser(clap::value_parser!(PathBuf))
-                .help("Vortexor identity keypair"),
-        )
-        .arg(
-            Arg::new("bind_address")
-                .long("bind-address")
-                .value_name("HOST")
-                .num_args(1)
-                .value_parser(solana_net_utils::parse_host)
-                .default_value(bind_address_static)
-                .help("IP address to bind the validator ports"),
-        )
-        .arg(
-            Arg::new("dynamic_port_range")
-                .long("dynamic-port-range")
-                .value_name("MIN_PORT-MAX_PORT")
-                .num_args(1)
-                .default_value(port_range_static)
-                .value_parser(port_range_validator)
-                .help("Range to use for dynamically assigned ports"),
-        )
-        .arg(
-            Arg::new("max_connections_per_peer")
-                .long("max-connections-per-peer")
-                .num_args(1)
-                .default_value(max_connections_per_peer_static)
-                .value_parser(clap::value_parser!(u64))
-                .help("Controls the max concurrent connections per IpAddr."),
-        )
-        .arg(
-            Arg::new("max_tpu_staked_connections")
-                .long("max-tpu-staked-connections")
-                .num_args(1)
-                .default_value(max_tpu_staked_connections_static)
-                .value_parser(clap::value_parser!(u64))
-                .help("Controls the max concurrent connections for TPU from staked nodes."),
-        )
-        .arg(
-            Arg::new("max_tpu_unstaked_connections")
-                .long("max-tpu-unstaked-connections")
-                .num_args(1)
-                .default_value(max_tpu_unstaked_connections_static)
-                .value_parser(clap::value_parser!(u64))
-                .help("Controls the max concurrent connections fort TPU from unstaked nodes."),
-        )
-        .arg(
-            Arg::new("max_fwd_staked_connections")
-                .long("max-fwd-staked-connections")
-                .num_args(1)
-                .default_value(max_fwd_staked_connections_static)
-                .value_parser(clap::value_parser!(u64))
-                .help("Controls the max concurrent connections for TPU-forward from staked nodes."),
-        )
-        .arg(
-            Arg::new("max_fwd_unstaked_connections")
-                .long("max-fwd-unstaked-connections")
-                .num_args(1)
-                .default_value(max_fwd_unstaked_connections_static)
-                .value_parser(clap::value_parser!(u64))
-                .help("Controls the max concurrent connections for TPU-forward from unstaked nodes."),
-        )
-        .arg(
-            Arg::new("max_connections_per_ipaddr_per_minute")
-                .long("max-connections-per-ipaddr-per-minute")
-                .num_args(1)
-                .default_value(max_connections_per_ipaddr_per_min_static)
-                .value_parser(clap::value_parser!(u64))
-                .help("Controls the rate of the clients connections per IpAddr per minute."),
-        )
-        .arg(
-            Arg::new("num_quic_endpoints")
-                .long("num-quic-endpoints")
-                .num_args(1)
-                .default_value(num_quic_endpoints_static)
-                .value_parser(clap::value_parser!(u64))
-                .help("The number of QUIC endpoints used for TPU and TPU-Forward. It can be increased to \
-                       increase network ingest throughput, at the expense of higher CPU and general \
-                       validator load."),
-        )
-        .arg(
-            Arg::new("max_streams_per_ms")
-                .long("max-streams-per-ms")
-                .num_args(1)
-                .default_value(max_streams_per_ms_static)
-                .value_parser(clap::value_parser!(u64))
-                .help("Max streams per second for a streamer."),
-        )
-        .arg(
-            Arg::new("tpu_coalesce_ms")
-                .long("tpu-coalesce-ms")
-                .value_name("MILLISECS")
-                .num_args(1)
-                .value_parser(clap::value_parser!(u64))
-                .help("Milliseconds to wait in the TPU receiver for packet coalescing."),
-        )
-        .arg(
-            Arg::new("logfile")
-                .long("log")
-                .value_name("FILE")
-                .num_args(1)
-                .help(
-                    "Redirect logging to the specified file, '-' for standard error. Sending the \
-                     SIGUSR1 signal to the vortexor process will cause it to re-open the log file.",
-                ),
-        )
-        .arg(
-            Arg::new("destination")
-                .long("destination")
-                .value_name("HOST:PORT")
-                .action(ArgAction::Append)
-                .num_args(1)
-                .value_parser(solana_net_utils::parse_host_port)
-                .help("The destination validator address to which the vortexor will forward transactions."),
-        )
+    version_static
 }
+
+fn get_default_port_range() -> &'static str {
+    let range = format!("{}-{}", VALIDATOR_PORT_RANGE.0, VALIDATOR_PORT_RANGE.1);
+    let range: &'static str = Box::leak(range.into_boxed_str());
+    range
+}
+
+fn get_default_tpu_coalesce_ms() -> &'static str {
+    let coalesce = DEFAULT_TPU_COALESCE.as_millis().to_string();
+    let coalesce: &'static str = Box::leak(coalesce.into_boxed_str());
+    coalesce
+}
+
+#[derive(Parser)]
+#[command(name=crate_name!(),version=get_version(), about=crate_description!(),
+    long_about = None, color=ColorChoice::Auto)]
+pub struct Cli {
+    /// Vortexor identity keypair
+    #[arg(long, num_args=1, value_parser=clap::value_parser!(PathBuf), required=true, value_name="KEYPAIR")]
path: vortexor/src/cli.rs
user: login: alexpyattaev
body: Do you have to specify value_parser? I think it will use appropriate one by default here. This works fine:
```
#[arg(short, long)]
        /// Directory with pcap files to write. Existing ontents will be destroyed!
        output: PathBuf,
```
num_args is also not necessary here. required=true is implicit unless the arg value is an Option<T>
created_at: 2025-03-11T21:10:54Z
author_association: NONE
diff_hunk: @@ -56,139 +26,91 @@ fn port_range_validator(port_range: String) -> Result<(), String> {
         } else if end.checked_add(QUIC_PORT_OFFSET).is_none() {
             Err("Invalid dynamic_port_range.".to_string())
         } else {
-            Ok(())
+            Ok((start, end))
         }
     } else {
         Err("Invalid port range".to_string())
     }
 }
 
-pub fn app<'a>(version: &'a str, default_args: &'a DefaultArgs) -> App<'a, 'a> {
-    App::new(crate_name!())
-        .about(crate_description!())
-        .version(version)
-        .global_setting(AppSettings::ColoredHelp)
-        .global_setting(AppSettings::InferSubcommands)
-        .global_setting(AppSettings::UnifiedHelpMessage)
-        .global_setting(AppSettings::VersionlessSubcommands)
-        .arg(
-            Arg::with_name("identity")
-                .short("i")
-                .long("identity")
-                .value_name("KEYPAIR")
-                .takes_value(true)
-                .validator(is_keypair_or_ask_keyword)
-                .help("Vortexor identity keypair"),
-        )
-        .arg(
-            Arg::with_name("bind_address")
-                .long("bind-address")
-                .value_name("HOST")
-                .takes_value(true)
-                .validator(solana_net_utils::is_host)
-                .default_value(&default_args.bind_address)
-                .help("IP address to bind the validator ports"),
-        )
-        .arg(
-            Arg::with_name("dynamic_port_range")
-                .long("dynamic-port-range")
-                .value_name("MIN_PORT-MAX_PORT")
-                .takes_value(true)
-                .default_value(&default_args.dynamic_port_range)
-                .validator(port_range_validator)
-                .help("Range to use for dynamically assigned ports"),
-        )
-        .arg(
-            Arg::with_name("max_connections_per_peer")
-                .long("max-connections-per-peer")
-                .takes_value(true)
-                .default_value(&default_args.max_connections_per_peer)
-                .validator(is_parsable::<u32>)
-                .help("Controls the max concurrent connections per IpAddr."),
-        )
-        .arg(
-            Arg::with_name("max_tpu_staked_connections")
-                .long("max-tpu-staked-connections")
-                .takes_value(true)
-                .default_value(&default_args.max_tpu_staked_connections)
-                .validator(is_parsable::<u32>)
-                .help("Controls the max concurrent connections for TPU from staked nodes."),
-        )
-        .arg(
-            Arg::with_name("max_tpu_unstaked_connections")
-                .long("max-tpu-unstaked-connections")
-                .takes_value(true)
-                .default_value(&default_args.max_tpu_unstaked_connections)
-                .validator(is_parsable::<u32>)
-                .help("Controls the max concurrent connections fort TPU from unstaked nodes."),
-        )
-        .arg(
-            Arg::with_name("max_fwd_staked_connections")
-                .long("max-fwd-staked-connections")
-                .takes_value(true)
-                .default_value(&default_args.max_fwd_staked_connections)
-                .validator(is_parsable::<u32>)
-                .help("Controls the max concurrent connections for TPU-forward from staked nodes."),
-        )
-        .arg(
-            Arg::with_name("max_fwd_unstaked_connections")
-                .long("max-fwd-unstaked-connections")
-                .takes_value(true)
-                .default_value(&default_args.max_fwd_unstaked_connections)
-                .validator(is_parsable::<u32>)
-                .help("Controls the max concurrent connections for TPU-forward from unstaked nodes."),
-        )
-        .arg(
-            Arg::with_name("max_connections_per_ipaddr_per_minute")
-                .long("max-connections-per-ipaddr-per-minute")
-                .takes_value(true)
-                .default_value(&default_args.max_connections_per_ipaddr_per_min)
-                .validator(is_parsable::<u32>)
-                .help("Controls the rate of the clients connections per IpAddr per minute."),
-        )
-        .arg(
-            Arg::with_name("num_quic_endpoints")
-                .long("num-quic-endpoints")
-                .takes_value(true)
-                .default_value(&default_args.num_quic_endpoints)
-                .validator(is_parsable::<usize>)
-                .help("The number of QUIC endpoints used for TPU and TPU-Forward. It can be increased to \
-                       increase network ingest throughput, at the expense of higher CPU and general \
-                       validator load."),
-        )
-        .arg(
-            Arg::with_name("max_streams_per_ms")
-                .long("max-streams-per-ms")
-                .takes_value(true)
-                .default_value(&default_args.max_streams_per_ms)
-                .validator(is_parsable::<usize>)
-                .help("Max streams per second for a streamer."),
-        )
-        .arg(
-            Arg::with_name("tpu_coalesce_ms")
-                .long("tpu-coalesce-ms")
-                .value_name("MILLISECS")
-                .takes_value(true)
-                .validator(is_parsable::<u64>)
-                .help("Milliseconds to wait in the TPU receiver for packet coalescing."),
-        )
-        .arg(
-            Arg::with_name("logfile")
-                .long("log")
-                .value_name("FILE")
-                .takes_value(true)
-                .help(
-                    "Redirect logging to the specified file, '-' for standard error. Sending the \
-                     SIGUSR1 signal to the vortexor process will cause it to re-open the log file.",
-                ),
-        )
-        .arg(
-            Arg::with_name("destination")
-                .long("destination")
-                .value_name("HOST:PORT")
-                .takes_value(true)
-                .multiple(true)
-                .validator(solana_net_utils::is_host_port)
-                .help("The destination validator address to which the vortexor will forward transactions."),
-        )
+fn get_version() -> &'static str {
+    let version = solana_version::version!();
+    let version_static: &'static str = Box::leak(version.to_string().into_boxed_str());
+    version_static
+}
+
+fn get_default_port_range() -> &'static str {
+    let range = format!("{}-{}", VALIDATOR_PORT_RANGE.0, VALIDATOR_PORT_RANGE.1);
+    let range: &'static str = Box::leak(range.into_boxed_str());
+    range
+}
+
+fn get_default_tpu_coalesce_ms() -> &'static str {
+    let coalesce = DEFAULT_TPU_COALESCE.as_millis().to_string();
+    let coalesce: &'static str = Box::leak(coalesce.into_boxed_str());
+    coalesce
+}
+
+#[derive(Parser)]
+#[command(name=crate_name!(),version=get_version(), about=crate_description!(),
+    long_about = None, color=ColorChoice::Auto)]
+pub struct Cli {
+    /// Vortexor identity keypair
+    #[arg(long, value_name = "KEYPAIR")]
+    pub identity: PathBuf,
+
+    /// IP address to bind the vortexor ports
+    #[arg(long, value_parser = solana_net_utils::parse_host, default_value = "0.0.0.0", value_name = "HOST")]
+    pub bind_address: IpAddr,
+
+    /// The destination validator address to which the vortexor will forward transactions.
+    #[arg(long, value_parser = solana_net_utils::parse_host_port, value_name = "HOST:PORT", action = ArgAction::Append)]
+    pub destination: Vec<SocketAddr>,
path: vortexor/src/cli.rs
user: login: alexpyattaev
body: You need not use custom parsers in v4, this works:
```
    /// IP address to bind the vortexor ports
    #[arg(long, default_value = "0.0.0.0", value_name = "HOST")]
    pub bind_address: IpAddr,

    /// The destination validator address to which the vortexor will forward transactions.
    #[arg(long, value_name = "HOST:PORT", action = ArgAction::Append)]
    pub destination: Vec<SocketAddr>,
```
and achieves the same result.
created_at: 2025-03-12T10:02:16Z
author_association: NONE
diff_hunk: @@ -56,139 +26,91 @@ fn port_range_validator(port_range: String) -> Result<(), String> {
         } else if end.checked_add(QUIC_PORT_OFFSET).is_none() {
             Err("Invalid dynamic_port_range.".to_string())
         } else {
-            Ok(())
+            Ok((start, end))
         }
     } else {
         Err("Invalid port range".to_string())
     }
 }
 
-pub fn app<'a>(version: &'a str, default_args: &'a DefaultArgs) -> App<'a, 'a> {
-    App::new(crate_name!())
-        .about(crate_description!())
-        .version(version)
-        .global_setting(AppSettings::ColoredHelp)
-        .global_setting(AppSettings::InferSubcommands)
-        .global_setting(AppSettings::UnifiedHelpMessage)
-        .global_setting(AppSettings::VersionlessSubcommands)
-        .arg(
-            Arg::with_name("identity")
-                .short("i")
-                .long("identity")
-                .value_name("KEYPAIR")
-                .takes_value(true)
-                .validator(is_keypair_or_ask_keyword)
-                .help("Vortexor identity keypair"),
-        )
-        .arg(
-            Arg::with_name("bind_address")
-                .long("bind-address")
-                .value_name("HOST")
-                .takes_value(true)
-                .validator(solana_net_utils::is_host)
-                .default_value(&default_args.bind_address)
-                .help("IP address to bind the validator ports"),
-        )
-        .arg(
-            Arg::with_name("dynamic_port_range")
-                .long("dynamic-port-range")
-                .value_name("MIN_PORT-MAX_PORT")
-                .takes_value(true)
-                .default_value(&default_args.dynamic_port_range)
-                .validator(port_range_validator)
-                .help("Range to use for dynamically assigned ports"),
-        )
-        .arg(
-            Arg::with_name("max_connections_per_peer")
-                .long("max-connections-per-peer")
-                .takes_value(true)
-                .default_value(&default_args.max_connections_per_peer)
-                .validator(is_parsable::<u32>)
-                .help("Controls the max concurrent connections per IpAddr."),
-        )
-        .arg(
-            Arg::with_name("max_tpu_staked_connections")
-                .long("max-tpu-staked-connections")
-                .takes_value(true)
-                .default_value(&default_args.max_tpu_staked_connections)
-                .validator(is_parsable::<u32>)
-                .help("Controls the max concurrent connections for TPU from staked nodes."),
-        )
-        .arg(
-            Arg::with_name("max_tpu_unstaked_connections")
-                .long("max-tpu-unstaked-connections")
-                .takes_value(true)
-                .default_value(&default_args.max_tpu_unstaked_connections)
-                .validator(is_parsable::<u32>)
-                .help("Controls the max concurrent connections fort TPU from unstaked nodes."),
-        )
-        .arg(
-            Arg::with_name("max_fwd_staked_connections")
-                .long("max-fwd-staked-connections")
-                .takes_value(true)
-                .default_value(&default_args.max_fwd_staked_connections)
-                .validator(is_parsable::<u32>)
-                .help("Controls the max concurrent connections for TPU-forward from staked nodes."),
-        )
-        .arg(
-            Arg::with_name("max_fwd_unstaked_connections")
-                .long("max-fwd-unstaked-connections")
-                .takes_value(true)
-                .default_value(&default_args.max_fwd_unstaked_connections)
-                .validator(is_parsable::<u32>)
-                .help("Controls the max concurrent connections for TPU-forward from unstaked nodes."),
-        )
-        .arg(
-            Arg::with_name("max_connections_per_ipaddr_per_minute")
-                .long("max-connections-per-ipaddr-per-minute")
-                .takes_value(true)
-                .default_value(&default_args.max_connections_per_ipaddr_per_min)
-                .validator(is_parsable::<u32>)
-                .help("Controls the rate of the clients connections per IpAddr per minute."),
-        )
-        .arg(
-            Arg::with_name("num_quic_endpoints")
-                .long("num-quic-endpoints")
-                .takes_value(true)
-                .default_value(&default_args.num_quic_endpoints)
-                .validator(is_parsable::<usize>)
-                .help("The number of QUIC endpoints used for TPU and TPU-Forward. It can be increased to \
-                       increase network ingest throughput, at the expense of higher CPU and general \
-                       validator load."),
-        )
-        .arg(
-            Arg::with_name("max_streams_per_ms")
-                .long("max-streams-per-ms")
-                .takes_value(true)
-                .default_value(&default_args.max_streams_per_ms)
-                .validator(is_parsable::<usize>)
-                .help("Max streams per second for a streamer."),
-        )
-        .arg(
-            Arg::with_name("tpu_coalesce_ms")
-                .long("tpu-coalesce-ms")
-                .value_name("MILLISECS")
-                .takes_value(true)
-                .validator(is_parsable::<u64>)
-                .help("Milliseconds to wait in the TPU receiver for packet coalescing."),
-        )
-        .arg(
-            Arg::with_name("logfile")
-                .long("log")
-                .value_name("FILE")
-                .takes_value(true)
-                .help(
-                    "Redirect logging to the specified file, '-' for standard error. Sending the \
-                     SIGUSR1 signal to the vortexor process will cause it to re-open the log file.",
-                ),
-        )
-        .arg(
-            Arg::with_name("destination")
-                .long("destination")
-                .value_name("HOST:PORT")
-                .takes_value(true)
-                .multiple(true)
-                .validator(solana_net_utils::is_host_port)
-                .help("The destination validator address to which the vortexor will forward transactions."),
-        )
+fn get_version() -> &'static str {
+    let version = solana_version::version!();
+    let version_static: &'static str = Box::leak(version.to_string().into_boxed_str());
+    version_static
+}
+
+fn get_default_port_range() -> &'static str {
+    let range = format!("{}-{}", VALIDATOR_PORT_RANGE.0, VALIDATOR_PORT_RANGE.1);
+    let range: &'static str = Box::leak(range.into_boxed_str());
+    range
+}
+
+fn get_default_tpu_coalesce_ms() -> &'static str {
+    let coalesce = DEFAULT_TPU_COALESCE.as_millis().to_string();
+    let coalesce: &'static str = Box::leak(coalesce.into_boxed_str());
+    coalesce
+}
+
+#[derive(Parser)]
+#[command(name=crate_name!(),version=get_version(), about=crate_description!(),
+    long_about = None, color=ColorChoice::Auto)]
+pub struct Cli {
+    /// Vortexor identity keypair
+    #[arg(long, value_name = "KEYPAIR")]
+    pub identity: PathBuf,
+
+    /// IP address to bind the vortexor ports
+    #[arg(long, value_parser = solana_net_utils::parse_host, default_value = "0.0.0.0", value_name = "HOST")]
+    pub bind_address: IpAddr,
+
+    /// The destination validator address to which the vortexor will forward transactions.
+    #[arg(long, value_parser = solana_net_utils::parse_host_port, value_name = "HOST:PORT", action = ArgAction::Append)]
+    pub destination: Vec<SocketAddr>,
+
+    /// Range to use for dynamically assigned ports
+    #[arg(long, value_parser = parse_port_range, value_name = "MIN_PORT-MAX_PORT", default_value = get_default_port_range())]
+    pub dynamic_port_range: (u16, u16),
+
+    /// Controls the max concurrent connections per IpAddr.
+    #[arg(long, default_value_t = DEFAULT_MAX_QUIC_CONNECTIONS_PER_PEER)]
+    pub max_connections_per_peer: usize,
+
+    /// Controls the max concurrent connections for TPU from staked nodes.
+    #[arg(long, default_value_t = DEFAULT_MAX_STAKED_CONNECTIONS)]
+    pub max_tpu_staked_connections: usize,
+
+    /// Controls the max concurrent connections fort TPU from unstaked nodes.
+    #[arg(long, default_value_t = DEFAULT_MAX_UNSTAKED_CONNECTIONS)]
+    pub max_tpu_unstaked_connections: usize,
+
+    /// Controls the max concurrent connections for TPU-forward from staked nodes.
+    #[arg(long, default_value_t = DEFAULT_MAX_STAKED_CONNECTIONS)]
+    pub max_fwd_staked_connections: usize,
+
+    /// Controls the max concurrent connections for TPU-forward from unstaked nodes.
+    #[arg(long, default_value_t = 0)]
+    pub max_fwd_unstaked_connections: usize,
+
+    /// Controls the rate of the clients connections per IpAddr per minute.
+    #[arg(long, default_value_t = DEFAULT_MAX_CONNECTIONS_PER_IPADDR_PER_MINUTE)]
+    pub max_connections_per_ipaddr_per_minute: u64,
+
+    /// The number of QUIC endpoints used for TPU and TPU-Forward. It can be increased to
+    /// increase network ingest throughput, at the expense of higher CPU and general
+    /// validator load.
+    #[arg(long, default_value_t = DEFAULT_NUM_QUIC_ENDPOINTS)]
+    pub num_quic_endpoints: usize,
+
+    /// Max streams per second for a streamer.
+    #[arg(long, default_value_t = DEFAULT_MAX_STREAMS_PER_MS)]
+    pub max_streams_per_ms: u64,
+
+    /// Milliseconds to wait in the TPU receiver for packet coalescing.
+    #[arg(long, default_value = get_default_tpu_coalesce_ms())]
+    pub tpu_coalesce_ms: u64,
+
+    /// Redirect logging to the specified file, '-' for standard error. Sending the
+    /// SIGUSR1 signal to the vortexor process will cause it to re-open the log file.
+    #[arg(long="log", value_name = "FILE", value_parser = clap::value_parser!(String))]
+    pub logfile: Option<String>,
path: vortexor/src/cli.rs
user: login: alexpyattaev
body: Typically `-` is used for redirect to stdout, which is the default in our case. I suggest this to be made into PathBuf so it can accept non-utf characters.
created_at: 2025-03-12T10:04:06Z
author_association: NONE
diff_hunk: @@ -63,155 +33,84 @@ fn port_range_validator(port_range: &str) -> Result<(u16, u16), String> {
     }
 }
 
-pub fn command(version: &str, default_args: DefaultArgs) -> Command {
-    // The default values need to be static:
+fn get_version() -> &'static str {
+    let version = solana_version::version!();
     let version_static: &'static str = Box::leak(version.to_string().into_boxed_str());
-    let bind_address_static: &'static str = Box::leak(default_args.bind_address.into_boxed_str());
-    let port_range_static: &'static str =
-        Box::leak(default_args.dynamic_port_range.into_boxed_str());
-    let max_connections_per_peer_static: &'static str =
-        Box::leak(default_args.max_connections_per_peer.into_boxed_str());
-    let max_tpu_staked_connections_static: &'static str =
-        Box::leak(default_args.max_tpu_staked_connections.into_boxed_str());
-    let max_tpu_unstaked_connections_static: &'static str =
-        Box::leak(default_args.max_tpu_unstaked_connections.into_boxed_str());
-    let max_fwd_staked_connections_static: &'static str =
-        Box::leak(default_args.max_fwd_staked_connections.into_boxed_str());
-    let max_fwd_unstaked_connections_static: &'static str =
-        Box::leak(default_args.max_fwd_unstaked_connections.into_boxed_str());
-    let max_connections_per_ipaddr_per_min_static: &'static str = Box::leak(
-        default_args
-            .max_connections_per_ipaddr_per_min
-            .into_boxed_str(),
-    );
-    let num_quic_endpoints_static: &'static str =
-        Box::leak(default_args.num_quic_endpoints.into_boxed_str());
-    let max_streams_per_ms_static: &'static str =
-        Box::leak(default_args.max_streams_per_ms.into_boxed_str());
-
-    Command::new(crate_name!())
-        .about(crate_description!())
-        .version(version_static)
-        .infer_subcommands(true)
-        .color(ColorChoice::Auto)
-        .arg(
-            Arg::new("identity")
-                .long("identity")
-                .value_name("KEYPAIR")
-                .num_args(1)
-                .required(true)
-                .value_parser(clap::value_parser!(PathBuf))
-                .help("Vortexor identity keypair"),
-        )
-        .arg(
-            Arg::new("bind_address")
-                .long("bind-address")
-                .value_name("HOST")
-                .num_args(1)
-                .value_parser(solana_net_utils::parse_host)
-                .default_value(bind_address_static)
-                .help("IP address to bind the validator ports"),
-        )
-        .arg(
-            Arg::new("dynamic_port_range")
-                .long("dynamic-port-range")
-                .value_name("MIN_PORT-MAX_PORT")
-                .num_args(1)
-                .default_value(port_range_static)
-                .value_parser(port_range_validator)
-                .help("Range to use for dynamically assigned ports"),
-        )
-        .arg(
-            Arg::new("max_connections_per_peer")
-                .long("max-connections-per-peer")
-                .num_args(1)
-                .default_value(max_connections_per_peer_static)
-                .value_parser(clap::value_parser!(u64))
-                .help("Controls the max concurrent connections per IpAddr."),
-        )
-        .arg(
-            Arg::new("max_tpu_staked_connections")
-                .long("max-tpu-staked-connections")
-                .num_args(1)
-                .default_value(max_tpu_staked_connections_static)
-                .value_parser(clap::value_parser!(u64))
-                .help("Controls the max concurrent connections for TPU from staked nodes."),
-        )
-        .arg(
-            Arg::new("max_tpu_unstaked_connections")
-                .long("max-tpu-unstaked-connections")
-                .num_args(1)
-                .default_value(max_tpu_unstaked_connections_static)
-                .value_parser(clap::value_parser!(u64))
-                .help("Controls the max concurrent connections fort TPU from unstaked nodes."),
-        )
-        .arg(
-            Arg::new("max_fwd_staked_connections")
-                .long("max-fwd-staked-connections")
-                .num_args(1)
-                .default_value(max_fwd_staked_connections_static)
-                .value_parser(clap::value_parser!(u64))
-                .help("Controls the max concurrent connections for TPU-forward from staked nodes."),
-        )
-        .arg(
-            Arg::new("max_fwd_unstaked_connections")
-                .long("max-fwd-unstaked-connections")
-                .num_args(1)
-                .default_value(max_fwd_unstaked_connections_static)
-                .value_parser(clap::value_parser!(u64))
-                .help("Controls the max concurrent connections for TPU-forward from unstaked nodes."),
-        )
-        .arg(
-            Arg::new("max_connections_per_ipaddr_per_minute")
-                .long("max-connections-per-ipaddr-per-minute")
-                .num_args(1)
-                .default_value(max_connections_per_ipaddr_per_min_static)
-                .value_parser(clap::value_parser!(u64))
-                .help("Controls the rate of the clients connections per IpAddr per minute."),
-        )
-        .arg(
-            Arg::new("num_quic_endpoints")
-                .long("num-quic-endpoints")
-                .num_args(1)
-                .default_value(num_quic_endpoints_static)
-                .value_parser(clap::value_parser!(u64))
-                .help("The number of QUIC endpoints used for TPU and TPU-Forward. It can be increased to \
-                       increase network ingest throughput, at the expense of higher CPU and general \
-                       validator load."),
-        )
-        .arg(
-            Arg::new("max_streams_per_ms")
-                .long("max-streams-per-ms")
-                .num_args(1)
-                .default_value(max_streams_per_ms_static)
-                .value_parser(clap::value_parser!(u64))
-                .help("Max streams per second for a streamer."),
-        )
-        .arg(
-            Arg::new("tpu_coalesce_ms")
-                .long("tpu-coalesce-ms")
-                .value_name("MILLISECS")
-                .num_args(1)
-                .value_parser(clap::value_parser!(u64))
-                .help("Milliseconds to wait in the TPU receiver for packet coalescing."),
-        )
-        .arg(
-            Arg::new("logfile")
-                .long("log")
-                .value_name("FILE")
-                .num_args(1)
-                .help(
-                    "Redirect logging to the specified file, '-' for standard error. Sending the \
-                     SIGUSR1 signal to the vortexor process will cause it to re-open the log file.",
-                ),
-        )
-        .arg(
-            Arg::new("destination")
-                .long("destination")
-                .value_name("HOST:PORT")
-                .action(ArgAction::Append)
-                .num_args(1)
-                .value_parser(solana_net_utils::parse_host_port)
-                .help("The destination validator address to which the vortexor will forward transactions."),
-        )
+    version_static
 }
+
+fn get_default_port_range() -> &'static str {
+    let range = format!("{}-{}", VALIDATOR_PORT_RANGE.0, VALIDATOR_PORT_RANGE.1);
+    let range: &'static str = Box::leak(range.into_boxed_str());
+    range
+}
+
+fn get_default_tpu_coalesce_ms() -> &'static str {
+    let coalesce = DEFAULT_TPU_COALESCE.as_millis().to_string();
+    let coalesce: &'static str = Box::leak(coalesce.into_boxed_str());
+    coalesce
+}
+
+#[derive(Parser)]
+#[command(name=crate_name!(),version=get_version(), about=crate_description!(),
+    long_about = None, color=ColorChoice::Auto)]
+pub struct Cli {
+    /// Vortexor identity keypair
+    #[arg(long, num_args=1, value_parser=clap::value_parser!(PathBuf), required=true, value_name="KEYPAIR")]
path: vortexor/src/cli.rs
user: login: lijunwangs
body: Done
created_at: 2025-03-12T19:48:26Z
author_association: NONE
diff_hunk: @@ -56,139 +26,91 @@ fn port_range_validator(port_range: String) -> Result<(), String> {
         } else if end.checked_add(QUIC_PORT_OFFSET).is_none() {
             Err("Invalid dynamic_port_range.".to_string())
         } else {
-            Ok(())
+            Ok((start, end))
         }
     } else {
         Err("Invalid port range".to_string())
     }
 }
 
-pub fn app<'a>(version: &'a str, default_args: &'a DefaultArgs) -> App<'a, 'a> {
-    App::new(crate_name!())
-        .about(crate_description!())
-        .version(version)
-        .global_setting(AppSettings::ColoredHelp)
-        .global_setting(AppSettings::InferSubcommands)
-        .global_setting(AppSettings::UnifiedHelpMessage)
-        .global_setting(AppSettings::VersionlessSubcommands)
-        .arg(
-            Arg::with_name("identity")
-                .short("i")
-                .long("identity")
-                .value_name("KEYPAIR")
-                .takes_value(true)
-                .validator(is_keypair_or_ask_keyword)
-                .help("Vortexor identity keypair"),
-        )
-        .arg(
-            Arg::with_name("bind_address")
-                .long("bind-address")
-                .value_name("HOST")
-                .takes_value(true)
-                .validator(solana_net_utils::is_host)
-                .default_value(&default_args.bind_address)
-                .help("IP address to bind the validator ports"),
-        )
-        .arg(
-            Arg::with_name("dynamic_port_range")
-                .long("dynamic-port-range")
-                .value_name("MIN_PORT-MAX_PORT")
-                .takes_value(true)
-                .default_value(&default_args.dynamic_port_range)
-                .validator(port_range_validator)
-                .help("Range to use for dynamically assigned ports"),
-        )
-        .arg(
-            Arg::with_name("max_connections_per_peer")
-                .long("max-connections-per-peer")
-                .takes_value(true)
-                .default_value(&default_args.max_connections_per_peer)
-                .validator(is_parsable::<u32>)
-                .help("Controls the max concurrent connections per IpAddr."),
-        )
-        .arg(
-            Arg::with_name("max_tpu_staked_connections")
-                .long("max-tpu-staked-connections")
-                .takes_value(true)
-                .default_value(&default_args.max_tpu_staked_connections)
-                .validator(is_parsable::<u32>)
-                .help("Controls the max concurrent connections for TPU from staked nodes."),
-        )
-        .arg(
-            Arg::with_name("max_tpu_unstaked_connections")
-                .long("max-tpu-unstaked-connections")
-                .takes_value(true)
-                .default_value(&default_args.max_tpu_unstaked_connections)
-                .validator(is_parsable::<u32>)
-                .help("Controls the max concurrent connections fort TPU from unstaked nodes."),
-        )
-        .arg(
-            Arg::with_name("max_fwd_staked_connections")
-                .long("max-fwd-staked-connections")
-                .takes_value(true)
-                .default_value(&default_args.max_fwd_staked_connections)
-                .validator(is_parsable::<u32>)
-                .help("Controls the max concurrent connections for TPU-forward from staked nodes."),
-        )
-        .arg(
-            Arg::with_name("max_fwd_unstaked_connections")
-                .long("max-fwd-unstaked-connections")
-                .takes_value(true)
-                .default_value(&default_args.max_fwd_unstaked_connections)
-                .validator(is_parsable::<u32>)
-                .help("Controls the max concurrent connections for TPU-forward from unstaked nodes."),
-        )
-        .arg(
-            Arg::with_name("max_connections_per_ipaddr_per_minute")
-                .long("max-connections-per-ipaddr-per-minute")
-                .takes_value(true)
-                .default_value(&default_args.max_connections_per_ipaddr_per_min)
-                .validator(is_parsable::<u32>)
-                .help("Controls the rate of the clients connections per IpAddr per minute."),
-        )
-        .arg(
-            Arg::with_name("num_quic_endpoints")
-                .long("num-quic-endpoints")
-                .takes_value(true)
-                .default_value(&default_args.num_quic_endpoints)
-                .validator(is_parsable::<usize>)
-                .help("The number of QUIC endpoints used for TPU and TPU-Forward. It can be increased to \
-                       increase network ingest throughput, at the expense of higher CPU and general \
-                       validator load."),
-        )
-        .arg(
-            Arg::with_name("max_streams_per_ms")
-                .long("max-streams-per-ms")
-                .takes_value(true)
-                .default_value(&default_args.max_streams_per_ms)
-                .validator(is_parsable::<usize>)
-                .help("Max streams per second for a streamer."),
-        )
-        .arg(
-            Arg::with_name("tpu_coalesce_ms")
-                .long("tpu-coalesce-ms")
-                .value_name("MILLISECS")
-                .takes_value(true)
-                .validator(is_parsable::<u64>)
-                .help("Milliseconds to wait in the TPU receiver for packet coalescing."),
-        )
-        .arg(
-            Arg::with_name("logfile")
-                .long("log")
-                .value_name("FILE")
-                .takes_value(true)
-                .help(
-                    "Redirect logging to the specified file, '-' for standard error. Sending the \
-                     SIGUSR1 signal to the vortexor process will cause it to re-open the log file.",
-                ),
-        )
-        .arg(
-            Arg::with_name("destination")
-                .long("destination")
-                .value_name("HOST:PORT")
-                .takes_value(true)
-                .multiple(true)
-                .validator(solana_net_utils::is_host_port)
-                .help("The destination validator address to which the vortexor will forward transactions."),
-        )
+fn get_version() -> &'static str {
+    let version = solana_version::version!();
+    let version_static: &'static str = Box::leak(version.to_string().into_boxed_str());
+    version_static
+}
+
+fn get_default_port_range() -> &'static str {
+    let range = format!("{}-{}", VALIDATOR_PORT_RANGE.0, VALIDATOR_PORT_RANGE.1);
+    let range: &'static str = Box::leak(range.into_boxed_str());
+    range
+}
+
+fn get_default_tpu_coalesce_ms() -> &'static str {
+    let coalesce = DEFAULT_TPU_COALESCE.as_millis().to_string();
+    let coalesce: &'static str = Box::leak(coalesce.into_boxed_str());
+    coalesce
+}
+
+#[derive(Parser)]
+#[command(name=crate_name!(),version=get_version(), about=crate_description!(),
+    long_about = None, color=ColorChoice::Auto)]
+pub struct Cli {
+    /// Vortexor identity keypair
+    #[arg(long, value_name = "KEYPAIR")]
+    pub identity: PathBuf,
+
+    /// IP address to bind the vortexor ports
+    #[arg(long, value_parser = solana_net_utils::parse_host, default_value = "0.0.0.0", value_name = "HOST")]
+    pub bind_address: IpAddr,
+
+    /// The destination validator address to which the vortexor will forward transactions.
+    #[arg(long, value_parser = solana_net_utils::parse_host_port, value_name = "HOST:PORT", action = ArgAction::Append)]
+    pub destination: Vec<SocketAddr>,
+
+    /// Range to use for dynamically assigned ports
+    #[arg(long, value_parser = parse_port_range, value_name = "MIN_PORT-MAX_PORT", default_value = get_default_port_range())]
+    pub dynamic_port_range: (u16, u16),
+
+    /// Controls the max concurrent connections per IpAddr.
+    #[arg(long, default_value_t = DEFAULT_MAX_QUIC_CONNECTIONS_PER_PEER)]
+    pub max_connections_per_peer: usize,
+
+    /// Controls the max concurrent connections for TPU from staked nodes.
+    #[arg(long, default_value_t = DEFAULT_MAX_STAKED_CONNECTIONS)]
+    pub max_tpu_staked_connections: usize,
+
+    /// Controls the max concurrent connections fort TPU from unstaked nodes.
+    #[arg(long, default_value_t = DEFAULT_MAX_UNSTAKED_CONNECTIONS)]
+    pub max_tpu_unstaked_connections: usize,
+
+    /// Controls the max concurrent connections for TPU-forward from staked nodes.
+    #[arg(long, default_value_t = DEFAULT_MAX_STAKED_CONNECTIONS)]
+    pub max_fwd_staked_connections: usize,
+
+    /// Controls the max concurrent connections for TPU-forward from unstaked nodes.
+    #[arg(long, default_value_t = 0)]
+    pub max_fwd_unstaked_connections: usize,
+
+    /// Controls the rate of the clients connections per IpAddr per minute.
+    #[arg(long, default_value_t = DEFAULT_MAX_CONNECTIONS_PER_IPADDR_PER_MINUTE)]
+    pub max_connections_per_ipaddr_per_minute: u64,
+
+    /// The number of QUIC endpoints used for TPU and TPU-Forward. It can be increased to
+    /// increase network ingest throughput, at the expense of higher CPU and general
+    /// validator load.
+    #[arg(long, default_value_t = DEFAULT_NUM_QUIC_ENDPOINTS)]
+    pub num_quic_endpoints: usize,
+
+    /// Max streams per second for a streamer.
+    #[arg(long, default_value_t = DEFAULT_MAX_STREAMS_PER_MS)]
+    pub max_streams_per_ms: u64,
+
+    /// Milliseconds to wait in the TPU receiver for packet coalescing.
+    #[arg(long, default_value = get_default_tpu_coalesce_ms())]
+    pub tpu_coalesce_ms: u64,
+
+    /// Redirect logging to the specified file, '-' for standard error. Sending the
+    /// SIGUSR1 signal to the vortexor process will cause it to re-open the log file.
+    #[arg(long="log", value_name = "FILE", value_parser = clap::value_parser!(String))]
+    pub logfile: Option<String>,
path: vortexor/src/cli.rs
user: login: lijunwangs
body: The default behavior is to open a auto-named file and write to it.
created_at: 2025-03-13T18:17:00Z
author_association: NONE
diff_hunk: @@ -56,139 +26,91 @@ fn port_range_validator(port_range: String) -> Result<(), String> {
         } else if end.checked_add(QUIC_PORT_OFFSET).is_none() {
             Err("Invalid dynamic_port_range.".to_string())
         } else {
-            Ok(())
+            Ok((start, end))
         }
     } else {
         Err("Invalid port range".to_string())
     }
 }
 
-pub fn app<'a>(version: &'a str, default_args: &'a DefaultArgs) -> App<'a, 'a> {
-    App::new(crate_name!())
-        .about(crate_description!())
-        .version(version)
-        .global_setting(AppSettings::ColoredHelp)
-        .global_setting(AppSettings::InferSubcommands)
-        .global_setting(AppSettings::UnifiedHelpMessage)
-        .global_setting(AppSettings::VersionlessSubcommands)
-        .arg(
-            Arg::with_name("identity")
-                .short("i")
-                .long("identity")
-                .value_name("KEYPAIR")
-                .takes_value(true)
-                .validator(is_keypair_or_ask_keyword)
-                .help("Vortexor identity keypair"),
-        )
-        .arg(
-            Arg::with_name("bind_address")
-                .long("bind-address")
-                .value_name("HOST")
-                .takes_value(true)
-                .validator(solana_net_utils::is_host)
-                .default_value(&default_args.bind_address)
-                .help("IP address to bind the validator ports"),
-        )
-        .arg(
-            Arg::with_name("dynamic_port_range")
-                .long("dynamic-port-range")
-                .value_name("MIN_PORT-MAX_PORT")
-                .takes_value(true)
-                .default_value(&default_args.dynamic_port_range)
-                .validator(port_range_validator)
-                .help("Range to use for dynamically assigned ports"),
-        )
-        .arg(
-            Arg::with_name("max_connections_per_peer")
-                .long("max-connections-per-peer")
-                .takes_value(true)
-                .default_value(&default_args.max_connections_per_peer)
-                .validator(is_parsable::<u32>)
-                .help("Controls the max concurrent connections per IpAddr."),
-        )
-        .arg(
-            Arg::with_name("max_tpu_staked_connections")
-                .long("max-tpu-staked-connections")
-                .takes_value(true)
-                .default_value(&default_args.max_tpu_staked_connections)
-                .validator(is_parsable::<u32>)
-                .help("Controls the max concurrent connections for TPU from staked nodes."),
-        )
-        .arg(
-            Arg::with_name("max_tpu_unstaked_connections")
-                .long("max-tpu-unstaked-connections")
-                .takes_value(true)
-                .default_value(&default_args.max_tpu_unstaked_connections)
-                .validator(is_parsable::<u32>)
-                .help("Controls the max concurrent connections fort TPU from unstaked nodes."),
-        )
-        .arg(
-            Arg::with_name("max_fwd_staked_connections")
-                .long("max-fwd-staked-connections")
-                .takes_value(true)
-                .default_value(&default_args.max_fwd_staked_connections)
-                .validator(is_parsable::<u32>)
-                .help("Controls the max concurrent connections for TPU-forward from staked nodes."),
-        )
-        .arg(
-            Arg::with_name("max_fwd_unstaked_connections")
-                .long("max-fwd-unstaked-connections")
-                .takes_value(true)
-                .default_value(&default_args.max_fwd_unstaked_connections)
-                .validator(is_parsable::<u32>)
-                .help("Controls the max concurrent connections for TPU-forward from unstaked nodes."),
-        )
-        .arg(
-            Arg::with_name("max_connections_per_ipaddr_per_minute")
-                .long("max-connections-per-ipaddr-per-minute")
-                .takes_value(true)
-                .default_value(&default_args.max_connections_per_ipaddr_per_min)
-                .validator(is_parsable::<u32>)
-                .help("Controls the rate of the clients connections per IpAddr per minute."),
-        )
-        .arg(
-            Arg::with_name("num_quic_endpoints")
-                .long("num-quic-endpoints")
-                .takes_value(true)
-                .default_value(&default_args.num_quic_endpoints)
-                .validator(is_parsable::<usize>)
-                .help("The number of QUIC endpoints used for TPU and TPU-Forward. It can be increased to \
-                       increase network ingest throughput, at the expense of higher CPU and general \
-                       validator load."),
-        )
-        .arg(
-            Arg::with_name("max_streams_per_ms")
-                .long("max-streams-per-ms")
-                .takes_value(true)
-                .default_value(&default_args.max_streams_per_ms)
-                .validator(is_parsable::<usize>)
-                .help("Max streams per second for a streamer."),
-        )
-        .arg(
-            Arg::with_name("tpu_coalesce_ms")
-                .long("tpu-coalesce-ms")
-                .value_name("MILLISECS")
-                .takes_value(true)
-                .validator(is_parsable::<u64>)
-                .help("Milliseconds to wait in the TPU receiver for packet coalescing."),
-        )
-        .arg(
-            Arg::with_name("logfile")
-                .long("log")
-                .value_name("FILE")
-                .takes_value(true)
-                .help(
-                    "Redirect logging to the specified file, '-' for standard error. Sending the \
-                     SIGUSR1 signal to the vortexor process will cause it to re-open the log file.",
-                ),
-        )
-        .arg(
-            Arg::with_name("destination")
-                .long("destination")
-                .value_name("HOST:PORT")
-                .takes_value(true)
-                .multiple(true)
-                .validator(solana_net_utils::is_host_port)
-                .help("The destination validator address to which the vortexor will forward transactions."),
-        )
+fn get_version() -> &'static str {
+    let version = solana_version::version!();
+    let version_static: &'static str = Box::leak(version.to_string().into_boxed_str());
+    version_static
+}
+
+fn get_default_port_range() -> &'static str {
+    let range = format!("{}-{}", VALIDATOR_PORT_RANGE.0, VALIDATOR_PORT_RANGE.1);
+    let range: &'static str = Box::leak(range.into_boxed_str());
+    range
+}
+
+fn get_default_tpu_coalesce_ms() -> &'static str {
+    let coalesce = DEFAULT_TPU_COALESCE.as_millis().to_string();
+    let coalesce: &'static str = Box::leak(coalesce.into_boxed_str());
+    coalesce
+}
+
+#[derive(Parser)]
+#[command(name=crate_name!(),version=get_version(), about=crate_description!(),
+    long_about = None, color=ColorChoice::Auto)]
+pub struct Cli {
+    /// Vortexor identity keypair
+    #[arg(long, value_name = "KEYPAIR")]
+    pub identity: PathBuf,
+
+    /// IP address to bind the vortexor ports
+    #[arg(long, value_parser = solana_net_utils::parse_host, default_value = "0.0.0.0", value_name = "HOST")]
+    pub bind_address: IpAddr,
+
+    /// The destination validator address to which the vortexor will forward transactions.
+    #[arg(long, value_parser = solana_net_utils::parse_host_port, value_name = "HOST:PORT", action = ArgAction::Append)]
+    pub destination: Vec<SocketAddr>,
path: vortexor/src/cli.rs
user: login: lijunwangs
body: will try
created_at: 2025-03-13T18:17:14Z
author_association: NONE
diff_hunk: @@ -56,139 +26,91 @@ fn port_range_validator(port_range: String) -> Result<(), String> {
         } else if end.checked_add(QUIC_PORT_OFFSET).is_none() {
             Err("Invalid dynamic_port_range.".to_string())
         } else {
-            Ok(())
+            Ok((start, end))
         }
     } else {
         Err("Invalid port range".to_string())
     }
 }
 
-pub fn app<'a>(version: &'a str, default_args: &'a DefaultArgs) -> App<'a, 'a> {
-    App::new(crate_name!())
-        .about(crate_description!())
-        .version(version)
-        .global_setting(AppSettings::ColoredHelp)
-        .global_setting(AppSettings::InferSubcommands)
-        .global_setting(AppSettings::UnifiedHelpMessage)
-        .global_setting(AppSettings::VersionlessSubcommands)
-        .arg(
-            Arg::with_name("identity")
-                .short("i")
-                .long("identity")
-                .value_name("KEYPAIR")
-                .takes_value(true)
-                .validator(is_keypair_or_ask_keyword)
-                .help("Vortexor identity keypair"),
-        )
-        .arg(
-            Arg::with_name("bind_address")
-                .long("bind-address")
-                .value_name("HOST")
-                .takes_value(true)
-                .validator(solana_net_utils::is_host)
-                .default_value(&default_args.bind_address)
-                .help("IP address to bind the validator ports"),
-        )
-        .arg(
-            Arg::with_name("dynamic_port_range")
-                .long("dynamic-port-range")
-                .value_name("MIN_PORT-MAX_PORT")
-                .takes_value(true)
-                .default_value(&default_args.dynamic_port_range)
-                .validator(port_range_validator)
-                .help("Range to use for dynamically assigned ports"),
-        )
-        .arg(
-            Arg::with_name("max_connections_per_peer")
-                .long("max-connections-per-peer")
-                .takes_value(true)
-                .default_value(&default_args.max_connections_per_peer)
-                .validator(is_parsable::<u32>)
-                .help("Controls the max concurrent connections per IpAddr."),
-        )
-        .arg(
-            Arg::with_name("max_tpu_staked_connections")
-                .long("max-tpu-staked-connections")
-                .takes_value(true)
-                .default_value(&default_args.max_tpu_staked_connections)
-                .validator(is_parsable::<u32>)
-                .help("Controls the max concurrent connections for TPU from staked nodes."),
-        )
-        .arg(
-            Arg::with_name("max_tpu_unstaked_connections")
-                .long("max-tpu-unstaked-connections")
-                .takes_value(true)
-                .default_value(&default_args.max_tpu_unstaked_connections)
-                .validator(is_parsable::<u32>)
-                .help("Controls the max concurrent connections fort TPU from unstaked nodes."),
-        )
-        .arg(
-            Arg::with_name("max_fwd_staked_connections")
-                .long("max-fwd-staked-connections")
-                .takes_value(true)
-                .default_value(&default_args.max_fwd_staked_connections)
-                .validator(is_parsable::<u32>)
-                .help("Controls the max concurrent connections for TPU-forward from staked nodes."),
-        )
-        .arg(
-            Arg::with_name("max_fwd_unstaked_connections")
-                .long("max-fwd-unstaked-connections")
-                .takes_value(true)
-                .default_value(&default_args.max_fwd_unstaked_connections)
-                .validator(is_parsable::<u32>)
-                .help("Controls the max concurrent connections for TPU-forward from unstaked nodes."),
-        )
-        .arg(
-            Arg::with_name("max_connections_per_ipaddr_per_minute")
-                .long("max-connections-per-ipaddr-per-minute")
-                .takes_value(true)
-                .default_value(&default_args.max_connections_per_ipaddr_per_min)
-                .validator(is_parsable::<u32>)
-                .help("Controls the rate of the clients connections per IpAddr per minute."),
-        )
-        .arg(
-            Arg::with_name("num_quic_endpoints")
-                .long("num-quic-endpoints")
-                .takes_value(true)
-                .default_value(&default_args.num_quic_endpoints)
-                .validator(is_parsable::<usize>)
-                .help("The number of QUIC endpoints used for TPU and TPU-Forward. It can be increased to \
-                       increase network ingest throughput, at the expense of higher CPU and general \
-                       validator load."),
-        )
-        .arg(
-            Arg::with_name("max_streams_per_ms")
-                .long("max-streams-per-ms")
-                .takes_value(true)
-                .default_value(&default_args.max_streams_per_ms)
-                .validator(is_parsable::<usize>)
-                .help("Max streams per second for a streamer."),
-        )
-        .arg(
-            Arg::with_name("tpu_coalesce_ms")
-                .long("tpu-coalesce-ms")
-                .value_name("MILLISECS")
-                .takes_value(true)
-                .validator(is_parsable::<u64>)
-                .help("Milliseconds to wait in the TPU receiver for packet coalescing."),
-        )
-        .arg(
-            Arg::with_name("logfile")
-                .long("log")
-                .value_name("FILE")
-                .takes_value(true)
-                .help(
-                    "Redirect logging to the specified file, '-' for standard error. Sending the \
-                     SIGUSR1 signal to the vortexor process will cause it to re-open the log file.",
-                ),
-        )
-        .arg(
-            Arg::with_name("destination")
-                .long("destination")
-                .value_name("HOST:PORT")
-                .takes_value(true)
-                .multiple(true)
-                .validator(solana_net_utils::is_host_port)
-                .help("The destination validator address to which the vortexor will forward transactions."),
-        )
+fn get_version() -> &'static str {
+    let version = solana_version::version!();
+    let version_static: &'static str = Box::leak(version.to_string().into_boxed_str());
+    version_static
+}
+
+fn get_default_port_range() -> &'static str {
+    let range = format!("{}-{}", VALIDATOR_PORT_RANGE.0, VALIDATOR_PORT_RANGE.1);
+    let range: &'static str = Box::leak(range.into_boxed_str());
+    range
+}
+
+fn get_default_tpu_coalesce_ms() -> &'static str {
+    let coalesce = DEFAULT_TPU_COALESCE.as_millis().to_string();
+    let coalesce: &'static str = Box::leak(coalesce.into_boxed_str());
+    coalesce
+}
+
+#[derive(Parser)]
+#[command(name=crate_name!(),version=get_version(), about=crate_description!(),
+    long_about = None, color=ColorChoice::Auto)]
+pub struct Cli {
+    /// Vortexor identity keypair
+    #[arg(long, value_name = "KEYPAIR")]
+    pub identity: PathBuf,
+
+    /// IP address to bind the vortexor ports
+    #[arg(long, value_parser = solana_net_utils::parse_host, default_value = "0.0.0.0", value_name = "HOST")]
+    pub bind_address: IpAddr,
+
+    /// The destination validator address to which the vortexor will forward transactions.
+    #[arg(long, value_parser = solana_net_utils::parse_host_port, value_name = "HOST:PORT", action = ArgAction::Append)]
+    pub destination: Vec<SocketAddr>,
+
+    /// Range to use for dynamically assigned ports
+    #[arg(long, value_parser = parse_port_range, value_name = "MIN_PORT-MAX_PORT", default_value = get_default_port_range())]
+    pub dynamic_port_range: (u16, u16),
+
+    /// Controls the max concurrent connections per IpAddr.
+    #[arg(long, default_value_t = DEFAULT_MAX_QUIC_CONNECTIONS_PER_PEER)]
+    pub max_connections_per_peer: usize,
+
+    /// Controls the max concurrent connections for TPU from staked nodes.
+    #[arg(long, default_value_t = DEFAULT_MAX_STAKED_CONNECTIONS)]
+    pub max_tpu_staked_connections: usize,
+
+    /// Controls the max concurrent connections fort TPU from unstaked nodes.
+    #[arg(long, default_value_t = DEFAULT_MAX_UNSTAKED_CONNECTIONS)]
+    pub max_tpu_unstaked_connections: usize,
+
+    /// Controls the max concurrent connections for TPU-forward from staked nodes.
+    #[arg(long, default_value_t = DEFAULT_MAX_STAKED_CONNECTIONS)]
+    pub max_fwd_staked_connections: usize,
+
+    /// Controls the max concurrent connections for TPU-forward from unstaked nodes.
+    #[arg(long, default_value_t = 0)]
+    pub max_fwd_unstaked_connections: usize,
+
+    /// Controls the rate of the clients connections per IpAddr per minute.
+    #[arg(long, default_value_t = DEFAULT_MAX_CONNECTIONS_PER_IPADDR_PER_MINUTE)]
+    pub max_connections_per_ipaddr_per_minute: u64,
+
+    /// The number of QUIC endpoints used for TPU and TPU-Forward. It can be increased to
+    /// increase network ingest throughput, at the expense of higher CPU and general
+    /// validator load.
+    #[arg(long, default_value_t = DEFAULT_NUM_QUIC_ENDPOINTS)]
+    pub num_quic_endpoints: usize,
+
+    /// Max streams per second for a streamer.
+    #[arg(long, default_value_t = DEFAULT_MAX_STREAMS_PER_MS)]
+    pub max_streams_per_ms: u64,
+
+    /// Milliseconds to wait in the TPU receiver for packet coalescing.
+    #[arg(long, default_value = get_default_tpu_coalesce_ms())]
+    pub tpu_coalesce_ms: u64,
+
+    /// Redirect logging to the specified file, '-' for standard error. Sending the
+    /// SIGUSR1 signal to the vortexor process will cause it to re-open the log file.
+    #[arg(long="log", value_name = "FILE", value_parser = clap::value_parser!(String))]
+    pub logfile: Option<String>,
path: vortexor/src/cli.rs
user: login: alexpyattaev
body: It does not matter much but in general PathBuf is preferred for file names. But in our case it does not matter.
created_at: 2025-03-14T06:42:31Z
author_association: NONE

PR comments:
user: login: alexpyattaev
created_at: 2025-03-08T06:59:30Z
body: Why are you not using the derive macros? They make code a lot more clean and compact.
author_association: NONE
user: login: lijunwangs
created_at: 2025-03-09T17:54:14Z
body: > Why are you not using the derive macros? They make code a lot more clean and compact.

will look
author_association: NONE
user: login: alexpyattaev
created_at: 2025-03-11T21:12:57Z
body: This is moving in the right direction! Removing unnecessary annotations (required=true, nargs=1) from the definitions will make the whole thing even shorter then it is now.
author_association: NONE