[clap-v3-utils] Deprecate input validators and add parsers to replace them (#33276)

* add tests for validating `Pubkey` and `Hash`

* add pubkey signature parser

* add parsers for straightforward validators

* add parser for token amounts

* add parser for derivation and seeds

* resolve warnings from deprecations in clap-v3-utils

* remove some deprecated functions from `solana_keygen`

* refactor signer related input parsers into a submodule

* fix deprecation notice for utl

* refactor parsers in `input_validators` to `input_parsers`

* cargo fmt

* Apply suggestions from code review

Co-authored-by: Trent Nelson <trent.a.b.nelson@gmail.com>

* Update clap-v3-utils/src/input_parsers/mod.rs

Co-authored-by: Trent Nelson <trent.a.b.nelson@gmail.com>

* mionr fixes to build

* add deprecation notice for old `input_parsers::signer` functions

* update `UiTokenAmount` to `Amount`

* refactor to-be-deprecated functions back to `input_parsers/mod.rs

* fmt

---------

Co-authored-by: Trent Nelson <trent.a.b.nelson@gmail.com>
This commit is contained in:
samkim-crypto 2023-09-28 07:03:41 -07:00 committed by GitHub
parent 25c27d452c
commit ec36369e47
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 656 additions and 169 deletions

View File

@ -0,0 +1,548 @@
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);
}
}
}
}

View File

@ -1,18 +1,13 @@
use {
crate::keypair::{
keypair_from_seed_phrase, pubkey_from_path, resolve_signer_from_path, signer_from_path,
ASK_KEYWORD, SKIP_SEED_PHRASE_VALIDATION_ARG,
crate::{
input_parsers::{keypair_of, keypairs_of, pubkey_of, pubkeys_of},
keypair::{pubkey_from_path, resolve_signer_from_path, signer_from_path},
},
chrono::DateTime,
clap::ArgMatches,
solana_remote_wallet::remote_wallet::RemoteWalletManager,
solana_sdk::{
clock::UnixTimestamp,
commitment_config::CommitmentConfig,
genesis_config::ClusterType,
native_token::sol_to_lamports,
pubkey::Pubkey,
signature::{read_keypair_file, Keypair, Signature, Signer},
signature::{Keypair, Signature, Signer},
},
std::{error, rc::Rc, str::FromStr},
};
@ -20,55 +15,6 @@ use {
// Sentinel value used to indicate to write to screen instead of file
pub const STDOUT_OUTFILE_TOKEN: &str = "-";
// 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,
{
if let Some(value) = matches.value_of(name) {
value.parse::<T>().ok()
} else {
None
}
}
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())
})
}
// 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
}
}
// Return the keypair for an argument with filename `name` or `None` if not present wrapped inside `Result`.
pub fn try_keypair_of(
matches: &ArgMatches,
@ -78,21 +24,6 @@ pub fn try_keypair_of(
Ok(keypair_of(matches, name))
}
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()
})
}
pub fn try_keypairs_of(
matches: &ArgMatches,
name: &str,
@ -101,12 +32,6 @@ pub fn try_keypairs_of(
Ok(keypairs_of(matches, name))
}
// 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()))
}
// Return a `Result` wrapped 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 try_pubkey_of(
@ -117,20 +42,6 @@ pub fn try_pubkey_of(
Ok(pubkey_of(matches, name))
}
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()
})
}
pub fn try_pubkeys_of(
matches: &ArgMatches,
name: &str,
@ -225,18 +136,28 @@ pub fn resolve_signer(
)
}
pub fn lamports_of_sol(matches: &ArgMatches, name: &str) -> Option<u64> {
value_of(matches, name).map(sol_to_lamports)
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PubkeySignature {
pubkey: Pubkey,
signature: Signature,
}
impl FromStr for PubkeySignature {
type Err = String;
pub fn cluster_type_of(matches: &ArgMatches, name: &str) -> Option<ClusterType> {
value_of(matches, name)
}
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut signer = s.split('=');
let pubkey = signer
.next()
.ok_or_else(|| String::from("Malformed signer string"))?;
let pubkey = Pubkey::from_str(pubkey).map_err(|err| format!("{err}"))?;
pub fn commitment_of(matches: &ArgMatches, name: &str) -> Option<CommitmentConfig> {
matches
.value_of(name)
.map(|value| CommitmentConfig::from_str(value).unwrap_or_default())
let signature = signer
.next()
.ok_or_else(|| String::from("Malformed signer string"))?;
let signature = Signature::from_str(signature).map_err(|err| format!("{err}"))?;
Ok(Self { pubkey, signature })
}
}
#[cfg(test)]
@ -268,38 +189,6 @@ mod tests {
format!("{out_dir}/tmp/{name}-{pubkey}")
}
#[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_keypair_of() {
let keypair = Keypair::new();
@ -376,14 +265,40 @@ mod tests {
}
#[test]
fn test_lamports_of_sol() {
let matches = app().get_matches_from(vec!["test", "--single", "50"]);
assert_eq!(lamports_of_sol(&matches, "single"), Some(50_000_000_000));
assert_eq!(lamports_of_sol(&matches, "multiple"), None);
let matches = app().get_matches_from(vec!["test", "--single", "1.5"]);
assert_eq!(lamports_of_sol(&matches, "single"), Some(1_500_000_000));
assert_eq!(lamports_of_sol(&matches, "multiple"), None);
let matches = app().get_matches_from(vec!["test", "--single", "0.03"]);
assert_eq!(lamports_of_sol(&matches, "single"), Some(30_000_000));
fn test_parse_pubkey_signature() {
let command = Command::new("test").arg(
Arg::new("pubkeysig")
.long("pubkeysig")
.takes_value(true)
.value_parser(clap::value_parser!(PubkeySignature)),
);
// success case
let matches = command
.clone()
.try_get_matches_from(vec![
"test",
"--pubkeysig",
"11111111111111111111111111111111=4TpFuec1u4BZfxgHg2VQXwvBHANZuNSJHmgrU34GViLAM5uYZ8t7uuhWMHN4k9r41B2p9mwnHjPGwTmTxyvCZw63"
]
)
.unwrap();
let expected = PubkeySignature {
pubkey: Pubkey::from_str("11111111111111111111111111111111").unwrap(),
signature: Signature::from_str("4TpFuec1u4BZfxgHg2VQXwvBHANZuNSJHmgrU34GViLAM5uYZ8t7uuhWMHN4k9r41B2p9mwnHjPGwTmTxyvCZw63").unwrap(),
};
assert_eq!(
*matches.get_one::<PubkeySignature>("pubkeysig").unwrap(),
expected,
);
// validation fails
let matches_error = command
.clone()
.try_get_matches_from(vec!["test", "--pubkeysig", "this_is_an_invalid_arg"])
.unwrap_err();
assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation);
}
}

View File

@ -25,6 +25,7 @@ where
// Return an error if string cannot be parsed as type T.
// Takes a String to avoid second type parameter when used as a clap validator
#[deprecated(since = "1.17.0", note = "please use `clap::value_parser!` instead")]
pub fn is_parsable<T>(string: &str) -> Result<(), String>
where
T: FromStr,
@ -35,6 +36,10 @@ where
// Return an error if string cannot be parsed as numeric type T, and value not within specified
// range
#[deprecated(
since = "1.17.0",
note = "please use `clap::builder::RangedI64ValueParser` instead"
)]
pub fn is_within_range<T, R>(string: String, range: R) -> Result<(), String>
where
T: FromStr + Copy + std::fmt::Debug + PartialOrd + std::ops::Add<Output = T> + From<usize>,
@ -59,6 +64,10 @@ pub fn is_pubkey(string: &str) -> Result<(), String> {
}
// Return an error if a hash cannot be parsed.
#[deprecated(
since = "1.17.0",
note = "please use `clap::value_parser!(Hash)` instead"
)]
pub fn is_hash<T>(string: T) -> Result<(), String>
where
T: AsRef<str> + Display,
@ -144,6 +153,10 @@ where
}
// Return an error if string cannot be parsed as pubkey=signature string
#[deprecated(
since = "1.17.0",
note = "please use `clap::value_parser!(PubkeySignature)` instead"
)]
pub fn is_pubkey_sig<T>(string: T) -> Result<(), String>
where
T: AsRef<str> + Display,
@ -169,6 +182,7 @@ where
}
// Return an error if a url cannot be parsed.
#[deprecated(since = "1.17.0", note = "please use `parse_url` instead")]
pub fn is_url<T>(string: T) -> Result<(), String>
where
T: AsRef<str> + Display,
@ -185,6 +199,7 @@ where
}
}
#[deprecated(since = "1.17.0", note = "please use `parse_url_or_moniker` instead")]
pub fn is_url_or_moniker<T>(string: T) -> Result<(), String>
where
T: AsRef<str> + Display,
@ -212,6 +227,10 @@ pub fn normalize_to_url_if_moniker<T: AsRef<str>>(url_or_moniker: T) -> String {
.to_string()
}
#[deprecated(
since = "1.17.0",
note = "please use `clap::value_parser!(Epoch)` instead"
)]
pub fn is_epoch<T>(epoch: T) -> Result<(), String>
where
T: AsRef<str> + Display,
@ -219,6 +238,10 @@ where
is_parsable_generic::<Epoch, _>(epoch)
}
#[deprecated(
since = "1.17.0",
note = "please use `clap::value_parser!(Slot)` instead"
)]
pub fn is_slot<T>(slot: T) -> Result<(), String>
where
T: AsRef<str> + Display,
@ -226,6 +249,7 @@ where
is_parsable_generic::<Slot, _>(slot)
}
#[deprecated(since = "1.17.0", note = "please use `parse_pow2` instead")]
pub fn is_pow2<T>(bins: T) -> Result<(), String>
where
T: AsRef<str> + Display,
@ -242,6 +266,10 @@ where
})
}
#[deprecated(
since = "1.17.0",
note = "please use `clap_value_parser!(u16)` instead"
)]
pub fn is_port<T>(port: T) -> Result<(), String>
where
T: AsRef<str> + Display,
@ -249,6 +277,7 @@ where
is_parsable_generic::<u16, _>(port)
}
#[deprecated(since = "1.17.0", note = "please use `parse_percentage` instead")]
pub fn is_valid_percentage<T>(percentage: T) -> Result<(), String>
where
T: AsRef<str> + Display,
@ -268,6 +297,7 @@ where
})
}
#[deprecated(since = "1.17.0", note = "please use `Amount::parse_decimal` instead")]
pub fn is_amount<T>(amount: T) -> Result<(), String>
where
T: AsRef<str> + Display,
@ -281,6 +311,10 @@ where
}
}
#[deprecated(
since = "1.17.0",
note = "please use `TokenAmount::parse_decimal` instead"
)]
pub fn is_amount_or_all<T>(amount: T) -> Result<(), String>
where
T: AsRef<str> + Display,
@ -297,6 +331,7 @@ where
}
}
#[deprecated(since = "1.17.0", note = "please use `parse_rfc3339_datetime` instead")]
pub fn is_rfc3339_datetime<T>(value: T) -> Result<(), String>
where
T: AsRef<str> + Display,
@ -306,6 +341,7 @@ where
.map_err(|e| format!("{e}"))
}
#[deprecated(since = "1.17.0", note = "please use `parse_derivation` instead")]
pub fn is_derivation<T>(value: T) -> Result<(), String>
where
T: AsRef<str> + Display,
@ -328,6 +364,7 @@ where
.map(|_| ())
}
#[deprecated(since = "1.17.0", note = "please use `parse_structured_seed` instead")]
pub fn is_structured_seed<T>(value: T) -> Result<(), String>
where
T: AsRef<str> + Display,
@ -373,6 +410,10 @@ where
}
}
#[deprecated(
since = "1.17.0",
note = "please use `parse_derived_address_seed` instead"
)]
pub fn is_derived_address_seed<T>(value: T) -> Result<(), String>
where
T: AsRef<str> + Display,
@ -386,21 +427,3 @@ where
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_derivation() {
assert_eq!(is_derivation("2"), Ok(()));
assert_eq!(is_derivation("0"), Ok(()));
assert_eq!(is_derivation("65537"), Ok(()));
assert_eq!(is_derivation("0/2"), Ok(()));
assert_eq!(is_derivation("0'/2'"), Ok(()));
assert!(is_derivation("a").is_err());
assert!(is_derivation("4294967296").is_err());
assert!(is_derivation("a/b").is_err());
assert!(is_derivation("0/4294967296").is_err());
}
}

View File

@ -1,6 +1,7 @@
use {
crate::{input_validators::*, ArgConstant},
clap::{Arg, Command},
crate::{input_parsers::signer::PubkeySignature, ArgConstant},
clap::{value_parser, Arg, Command},
solana_sdk::hash::Hash,
};
pub const BLOCKHASH_ARG: ArgConstant<'static> = ArgConstant {
@ -32,7 +33,7 @@ pub fn blockhash_arg<'a>() -> Arg<'a> {
.long(BLOCKHASH_ARG.long)
.takes_value(true)
.value_name("BLOCKHASH")
.validator(|s| is_hash(s))
.value_parser(value_parser!(Hash))
.help(BLOCKHASH_ARG.help)
}
@ -49,7 +50,7 @@ fn signer_arg<'a>() -> Arg<'a> {
.long(SIGNER_ARG.long)
.takes_value(true)
.value_name("PUBKEY=SIGNATURE")
.validator(|s| is_pubkey_sig(s))
.value_parser(value_parser!(PubkeySignature))
.requires(BLOCKHASH_ARG.name)
.multiple_occurrences(true)
.multiple_values(true)

View File

@ -1,10 +1,10 @@
#![allow(clippy::arithmetic_side_effects)]
use {
bip39::{Mnemonic, MnemonicType, Seed},
clap::{crate_description, crate_name, Arg, ArgMatches, Command},
clap::{crate_description, crate_name, value_parser, Arg, ArgMatches, Command},
solana_clap_v3_utils::{
input_parsers::STDOUT_OUTFILE_TOKEN,
input_validators::{is_parsable, is_prompt_signer_source},
input_validators::is_prompt_signer_source,
keygen::{
check_for_overwrite,
derivation_path::{acquire_derivation_path, derivation_path_arg},
@ -339,7 +339,7 @@ fn app<'a>(num_threads: &'a str, crate_version: &'a str) -> Command<'a> {
.long("num-threads")
.value_name("NUMBER")
.takes_value(true)
.validator(is_parsable::<usize>)
.value_parser(value_parser!(usize))
.default_value(num_threads)
.help("Specify the number of grind threads"),
)
@ -571,7 +571,7 @@ fn do_main(matches: &ArgMatches) -> Result<(), Box<dyn error::Error>> {
);
}
let num_threads: usize = matches.value_of_t_or_exit("num_threads");
let num_threads = *matches.get_one::<usize>("num_threads").unwrap();
let grind_matches = grind_parse_args(
ignore_case,