solana/clap-v3-utils/src/input_parsers/mod.rs

549 lines
17 KiB
Rust

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<T>(matches: &ArgMatches, name: &str) -> Option<Vec<T>>
where
T: std::str::FromStr,
<T as std::str::FromStr>::Err: std::fmt::Debug,
{
matches
.values_of(name)
.map(|xs| xs.map(|x| x.parse::<T>().unwrap()).collect())
}
// Return a parsed value from matches at `name`
pub fn value_of<T>(matches: &ArgMatches, name: &str) -> Option<T>
where
T: std::str::FromStr,
<T as std::str::FromStr>::Err: std::fmt::Debug,
{
matches
.value_of(name)
.and_then(|value| value.parse::<T>().ok())
}
pub fn unix_timestamp_from_rfc3339_datetime(
matches: &ArgMatches,
name: &str,
) -> Option<UnixTimestamp> {
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<u64> {
value_of(matches, name).map(sol_to_lamports)
}
pub fn cluster_type_of(matches: &ArgMatches, name: &str) -> Option<ClusterType> {
value_of(matches, name)
}
pub fn commitment_of(matches: &ArgMatches, name: &str) -> Option<CommitmentConfig> {
matches
.value_of(name)
.map(|value| CommitmentConfig::from_str(value).unwrap_or_default())
}
pub fn parse_url(arg: &str) -> Result<String, String> {
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<String, String> {
parse_url(&normalize_to_url_if_moniker(arg))
}
pub fn parse_pow2(arg: &str) -> Result<usize, String> {
arg.parse::<usize>()
.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<u8, String> {
arg.parse::<u8>()
.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<Amount, String> {
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<Amount, String> {
arg.parse::<f64>()
.map(Amount::Decimal)
.map_err(|_| format!("Unable to parse input amount, provided: {arg}"))
}
pub fn parse_raw(arg: &str) -> Result<Amount, String> {
arg.parse::<u64>()
.map(Amount::Raw)
.map_err(|_| format!("Unable to parse input amount, provided: {arg}"))
}
pub fn parse_decimal_or_all(arg: &str) -> Result<Amount, String> {
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<String, String> {
DateTime::parse_from_rfc3339(arg)
.map(|_| arg.to_string())
.map_err(|e| format!("{e}"))
}
pub fn parse_derivation(arg: &str) -> Result<String, String> {
let value = arg.replace('\'', "");
let mut parts = value.split('/');
let account = parts.next().unwrap();
account
.parse::<u32>()
.map_err(|e| format!("Unable to parse derivation, provided: {account}, err: {e}"))
.and_then(|_| {
if let Some(change) = parts.next() {
change.parse::<u32>().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<String, String> {
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<String, String> {
(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<Keypair> {
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<Vec<Keypair>> {
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<Pubkey> {
value_of(matches, name).or_else(|| keypair_of(matches, name).map(|keypair| keypair.pubkey()))
}
pub fn pubkeys_of(matches: &ArgMatches, name: &str) -> Option<Vec<Pubkey>> {
matches.values_of(name).map(|values| {
values
.map(|value| {
value.parse::<Pubkey>().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::<u64>(&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::<u64>(&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>("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>("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>("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>("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>("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>("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>("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>("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::<String>("derivation").unwrap(), arg);
}
}
}
}