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#[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#[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#[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#[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}