solana_clap_v3_utils/input_parsers/
mod.rs

1use {
2    crate::{
3        input_validators::normalize_to_url_if_moniker,
4        keypair::{keypair_from_seed_phrase, ASK_KEYWORD, SKIP_SEED_PHRASE_VALIDATION_ARG},
5    },
6    chrono::DateTime,
7    clap::ArgMatches,
8    solana_clock::UnixTimestamp,
9    solana_cluster_type::ClusterType,
10    solana_commitment_config::CommitmentConfig,
11    solana_keypair::{read_keypair_file, Keypair},
12    solana_native_token::sol_str_to_lamports,
13    solana_pubkey::{Pubkey, MAX_SEED_LEN},
14    solana_signer::Signer,
15    std::str::FromStr,
16};
17
18pub mod signer;
19#[deprecated(
20    since = "1.17.0",
21    note = "Please use the functions in `solana_clap_v3_utils::input_parsers::signer` directly instead"
22)]
23#[allow(deprecated)]
24pub use signer::{
25    pubkey_of_signer, pubkeys_of_multiple_signers, pubkeys_sigs_of, resolve_signer, signer_of,
26    STDOUT_OUTFILE_TOKEN,
27};
28
29// Return parsed values from matches at `name`
30#[deprecated(
31    since = "2.0.0",
32    note = "Please use the functions `ArgMatches::get_many` or `ArgMatches::try_get_many` instead"
33)]
34#[allow(deprecated)]
35pub fn values_of<T>(matches: &ArgMatches, name: &str) -> Option<Vec<T>>
36where
37    T: std::str::FromStr,
38    <T as std::str::FromStr>::Err: std::fmt::Debug,
39{
40    matches
41        .values_of(name)
42        .map(|xs| xs.map(|x| x.parse::<T>().unwrap()).collect())
43}
44
45// Return a parsed value from matches at `name`
46#[deprecated(
47    since = "2.0.0",
48    note = "Please use the functions `ArgMatches::get_one` or `ArgMatches::try_get_one` instead"
49)]
50#[allow(deprecated)]
51pub fn value_of<T>(matches: &ArgMatches, name: &str) -> Option<T>
52where
53    T: std::str::FromStr,
54    <T as std::str::FromStr>::Err: std::fmt::Debug,
55{
56    matches
57        .value_of(name)
58        .and_then(|value| value.parse::<T>().ok())
59}
60
61#[deprecated(
62    since = "2.0.0",
63    note = "Please use `ArgMatches::get_one::<UnixTimestamp>(...)` instead"
64)]
65#[allow(deprecated)]
66pub fn unix_timestamp_from_rfc3339_datetime(
67    matches: &ArgMatches,
68    name: &str,
69) -> Option<UnixTimestamp> {
70    matches.value_of(name).and_then(|value| {
71        DateTime::parse_from_rfc3339(value)
72            .ok()
73            .map(|date_time| date_time.timestamp())
74    })
75}
76
77#[deprecated(
78    since = "1.17.0",
79    note = "please use `Amount::parse_decimal` and `Amount::sol_to_lamport` instead"
80)]
81#[allow(deprecated)]
82pub fn lamports_of_sol(matches: &ArgMatches, name: &str) -> Option<u64> {
83    matches.value_of(name).and_then(sol_str_to_lamports)
84}
85
86#[deprecated(
87    since = "2.0.0",
88    note = "Please use `ArgMatches::get_one::<ClusterType>(...)` instead"
89)]
90#[allow(deprecated)]
91pub fn cluster_type_of(matches: &ArgMatches, name: &str) -> Option<ClusterType> {
92    value_of(matches, name)
93}
94
95#[deprecated(
96    since = "2.0.0",
97    note = "Please use `ArgMatches::get_one::<CommitmentConfig>(...)` instead"
98)]
99#[allow(deprecated)]
100pub fn commitment_of(matches: &ArgMatches, name: &str) -> Option<CommitmentConfig> {
101    matches
102        .value_of(name)
103        .map(|value| CommitmentConfig::from_str(value).unwrap_or_default())
104}
105
106pub fn parse_url(arg: &str) -> Result<String, String> {
107    url::Url::parse(arg)
108        .map_err(|err| err.to_string())
109        .and_then(|url| {
110            url.has_host()
111                .then_some(arg.to_string())
112                .ok_or("no host provided".to_string())
113        })
114}
115
116pub fn parse_url_or_moniker(arg: &str) -> Result<String, String> {
117    parse_url(&normalize_to_url_if_moniker(arg))
118}
119
120pub fn parse_pow2(arg: &str) -> Result<usize, String> {
121    arg.parse::<usize>()
122        .map_err(|e| format!("Unable to parse, provided: {arg}, err: {e}"))
123        .and_then(|v| {
124            v.is_power_of_two()
125                .then_some(v)
126                .ok_or(format!("Must be a power of 2: {v}"))
127        })
128}
129
130pub fn parse_percentage(arg: &str) -> Result<u8, String> {
131    arg.parse::<u8>()
132        .map_err(|e| format!("Unable to parse input percentage, provided: {arg}, err: {e}"))
133        .and_then(|v| {
134            (v <= 100).then_some(v).ok_or(format!(
135                "Percentage must be in range of 0 to 100, provided: {v}"
136            ))
137        })
138}
139
140#[derive(Clone, Copy, Debug, PartialEq)]
141pub enum Amount {
142    Decimal(f64),
143    Raw(u64),
144    All,
145}
146impl Amount {
147    pub fn parse(arg: &str) -> Result<Amount, String> {
148        if arg == "ALL" {
149            Ok(Amount::All)
150        } else {
151            Self::parse_decimal(arg).or(Self::parse_raw(arg)
152                .map_err(|_| format!("Unable to parse input amount, provided: {arg}")))
153        }
154    }
155
156    pub fn parse_decimal(arg: &str) -> Result<Amount, String> {
157        arg.parse::<f64>()
158            .map(Amount::Decimal)
159            .map_err(|_| format!("Unable to parse input amount, provided: {arg}"))
160    }
161
162    pub fn parse_raw(arg: &str) -> Result<Amount, String> {
163        arg.parse::<u64>()
164            .map(Amount::Raw)
165            .map_err(|_| format!("Unable to parse input amount, provided: {arg}"))
166    }
167
168    pub fn parse_decimal_or_all(arg: &str) -> Result<Amount, String> {
169        if arg == "ALL" {
170            Ok(Amount::All)
171        } else {
172            Self::parse_decimal(arg).map_err(|_| {
173                format!("Unable to parse input amount as float or 'ALL' keyword, provided: {arg}")
174            })
175        }
176    }
177
178    pub fn to_raw_amount(&self, decimals: u8) -> Self {
179        match self {
180            Amount::Decimal(amount) => {
181                Amount::Raw((amount * 10_usize.pow(decimals as u32) as f64) as u64)
182            }
183            Amount::Raw(amount) => Amount::Raw(*amount),
184            Amount::All => Amount::All,
185        }
186    }
187
188    pub fn sol_to_lamport(&self) -> Amount {
189        const NATIVE_SOL_DECIMALS: u8 = 9;
190        self.to_raw_amount(NATIVE_SOL_DECIMALS)
191    }
192}
193
194#[derive(Clone, Copy, Debug, PartialEq)]
195pub enum RawTokenAmount {
196    Amount(u64),
197    All,
198}
199
200pub fn parse_rfc3339_datetime(arg: &str) -> Result<String, String> {
201    DateTime::parse_from_rfc3339(arg)
202        .map(|_| arg.to_string())
203        .map_err(|e| format!("{e}"))
204}
205
206pub fn parse_derivation(arg: &str) -> Result<String, String> {
207    let value = arg.replace('\'', "");
208    let mut parts = value.split('/');
209    let account = parts.next().unwrap();
210    account
211        .parse::<u32>()
212        .map_err(|e| format!("Unable to parse derivation, provided: {account}, err: {e}"))
213        .and_then(|_| {
214            if let Some(change) = parts.next() {
215                change.parse::<u32>().map_err(|e| {
216                    format!("Unable to parse derivation, provided: {change}, err: {e}")
217                })
218            } else {
219                Ok(0)
220            }
221        })?;
222    Ok(arg.to_string())
223}
224
225pub fn parse_structured_seed(arg: &str) -> Result<String, String> {
226    let (prefix, value) = arg
227        .split_once(':')
228        .ok_or("Seed must contain ':' as delimiter")
229        .unwrap();
230    if prefix.is_empty() || value.is_empty() {
231        Err(String::from("Seed prefix or value is empty"))
232    } else {
233        match prefix {
234            "string" | "pubkey" | "hex" | "u8" => Ok(arg.to_string()),
235            _ => {
236                let len = prefix.len();
237                if len != 5 && len != 6 {
238                    Err(format!("Wrong prefix length {len} {prefix}:{value}"))
239                } else {
240                    let sign = &prefix[0..1];
241                    let type_size = &prefix[1..len.saturating_sub(2)];
242                    let byte_order = &prefix[len.saturating_sub(2)..len];
243                    if sign != "u" && sign != "i" {
244                        Err(format!("Wrong prefix sign {sign} {prefix}:{value}"))
245                    } else if type_size != "16"
246                        && type_size != "32"
247                        && type_size != "64"
248                        && type_size != "128"
249                    {
250                        Err(format!(
251                            "Wrong prefix type size {type_size} {prefix}:{value}"
252                        ))
253                    } else if byte_order != "le" && byte_order != "be" {
254                        Err(format!(
255                            "Wrong prefix byte order {byte_order} {prefix}:{value}"
256                        ))
257                    } else {
258                        Ok(arg.to_string())
259                    }
260                }
261            }
262        }
263    }
264}
265
266pub fn parse_derived_address_seed(arg: &str) -> Result<String, String> {
267    (arg.len() <= MAX_SEED_LEN)
268        .then_some(arg.to_string())
269        .ok_or(format!(
270            "Address seed must not be longer than {MAX_SEED_LEN} bytes"
271        ))
272}
273
274// Return the keypair for an argument with filename `name` or None if not present.
275#[deprecated(
276    since = "2.0.0",
277    note = "Please use `input_parsers::signer::try_keypair_of` instead"
278)]
279#[allow(deprecated)]
280pub fn keypair_of(matches: &ArgMatches, name: &str) -> Option<Keypair> {
281    if let Some(value) = matches.value_of(name) {
282        if value == ASK_KEYWORD {
283            let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name);
284            keypair_from_seed_phrase(name, skip_validation, true, None, true).ok()
285        } else {
286            read_keypair_file(value).ok()
287        }
288    } else {
289        None
290    }
291}
292
293#[deprecated(
294    since = "2.0.0",
295    note = "Please use `input_parsers::signer::try_keypairs_of` instead"
296)]
297#[allow(deprecated)]
298pub fn keypairs_of(matches: &ArgMatches, name: &str) -> Option<Vec<Keypair>> {
299    matches.values_of(name).map(|values| {
300        values
301            .filter_map(|value| {
302                if value == ASK_KEYWORD {
303                    let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name);
304                    keypair_from_seed_phrase(name, skip_validation, true, None, true).ok()
305                } else {
306                    read_keypair_file(value).ok()
307                }
308            })
309            .collect()
310    })
311}
312
313// Return a pubkey for an argument that can itself be parsed into a pubkey,
314// or is a filename that can be read as a keypair
315#[deprecated(
316    since = "2.0.0",
317    note = "Please use `input_parsers::signer::try_pubkey_of` instead"
318)]
319#[allow(deprecated)]
320pub fn pubkey_of(matches: &ArgMatches, name: &str) -> Option<Pubkey> {
321    value_of(matches, name).or_else(|| keypair_of(matches, name).map(|keypair| keypair.pubkey()))
322}
323
324#[deprecated(
325    since = "2.0.0",
326    note = "Please use `input_parsers::signer::try_pubkeys_of` instead"
327)]
328#[allow(deprecated)]
329pub fn pubkeys_of(matches: &ArgMatches, name: &str) -> Option<Vec<Pubkey>> {
330    matches.values_of(name).map(|values| {
331        values
332            .map(|value| {
333                value.parse::<Pubkey>().unwrap_or_else(|_| {
334                    read_keypair_file(value)
335                        .expect("read_keypair_file failed")
336                        .pubkey()
337                })
338            })
339            .collect()
340    })
341}
342
343#[allow(deprecated)]
344#[cfg(test)]
345mod tests {
346    use {
347        super::*,
348        clap::{Arg, ArgAction, Command},
349        solana_commitment_config::{CommitmentConfig, CommitmentLevel},
350        solana_hash::Hash,
351        solana_pubkey::Pubkey,
352    };
353
354    fn app<'ab>() -> Command<'ab> {
355        Command::new("test")
356            .arg(
357                Arg::new("multiple")
358                    .long("multiple")
359                    .takes_value(true)
360                    .action(ArgAction::Append)
361                    .multiple_values(true),
362            )
363            .arg(Arg::new("single").takes_value(true).long("single"))
364            .arg(Arg::new("unit").takes_value(true).long("unit"))
365    }
366
367    #[test]
368    fn test_values_of() {
369        let matches = app().get_matches_from(vec!["test", "--multiple", "50", "--multiple", "39"]);
370        assert_eq!(values_of(&matches, "multiple"), Some(vec![50, 39]));
371        assert_eq!(values_of::<u64>(&matches, "single"), None);
372
373        let pubkey0 = solana_pubkey::new_rand();
374        let pubkey1 = solana_pubkey::new_rand();
375        let matches = app().get_matches_from(vec![
376            "test",
377            "--multiple",
378            &pubkey0.to_string(),
379            "--multiple",
380            &pubkey1.to_string(),
381        ]);
382        assert_eq!(
383            values_of(&matches, "multiple"),
384            Some(vec![pubkey0, pubkey1])
385        );
386    }
387
388    #[test]
389    fn test_value_of() {
390        let matches = app().get_matches_from(vec!["test", "--single", "50"]);
391        assert_eq!(value_of(&matches, "single"), Some(50));
392        assert_eq!(value_of::<u64>(&matches, "multiple"), None);
393
394        let pubkey = solana_pubkey::new_rand();
395        let matches = app().get_matches_from(vec!["test", "--single", &pubkey.to_string()]);
396        assert_eq!(value_of(&matches, "single"), Some(pubkey));
397    }
398
399    #[test]
400    fn test_parse_pubkey() {
401        let command = Command::new("test").arg(
402            Arg::new("pubkey")
403                .long("pubkey")
404                .takes_value(true)
405                .value_parser(clap::value_parser!(Pubkey)),
406        );
407
408        // success case
409        let matches = command
410            .clone()
411            .try_get_matches_from(vec!["test", "--pubkey", "11111111111111111111111111111111"])
412            .unwrap();
413        assert_eq!(
414            *matches.get_one::<Pubkey>("pubkey").unwrap(),
415            Pubkey::from_str("11111111111111111111111111111111").unwrap(),
416        );
417
418        // validation fails
419        let matches_error = command
420            .clone()
421            .try_get_matches_from(vec!["test", "--pubkey", "this_is_an_invalid_arg"])
422            .unwrap_err();
423        assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation);
424    }
425
426    #[test]
427    fn test_parse_hash() {
428        let command = Command::new("test").arg(
429            Arg::new("hash")
430                .long("hash")
431                .takes_value(true)
432                .value_parser(clap::value_parser!(Hash)),
433        );
434
435        // success case
436        let matches = command
437            .clone()
438            .try_get_matches_from(vec!["test", "--hash", "11111111111111111111111111111111"])
439            .unwrap();
440        assert_eq!(
441            *matches.get_one::<Hash>("hash").unwrap(),
442            Hash::from_str("11111111111111111111111111111111").unwrap(),
443        );
444
445        // validation fails
446        let matches_error = command
447            .clone()
448            .try_get_matches_from(vec!["test", "--hash", "this_is_an_invalid_arg"])
449            .unwrap_err();
450        assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation);
451    }
452
453    #[test]
454    fn test_parse_token_decimal() {
455        let command = Command::new("test").arg(
456            Arg::new("amount")
457                .long("amount")
458                .takes_value(true)
459                .value_parser(Amount::parse_decimal),
460        );
461
462        // success cases
463        let matches = command
464            .clone()
465            .try_get_matches_from(vec!["test", "--amount", "11223344"])
466            .unwrap();
467        assert_eq!(
468            *matches.get_one::<Amount>("amount").unwrap(),
469            Amount::Decimal(11223344_f64),
470        );
471
472        let matches = command
473            .clone()
474            .try_get_matches_from(vec!["test", "--amount", "0.11223344"])
475            .unwrap();
476        assert_eq!(
477            *matches.get_one::<Amount>("amount").unwrap(),
478            Amount::Decimal(0.11223344),
479        );
480
481        // validation fail cases
482        let matches_error = command
483            .clone()
484            .try_get_matches_from(vec!["test", "--amount", "this_is_an_invalid_arg"])
485            .unwrap_err();
486        assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation);
487
488        let matches_error = command
489            .clone()
490            .try_get_matches_from(vec!["test", "--amount", "all"])
491            .unwrap_err();
492        assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation);
493    }
494
495    #[test]
496    fn test_parse_token_decimal_or_all() {
497        let command = Command::new("test").arg(
498            Arg::new("amount")
499                .long("amount")
500                .takes_value(true)
501                .value_parser(Amount::parse_decimal_or_all),
502        );
503
504        // success cases
505        let matches = command
506            .clone()
507            .try_get_matches_from(vec!["test", "--amount", "11223344"])
508            .unwrap();
509        assert_eq!(
510            *matches.get_one::<Amount>("amount").unwrap(),
511            Amount::Decimal(11223344_f64),
512        );
513
514        let matches = command
515            .clone()
516            .try_get_matches_from(vec!["test", "--amount", "0.11223344"])
517            .unwrap();
518        assert_eq!(
519            *matches.get_one::<Amount>("amount").unwrap(),
520            Amount::Decimal(0.11223344),
521        );
522
523        let matches = command
524            .clone()
525            .try_get_matches_from(vec!["test", "--amount", "ALL"])
526            .unwrap();
527        assert_eq!(*matches.get_one::<Amount>("amount").unwrap(), Amount::All,);
528
529        // validation fail cases
530        let matches_error = command
531            .clone()
532            .try_get_matches_from(vec!["test", "--amount", "this_is_an_invalid_arg"])
533            .unwrap_err();
534        assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation);
535    }
536
537    #[test]
538    fn test_sol_to_lamports() {
539        let command = Command::new("test").arg(
540            Arg::new("amount")
541                .long("amount")
542                .takes_value(true)
543                .value_parser(Amount::parse_decimal_or_all),
544        );
545
546        let test_cases = vec![
547            ("50", 50_000_000_000),
548            ("1.5", 1_500_000_000),
549            ("0.03", 30_000_000),
550        ];
551
552        for (arg, expected_lamport) in test_cases {
553            let matches = command
554                .clone()
555                .try_get_matches_from(vec!["test", "--amount", arg])
556                .unwrap();
557            assert_eq!(
558                matches
559                    .get_one::<Amount>("amount")
560                    .unwrap()
561                    .sol_to_lamport(),
562                Amount::Raw(expected_lamport),
563            );
564        }
565    }
566
567    #[test]
568    fn test_derivation() {
569        let command = Command::new("test").arg(
570            Arg::new("derivation")
571                .long("derivation")
572                .takes_value(true)
573                .value_parser(parse_derivation),
574        );
575
576        let test_arguments = vec![
577            ("2", true),
578            ("0", true),
579            ("65537", true),
580            ("0/2", true),
581            ("a", false),
582            ("4294967296", false),
583            ("a/b", false),
584            ("0/4294967296", false),
585        ];
586
587        for (arg, should_accept) in test_arguments {
588            if should_accept {
589                let matches = command
590                    .clone()
591                    .try_get_matches_from(vec!["test", "--derivation", arg])
592                    .unwrap();
593                assert_eq!(matches.get_one::<String>("derivation").unwrap(), arg);
594            }
595        }
596    }
597
598    #[test]
599    fn test_unix_timestamp_from_rfc3339_datetime() {
600        let command = Command::new("test").arg(
601            Arg::new("timestamp")
602                .long("timestamp")
603                .takes_value(true)
604                .value_parser(clap::value_parser!(UnixTimestamp)),
605        );
606
607        // success case
608        let matches = command
609            .clone()
610            .try_get_matches_from(vec!["test", "--timestamp", "1234"])
611            .unwrap();
612        assert_eq!(
613            *matches.get_one::<UnixTimestamp>("timestamp").unwrap(),
614            1234,
615        );
616
617        // validation fails
618        let matches_error = command
619            .clone()
620            .try_get_matches_from(vec!["test", "--timestamp", "this_is_an_invalid_arg"])
621            .unwrap_err();
622        assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation);
623    }
624
625    #[test]
626    fn test_cluster_type() {
627        let command = Command::new("test").arg(
628            Arg::new("cluster")
629                .long("cluster")
630                .takes_value(true)
631                .value_parser(clap::value_parser!(ClusterType)),
632        );
633
634        // success case
635        let matches = command
636            .clone()
637            .try_get_matches_from(vec!["test", "--cluster", "testnet"])
638            .unwrap();
639        assert_eq!(
640            *matches.get_one::<ClusterType>("cluster").unwrap(),
641            ClusterType::Testnet
642        );
643
644        // validation fails
645        let matches_error = command
646            .clone()
647            .try_get_matches_from(vec!["test", "--cluster", "this_is_an_invalid_arg"])
648            .unwrap_err();
649        assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation);
650    }
651
652    #[test]
653    fn test_commitment_config() {
654        let command = Command::new("test").arg(
655            Arg::new("commitment")
656                .long("commitment")
657                .takes_value(true)
658                .value_parser(clap::value_parser!(CommitmentConfig)),
659        );
660
661        // success case
662        let matches = command
663            .clone()
664            .try_get_matches_from(vec!["test", "--commitment", "finalized"])
665            .unwrap();
666        assert_eq!(
667            *matches.get_one::<CommitmentConfig>("commitment").unwrap(),
668            CommitmentConfig {
669                commitment: CommitmentLevel::Finalized
670            },
671        );
672
673        // validation fails
674        let matches_error = command
675            .clone()
676            .try_get_matches_from(vec!["test", "--commitment", "this_is_an_invalid_arg"])
677            .unwrap_err();
678        assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation);
679    }
680}