use { crate::{ input_validators::normalize_to_url_if_moniker, keypair::{keypair_from_seed_phrase, ASK_KEYWORD, SKIP_SEED_PHRASE_VALIDATION_ARG}, }, chrono::DateTime, clap::ArgMatches, solana_sdk::{ clock::UnixTimestamp, commitment_config::CommitmentConfig, genesis_config::ClusterType, native_token::sol_to_lamports, pubkey::{Pubkey, MAX_SEED_LEN}, signature::{read_keypair_file, Keypair, Signer}, }, std::str::FromStr, }; pub mod signer; #[deprecated( since = "1.17.0", note = "Please use the functions in `solana_clap_v3_utils::input_parsers::signer` directly instead" )] pub use signer::{ pubkey_of_signer, pubkeys_of_multiple_signers, pubkeys_sigs_of, resolve_signer, signer_of, STDOUT_OUTFILE_TOKEN, }; // Return parsed values from matches at `name` pub fn values_of(matches: &ArgMatches, name: &str) -> Option> where T: std::str::FromStr, ::Err: std::fmt::Debug, { matches .values_of(name) .map(|xs| xs.map(|x| x.parse::().unwrap()).collect()) } // Return a parsed value from matches at `name` pub fn value_of(matches: &ArgMatches, name: &str) -> Option where T: std::str::FromStr, ::Err: std::fmt::Debug, { matches .value_of(name) .and_then(|value| value.parse::().ok()) } pub fn unix_timestamp_from_rfc3339_datetime( matches: &ArgMatches, name: &str, ) -> Option { matches.value_of(name).and_then(|value| { DateTime::parse_from_rfc3339(value) .ok() .map(|date_time| date_time.timestamp()) }) } #[deprecated( since = "1.17.0", note = "please use `Amount::parse_decimal` and `Amount::sol_to_lamport` instead" )] pub fn lamports_of_sol(matches: &ArgMatches, name: &str) -> Option { value_of(matches, name).map(sol_to_lamports) } pub fn cluster_type_of(matches: &ArgMatches, name: &str) -> Option { value_of(matches, name) } pub fn commitment_of(matches: &ArgMatches, name: &str) -> Option { matches .value_of(name) .map(|value| CommitmentConfig::from_str(value).unwrap_or_default()) } pub fn parse_url(arg: &str) -> Result { url::Url::parse(arg) .map_err(|err| err.to_string()) .and_then(|url| { url.has_host() .then_some(arg.to_string()) .ok_or("no host provided".to_string()) }) } pub fn parse_url_or_moniker(arg: &str) -> Result { parse_url(&normalize_to_url_if_moniker(arg)) } pub fn parse_pow2(arg: &str) -> Result { arg.parse::() .map_err(|e| format!("Unable to parse, provided: {arg}, err: {e}")) .and_then(|v| { v.is_power_of_two() .then_some(v) .ok_or(format!("Must be a power of 2: {v}")) }) } pub fn parse_percentage(arg: &str) -> Result { arg.parse::() .map_err(|e| format!("Unable to parse input percentage, provided: {arg}, err: {e}")) .and_then(|v| { (v <= 100).then_some(v).ok_or(format!( "Percentage must be in range of 0 to 100, provided: {v}" )) }) } #[derive(Clone, Copy, Debug, PartialEq)] pub enum Amount { Decimal(f64), Raw(u64), All, } impl Amount { pub fn parse(arg: &str) -> Result { if arg == "ALL" { Ok(Amount::All) } else { Self::parse_decimal(arg).or(Self::parse_raw(arg) .map_err(|_| format!("Unable to parse input amount, provided: {arg}"))) } } pub fn parse_decimal(arg: &str) -> Result { arg.parse::() .map(Amount::Decimal) .map_err(|_| format!("Unable to parse input amount, provided: {arg}")) } pub fn parse_raw(arg: &str) -> Result { arg.parse::() .map(Amount::Raw) .map_err(|_| format!("Unable to parse input amount, provided: {arg}")) } pub fn parse_decimal_or_all(arg: &str) -> Result { if arg == "ALL" { Ok(Amount::All) } else { Self::parse_decimal(arg).map_err(|_| { format!("Unable to parse input amount as float or 'ALL' keyword, provided: {arg}") }) } } pub fn to_raw_amount(&self, decimals: u8) -> Self { match self { Amount::Decimal(amount) => { Amount::Raw((amount * 10_usize.pow(decimals as u32) as f64) as u64) } Amount::Raw(amount) => Amount::Raw(*amount), Amount::All => Amount::All, } } pub fn sol_to_lamport(&self) -> Amount { const NATIVE_SOL_DECIMALS: u8 = 9; self.to_raw_amount(NATIVE_SOL_DECIMALS) } } #[derive(Clone, Copy, Debug, PartialEq)] pub enum RawTokenAmount { Amount(u64), All, } pub fn parse_rfc3339_datetime(arg: &str) -> Result { DateTime::parse_from_rfc3339(arg) .map(|_| arg.to_string()) .map_err(|e| format!("{e}")) } pub fn parse_derivation(arg: &str) -> Result { let value = arg.replace('\'', ""); let mut parts = value.split('/'); let account = parts.next().unwrap(); account .parse::() .map_err(|e| format!("Unable to parse derivation, provided: {account}, err: {e}")) .and_then(|_| { if let Some(change) = parts.next() { change.parse::().map_err(|e| { format!("Unable to parse derivation, provided: {change}, err: {e}") }) } else { Ok(0) } })?; Ok(arg.to_string()) } pub fn parse_structured_seed(arg: &str) -> Result { let (prefix, value) = arg .split_once(':') .ok_or("Seed must contain ':' as delimiter") .unwrap(); if prefix.is_empty() || value.is_empty() { Err(String::from("Seed prefix or value is empty")) } else { match prefix { "string" | "pubkey" | "hex" | "u8" => Ok(arg.to_string()), _ => { let len = prefix.len(); if len != 5 && len != 6 { Err(format!("Wrong prefix length {len} {prefix}:{value}")) } else { let sign = &prefix[0..1]; let type_size = &prefix[1..len.saturating_sub(2)]; let byte_order = &prefix[len.saturating_sub(2)..len]; if sign != "u" && sign != "i" { Err(format!("Wrong prefix sign {sign} {prefix}:{value}")) } else if type_size != "16" && type_size != "32" && type_size != "64" && type_size != "128" { Err(format!( "Wrong prefix type size {type_size} {prefix}:{value}" )) } else if byte_order != "le" && byte_order != "be" { Err(format!( "Wrong prefix byte order {byte_order} {prefix}:{value}" )) } else { Ok(arg.to_string()) } } } } } } pub fn parse_derived_address_seed(arg: &str) -> Result { (arg.len() <= MAX_SEED_LEN) .then_some(arg.to_string()) .ok_or(format!( "Address seed must not be longer than {MAX_SEED_LEN} bytes" )) } // Return the keypair for an argument with filename `name` or None if not present. pub fn keypair_of(matches: &ArgMatches, name: &str) -> Option { if let Some(value) = matches.value_of(name) { if value == ASK_KEYWORD { let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name); keypair_from_seed_phrase(name, skip_validation, true, None, true).ok() } else { read_keypair_file(value).ok() } } else { None } } pub fn keypairs_of(matches: &ArgMatches, name: &str) -> Option> { matches.values_of(name).map(|values| { values .filter_map(|value| { if value == ASK_KEYWORD { let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name); keypair_from_seed_phrase(name, skip_validation, true, None, true).ok() } else { read_keypair_file(value).ok() } }) .collect() }) } // Return a pubkey for an argument that can itself be parsed into a pubkey, // or is a filename that can be read as a keypair pub fn pubkey_of(matches: &ArgMatches, name: &str) -> Option { value_of(matches, name).or_else(|| keypair_of(matches, name).map(|keypair| keypair.pubkey())) } pub fn pubkeys_of(matches: &ArgMatches, name: &str) -> Option> { matches.values_of(name).map(|values| { values .map(|value| { value.parse::().unwrap_or_else(|_| { read_keypair_file(value) .expect("read_keypair_file failed") .pubkey() }) }) .collect() }) } #[cfg(test)] mod tests { use { super::*, clap::{Arg, Command}, solana_sdk::{hash::Hash, pubkey::Pubkey}, }; fn app<'ab>() -> Command<'ab> { Command::new("test") .arg( Arg::new("multiple") .long("multiple") .takes_value(true) .multiple_occurrences(true) .multiple_values(true), ) .arg(Arg::new("single").takes_value(true).long("single")) .arg(Arg::new("unit").takes_value(true).long("unit")) } #[test] fn test_values_of() { let matches = app().get_matches_from(vec!["test", "--multiple", "50", "--multiple", "39"]); assert_eq!(values_of(&matches, "multiple"), Some(vec![50, 39])); assert_eq!(values_of::(&matches, "single"), None); let pubkey0 = solana_sdk::pubkey::new_rand(); let pubkey1 = solana_sdk::pubkey::new_rand(); let matches = app().get_matches_from(vec![ "test", "--multiple", &pubkey0.to_string(), "--multiple", &pubkey1.to_string(), ]); assert_eq!( values_of(&matches, "multiple"), Some(vec![pubkey0, pubkey1]) ); } #[test] fn test_value_of() { let matches = app().get_matches_from(vec!["test", "--single", "50"]); assert_eq!(value_of(&matches, "single"), Some(50)); assert_eq!(value_of::(&matches, "multiple"), None); let pubkey = solana_sdk::pubkey::new_rand(); let matches = app().get_matches_from(vec!["test", "--single", &pubkey.to_string()]); assert_eq!(value_of(&matches, "single"), Some(pubkey)); } #[test] fn test_parse_pubkey() { let command = Command::new("test").arg( Arg::new("pubkey") .long("pubkey") .takes_value(true) .value_parser(clap::value_parser!(Pubkey)), ); // success case let matches = command .clone() .try_get_matches_from(vec!["test", "--pubkey", "11111111111111111111111111111111"]) .unwrap(); assert_eq!( *matches.get_one::("pubkey").unwrap(), Pubkey::from_str("11111111111111111111111111111111").unwrap(), ); // validation fails let matches_error = command .clone() .try_get_matches_from(vec!["test", "--pubkey", "this_is_an_invalid_arg"]) .unwrap_err(); assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation); } #[test] fn test_parse_hash() { let command = Command::new("test").arg( Arg::new("hash") .long("hash") .takes_value(true) .value_parser(clap::value_parser!(Hash)), ); // success case let matches = command .clone() .try_get_matches_from(vec!["test", "--hash", "11111111111111111111111111111111"]) .unwrap(); assert_eq!( *matches.get_one::("hash").unwrap(), Hash::from_str("11111111111111111111111111111111").unwrap(), ); // validation fails let matches_error = command .clone() .try_get_matches_from(vec!["test", "--hash", "this_is_an_invalid_arg"]) .unwrap_err(); assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation); } #[test] fn test_parse_token_decimal() { let command = Command::new("test").arg( Arg::new("amount") .long("amount") .takes_value(true) .value_parser(Amount::parse_decimal), ); // success cases let matches = command .clone() .try_get_matches_from(vec!["test", "--amount", "11223344"]) .unwrap(); assert_eq!( *matches.get_one::("amount").unwrap(), Amount::Decimal(11223344_f64), ); let matches = command .clone() .try_get_matches_from(vec!["test", "--amount", "0.11223344"]) .unwrap(); assert_eq!( *matches.get_one::("amount").unwrap(), Amount::Decimal(0.11223344), ); // validation fail cases let matches_error = command .clone() .try_get_matches_from(vec!["test", "--amount", "this_is_an_invalid_arg"]) .unwrap_err(); assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation); let matches_error = command .clone() .try_get_matches_from(vec!["test", "--amount", "all"]) .unwrap_err(); assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation); } #[test] fn test_parse_token_decimal_or_all() { let command = Command::new("test").arg( Arg::new("amount") .long("amount") .takes_value(true) .value_parser(Amount::parse_decimal_or_all), ); // success cases let matches = command .clone() .try_get_matches_from(vec!["test", "--amount", "11223344"]) .unwrap(); assert_eq!( *matches.get_one::("amount").unwrap(), Amount::Decimal(11223344_f64), ); let matches = command .clone() .try_get_matches_from(vec!["test", "--amount", "0.11223344"]) .unwrap(); assert_eq!( *matches.get_one::("amount").unwrap(), Amount::Decimal(0.11223344), ); let matches = command .clone() .try_get_matches_from(vec!["test", "--amount", "ALL"]) .unwrap(); assert_eq!(*matches.get_one::("amount").unwrap(), Amount::All,); // validation fail cases let matches_error = command .clone() .try_get_matches_from(vec!["test", "--amount", "this_is_an_invalid_arg"]) .unwrap_err(); assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation); } #[test] fn test_sol_to_lamports() { let command = Command::new("test").arg( Arg::new("amount") .long("amount") .takes_value(true) .value_parser(Amount::parse_decimal_or_all), ); let test_cases = vec![ ("50", 50_000_000_000), ("1.5", 1_500_000_000), ("0.03", 30_000_000), ]; for (arg, expected_lamport) in test_cases { let matches = command .clone() .try_get_matches_from(vec!["test", "--amount", arg]) .unwrap(); assert_eq!( matches .get_one::("amount") .unwrap() .sol_to_lamport(), Amount::Raw(expected_lamport), ); } } #[test] fn test_derivation() { let command = Command::new("test").arg( Arg::new("derivation") .long("derivation") .takes_value(true) .value_parser(parse_derivation), ); let test_arguments = vec![ ("2", true), ("0", true), ("65537", true), ("0/2", true), ("a", false), ("4294967296", false), ("a/b", false), ("0/4294967296", false), ]; for (arg, should_accept) in test_arguments { if should_accept { let matches = command .clone() .try_get_matches_from(vec!["test", "--derivation", arg]) .unwrap(); assert_eq!(matches.get_one::("derivation").unwrap(), arg); } } } }