Add clap-v3-utils (#24096)
* Add clap-utils-v3 * update Cargo.lock * address PR comment: rename crate * address PR comment: rename to clap-v3-utils * update dep version * fix clippy errors * update Cargo.lock
This commit is contained in:
parent
998cdd1c29
commit
76c80d9573
|
@ -4547,6 +4547,23 @@ dependencies = [
|
|||
"url 2.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solana-clap-v3-utils"
|
||||
version = "1.11.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap 3.1.8",
|
||||
"rpassword",
|
||||
"solana-perf",
|
||||
"solana-remote-wallet",
|
||||
"solana-sdk",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"tiny-bip39",
|
||||
"uriparse",
|
||||
"url 2.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solana-cli"
|
||||
version = "1.11.0"
|
||||
|
|
|
@ -12,6 +12,7 @@ members = [
|
|||
"bloom",
|
||||
"bucket_map",
|
||||
"clap-utils",
|
||||
"clap-v3-utils",
|
||||
"cli",
|
||||
"cli-config",
|
||||
"cli-output",
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
[package]
|
||||
name = "solana-clap-v3-utils"
|
||||
version = "1.11.0"
|
||||
description = "Solana utilities for the clap v3"
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
documentation = "https://docs.rs/solana-clap-utils"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4"
|
||||
clap = { version = "3.1.5", features = ["cargo"] }
|
||||
rpassword = "6.0"
|
||||
solana-perf = { path = "../perf", version = "=1.11.0" }
|
||||
solana-remote-wallet = { path = "../remote-wallet", version = "=1.11.0", default-features = false }
|
||||
solana-sdk = { path = "../sdk", version = "=1.11.0" }
|
||||
thiserror = "1.0.30"
|
||||
tiny-bip39 = "0.8.2"
|
||||
uriparse = "0.6.4"
|
||||
url = "2.2.2"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.3.0"
|
||||
|
||||
[lib]
|
||||
name = "solana_clap_v3_utils"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
|
@ -0,0 +1,21 @@
|
|||
use {
|
||||
crate::{input_validators, ArgConstant},
|
||||
clap::Arg,
|
||||
};
|
||||
|
||||
pub const FEE_PAYER_ARG: ArgConstant<'static> = ArgConstant {
|
||||
name: "fee_payer",
|
||||
long: "fee-payer",
|
||||
help: "Specify the fee-payer account. This may be a keypair file, the ASK keyword \n\
|
||||
or the pubkey of an offline signer, provided an appropriate --signer argument \n\
|
||||
is also passed. Defaults to the client keypair.",
|
||||
};
|
||||
|
||||
pub fn fee_payer_arg<'a>() -> Arg<'a> {
|
||||
Arg::new(FEE_PAYER_ARG.name)
|
||||
.long(FEE_PAYER_ARG.long)
|
||||
.takes_value(true)
|
||||
.value_name("KEYPAIR")
|
||||
.validator(|s| input_validators::is_valid_signer(s))
|
||||
.help(FEE_PAYER_ARG.help)
|
||||
}
|
|
@ -0,0 +1,374 @@
|
|||
use {
|
||||
crate::keypair::{
|
||||
keypair_from_seed_phrase, pubkey_from_path, resolve_signer_from_path, signer_from_path,
|
||||
ASK_KEYWORD, SKIP_SEED_PHRASE_VALIDATION_ARG,
|
||||
},
|
||||
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},
|
||||
},
|
||||
std::{str::FromStr, sync::Arc},
|
||||
};
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
})
|
||||
}
|
||||
|
||||
// Return pubkey/signature pairs for a string of the form pubkey=signature
|
||||
pub fn pubkeys_sigs_of(matches: &ArgMatches, name: &str) -> Option<Vec<(Pubkey, Signature)>> {
|
||||
matches.values_of(name).map(|values| {
|
||||
values
|
||||
.map(|pubkey_signer_string| {
|
||||
let mut signer = pubkey_signer_string.split('=');
|
||||
let key = Pubkey::from_str(signer.next().unwrap()).unwrap();
|
||||
let sig = Signature::from_str(signer.next().unwrap()).unwrap();
|
||||
(key, sig)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
// Return a signer from matches at `name`
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn signer_of(
|
||||
matches: &ArgMatches,
|
||||
name: &str,
|
||||
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
|
||||
) -> Result<(Option<Box<dyn Signer>>, Option<Pubkey>), Box<dyn std::error::Error>> {
|
||||
if let Some(location) = matches.value_of(name) {
|
||||
let signer = signer_from_path(matches, location, name, wallet_manager)?;
|
||||
let signer_pubkey = signer.pubkey();
|
||||
Ok((Some(signer), Some(signer_pubkey)))
|
||||
} else {
|
||||
Ok((None, None))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pubkey_of_signer(
|
||||
matches: &ArgMatches,
|
||||
name: &str,
|
||||
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
|
||||
) -> Result<Option<Pubkey>, Box<dyn std::error::Error>> {
|
||||
if let Some(location) = matches.value_of(name) {
|
||||
Ok(Some(pubkey_from_path(
|
||||
matches,
|
||||
location,
|
||||
name,
|
||||
wallet_manager,
|
||||
)?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pubkeys_of_multiple_signers(
|
||||
matches: &ArgMatches,
|
||||
name: &str,
|
||||
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
|
||||
) -> Result<Option<Vec<Pubkey>>, Box<dyn std::error::Error>> {
|
||||
if let Some(pubkey_matches) = matches.values_of(name) {
|
||||
let mut pubkeys: Vec<Pubkey> = vec![];
|
||||
for signer in pubkey_matches {
|
||||
pubkeys.push(pubkey_from_path(matches, signer, name, wallet_manager)?);
|
||||
}
|
||||
Ok(Some(pubkeys))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_signer(
|
||||
matches: &ArgMatches,
|
||||
name: &str,
|
||||
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
|
||||
) -> Result<Option<String>, Box<dyn std::error::Error>> {
|
||||
resolve_signer_from_path(
|
||||
matches,
|
||||
matches.value_of(name).unwrap(),
|
||||
name,
|
||||
wallet_manager,
|
||||
)
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use {
|
||||
super::*,
|
||||
clap::{Arg, Command},
|
||||
solana_sdk::signature::write_keypair_file,
|
||||
std::fs,
|
||||
};
|
||||
|
||||
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"))
|
||||
}
|
||||
|
||||
fn tmp_file_path(name: &str, pubkey: &Pubkey) -> String {
|
||||
use std::env;
|
||||
let out_dir = env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_string());
|
||||
|
||||
format!("{}/tmp/{}-{}", out_dir, name, pubkey)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_values_of() {
|
||||
let matches =
|
||||
app()
|
||||
.clone()
|
||||
.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().clone().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()
|
||||
.clone()
|
||||
.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()
|
||||
.clone()
|
||||
.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();
|
||||
let outfile = tmp_file_path("test_keypair_of.json", &keypair.pubkey());
|
||||
let _ = write_keypair_file(&keypair, &outfile).unwrap();
|
||||
|
||||
let matches = app()
|
||||
.clone()
|
||||
.get_matches_from(vec!["test", "--single", &outfile]);
|
||||
assert_eq!(
|
||||
keypair_of(&matches, "single").unwrap().pubkey(),
|
||||
keypair.pubkey()
|
||||
);
|
||||
assert!(keypair_of(&matches, "multiple").is_none());
|
||||
|
||||
let matches =
|
||||
app()
|
||||
.clone()
|
||||
.get_matches_from(vec!["test", "--single", "random_keypair_file.json"]);
|
||||
assert!(keypair_of(&matches, "single").is_none());
|
||||
|
||||
fs::remove_file(&outfile).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pubkey_of() {
|
||||
let keypair = Keypair::new();
|
||||
let outfile = tmp_file_path("test_pubkey_of.json", &keypair.pubkey());
|
||||
let _ = write_keypair_file(&keypair, &outfile).unwrap();
|
||||
|
||||
let matches = app()
|
||||
.clone()
|
||||
.get_matches_from(vec!["test", "--single", &outfile]);
|
||||
assert_eq!(pubkey_of(&matches, "single"), Some(keypair.pubkey()));
|
||||
assert_eq!(pubkey_of(&matches, "multiple"), None);
|
||||
|
||||
let matches =
|
||||
app()
|
||||
.clone()
|
||||
.get_matches_from(vec!["test", "--single", &keypair.pubkey().to_string()]);
|
||||
assert_eq!(pubkey_of(&matches, "single"), Some(keypair.pubkey()));
|
||||
|
||||
let matches =
|
||||
app()
|
||||
.clone()
|
||||
.get_matches_from(vec!["test", "--single", "random_keypair_file.json"]);
|
||||
assert_eq!(pubkey_of(&matches, "single"), None);
|
||||
|
||||
fs::remove_file(&outfile).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pubkeys_of() {
|
||||
let keypair = Keypair::new();
|
||||
let outfile = tmp_file_path("test_pubkeys_of.json", &keypair.pubkey());
|
||||
let _ = write_keypair_file(&keypair, &outfile).unwrap();
|
||||
|
||||
let matches = app().clone().get_matches_from(vec![
|
||||
"test",
|
||||
"--multiple",
|
||||
&keypair.pubkey().to_string(),
|
||||
"--multiple",
|
||||
&outfile,
|
||||
]);
|
||||
assert_eq!(
|
||||
pubkeys_of(&matches, "multiple"),
|
||||
Some(vec![keypair.pubkey(), keypair.pubkey()])
|
||||
);
|
||||
fs::remove_file(&outfile).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pubkeys_sigs_of() {
|
||||
let key1 = solana_sdk::pubkey::new_rand();
|
||||
let key2 = solana_sdk::pubkey::new_rand();
|
||||
let sig1 = Keypair::new().sign_message(&[0u8]);
|
||||
let sig2 = Keypair::new().sign_message(&[1u8]);
|
||||
let signer1 = format!("{}={}", key1, sig1);
|
||||
let signer2 = format!("{}={}", key2, sig2);
|
||||
let matches = app().clone().get_matches_from(vec![
|
||||
"test",
|
||||
"--multiple",
|
||||
&signer1,
|
||||
"--multiple",
|
||||
&signer2,
|
||||
]);
|
||||
assert_eq!(
|
||||
pubkeys_sigs_of(&matches, "multiple"),
|
||||
Some(vec![(key1, sig1), (key2, sig2)])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lamports_of_sol() {
|
||||
let matches = app()
|
||||
.clone()
|
||||
.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()
|
||||
.clone()
|
||||
.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()
|
||||
.clone()
|
||||
.get_matches_from(vec!["test", "--single", "0.03"]);
|
||||
assert_eq!(lamports_of_sol(&matches, "single"), Some(30_000_000));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,416 @@
|
|||
use {
|
||||
crate::keypair::{parse_signer_source, SignerSourceKind, ASK_KEYWORD},
|
||||
chrono::DateTime,
|
||||
solana_sdk::{
|
||||
clock::{Epoch, Slot},
|
||||
hash::Hash,
|
||||
pubkey::{Pubkey, MAX_SEED_LEN},
|
||||
signature::{read_keypair_file, Signature},
|
||||
},
|
||||
std::{fmt::Display, str::FromStr},
|
||||
};
|
||||
|
||||
fn is_parsable_generic<U, T>(string: T) -> Result<(), String>
|
||||
where
|
||||
T: AsRef<str> + Display,
|
||||
U: FromStr,
|
||||
U::Err: Display,
|
||||
{
|
||||
string
|
||||
.as_ref()
|
||||
.parse::<U>()
|
||||
.map(|_| ())
|
||||
.map_err(|err| format!("error parsing '{}': {}", string, err))
|
||||
}
|
||||
|
||||
// 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
|
||||
pub fn is_parsable<T>(string: String) -> Result<(), String>
|
||||
where
|
||||
T: FromStr,
|
||||
T::Err: Display,
|
||||
{
|
||||
is_parsable_generic::<T, String>(string)
|
||||
}
|
||||
|
||||
// Return an error if string cannot be parsed as numeric type T, and value not within specified
|
||||
// range
|
||||
pub fn is_within_range<T>(string: String, range_min: T, range_max: T) -> Result<(), String>
|
||||
where
|
||||
T: FromStr + Copy + std::fmt::Debug + PartialOrd + std::ops::Add<Output = T> + From<usize>,
|
||||
T::Err: Display,
|
||||
{
|
||||
match string.parse::<T>() {
|
||||
Ok(input) => {
|
||||
let range = range_min..range_max + 1.into();
|
||||
if !range.contains(&input) {
|
||||
Err(format!(
|
||||
"input '{:?}' out of range ({:?}..{:?}]",
|
||||
input, range_min, range_max
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Err(err) => Err(format!("error parsing '{}': {}", string, err)),
|
||||
}
|
||||
}
|
||||
|
||||
// Return an error if a pubkey cannot be parsed.
|
||||
pub fn is_pubkey<T>(string: T) -> Result<(), String>
|
||||
where
|
||||
T: AsRef<str> + Display,
|
||||
{
|
||||
is_parsable_generic::<Pubkey, _>(string)
|
||||
}
|
||||
|
||||
// Return an error if a hash cannot be parsed.
|
||||
pub fn is_hash<T>(string: T) -> Result<(), String>
|
||||
where
|
||||
T: AsRef<str> + Display,
|
||||
{
|
||||
is_parsable_generic::<Hash, _>(string)
|
||||
}
|
||||
|
||||
// Return an error if a keypair file cannot be parsed.
|
||||
pub fn is_keypair<T>(string: T) -> Result<(), String>
|
||||
where
|
||||
T: AsRef<str> + Display,
|
||||
{
|
||||
read_keypair_file(string.as_ref())
|
||||
.map(|_| ())
|
||||
.map_err(|err| format!("{}", err))
|
||||
}
|
||||
|
||||
// Return an error if a keypair file cannot be parsed
|
||||
pub fn is_keypair_or_ask_keyword<T>(string: T) -> Result<(), String>
|
||||
where
|
||||
T: AsRef<str> + Display,
|
||||
{
|
||||
if string.as_ref() == ASK_KEYWORD {
|
||||
return Ok(());
|
||||
}
|
||||
read_keypair_file(string.as_ref())
|
||||
.map(|_| ())
|
||||
.map_err(|err| format!("{}", err))
|
||||
}
|
||||
|
||||
// Return an error if a `SignerSourceKind::Prompt` cannot be parsed
|
||||
pub fn is_prompt_signer_source<T>(string: T) -> Result<(), String>
|
||||
where
|
||||
T: AsRef<str> + Display,
|
||||
{
|
||||
if string.as_ref() == ASK_KEYWORD {
|
||||
return Ok(());
|
||||
}
|
||||
match parse_signer_source(string.as_ref())
|
||||
.map_err(|err| format!("{}", err))?
|
||||
.kind
|
||||
{
|
||||
SignerSourceKind::Prompt => Ok(()),
|
||||
_ => Err(format!(
|
||||
"Unable to parse input as `prompt:` URI scheme or `ASK` keyword: {}",
|
||||
string
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
// Return an error if string cannot be parsed as pubkey string or keypair file location
|
||||
pub fn is_pubkey_or_keypair<T>(string: T) -> Result<(), String>
|
||||
where
|
||||
T: AsRef<str> + Display,
|
||||
{
|
||||
is_pubkey(string.as_ref()).or_else(|_| is_keypair(string))
|
||||
}
|
||||
|
||||
// Return an error if string cannot be parsed as a pubkey string, or a valid Signer that can
|
||||
// produce a pubkey()
|
||||
pub fn is_valid_pubkey<T>(string: T) -> Result<(), String>
|
||||
where
|
||||
T: AsRef<str> + Display,
|
||||
{
|
||||
match parse_signer_source(string.as_ref())
|
||||
.map_err(|err| format!("{}", err))?
|
||||
.kind
|
||||
{
|
||||
SignerSourceKind::Filepath(path) => is_keypair(path),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
// Return an error if string cannot be parsed as a valid Signer. This is an alias of
|
||||
// `is_valid_pubkey`, and does accept pubkey strings, even though a Pubkey is not by itself
|
||||
// sufficient to sign a transaction.
|
||||
//
|
||||
// In the current offline-signing implementation, a pubkey is the valid input for a signer field
|
||||
// when paired with an offline `--signer` argument to provide a Presigner (pubkey + signature).
|
||||
// Clap validators can't check multiple fields at once, so the verification that a `--signer` is
|
||||
// also provided and correct happens in parsing, not in validation.
|
||||
pub fn is_valid_signer<T>(string: T) -> Result<(), String>
|
||||
where
|
||||
T: AsRef<str> + Display,
|
||||
{
|
||||
is_valid_pubkey(string)
|
||||
}
|
||||
|
||||
// Return an error if string cannot be parsed as pubkey=signature string
|
||||
pub fn is_pubkey_sig<T>(string: T) -> Result<(), String>
|
||||
where
|
||||
T: AsRef<str> + Display,
|
||||
{
|
||||
let mut signer = string.as_ref().split('=');
|
||||
match Pubkey::from_str(
|
||||
signer
|
||||
.next()
|
||||
.ok_or_else(|| "Malformed signer string".to_string())?,
|
||||
) {
|
||||
Ok(_) => {
|
||||
match Signature::from_str(
|
||||
signer
|
||||
.next()
|
||||
.ok_or_else(|| "Malformed signer string".to_string())?,
|
||||
) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(format!("{}", err)),
|
||||
}
|
||||
}
|
||||
Err(err) => Err(format!("{}", err)),
|
||||
}
|
||||
}
|
||||
|
||||
// Return an error if a url cannot be parsed.
|
||||
pub fn is_url<T>(string: T) -> Result<(), String>
|
||||
where
|
||||
T: AsRef<str> + Display,
|
||||
{
|
||||
match url::Url::parse(string.as_ref()) {
|
||||
Ok(url) => {
|
||||
if url.has_host() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("no host provided".to_string())
|
||||
}
|
||||
}
|
||||
Err(err) => Err(format!("{}", err)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_url_or_moniker<T>(string: T) -> Result<(), String>
|
||||
where
|
||||
T: AsRef<str> + Display,
|
||||
{
|
||||
match url::Url::parse(&normalize_to_url_if_moniker(string.as_ref())) {
|
||||
Ok(url) => {
|
||||
if url.has_host() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("no host provided".to_string())
|
||||
}
|
||||
}
|
||||
Err(err) => Err(format!("{}", err)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn normalize_to_url_if_moniker<T: AsRef<str>>(url_or_moniker: T) -> String {
|
||||
match url_or_moniker.as_ref() {
|
||||
"m" | "mainnet-beta" => "https://api.mainnet-beta.solana.com",
|
||||
"t" | "testnet" => "https://api.testnet.solana.com",
|
||||
"d" | "devnet" => "https://api.devnet.solana.com",
|
||||
"l" | "localhost" => "http://localhost:8899",
|
||||
url => url,
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub fn is_epoch<T>(epoch: T) -> Result<(), String>
|
||||
where
|
||||
T: AsRef<str> + Display,
|
||||
{
|
||||
is_parsable_generic::<Epoch, _>(epoch)
|
||||
}
|
||||
|
||||
pub fn is_slot<T>(slot: T) -> Result<(), String>
|
||||
where
|
||||
T: AsRef<str> + Display,
|
||||
{
|
||||
is_parsable_generic::<Slot, _>(slot)
|
||||
}
|
||||
|
||||
pub fn is_pow2<T>(bins: T) -> Result<(), String>
|
||||
where
|
||||
T: AsRef<str> + Display,
|
||||
{
|
||||
bins.as_ref()
|
||||
.parse::<usize>()
|
||||
.map_err(|e| format!("Unable to parse, provided: {}, err: {}", bins, e))
|
||||
.and_then(|v| {
|
||||
if !v.is_power_of_two() {
|
||||
Err(format!("Must be a power of 2: {}", v))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_port<T>(port: T) -> Result<(), String>
|
||||
where
|
||||
T: AsRef<str> + Display,
|
||||
{
|
||||
is_parsable_generic::<u16, _>(port)
|
||||
}
|
||||
|
||||
pub fn is_valid_percentage<T>(percentage: T) -> Result<(), String>
|
||||
where
|
||||
T: AsRef<str> + Display,
|
||||
{
|
||||
percentage
|
||||
.as_ref()
|
||||
.parse::<u8>()
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"Unable to parse input percentage, provided: {}, err: {}",
|
||||
percentage, e
|
||||
)
|
||||
})
|
||||
.and_then(|v| {
|
||||
if v > 100 {
|
||||
Err(format!(
|
||||
"Percentage must be in range of 0 to 100, provided: {}",
|
||||
v
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_amount<T>(amount: T) -> Result<(), String>
|
||||
where
|
||||
T: AsRef<str> + Display,
|
||||
{
|
||||
if amount.as_ref().parse::<u64>().is_ok() || amount.as_ref().parse::<f64>().is_ok() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!(
|
||||
"Unable to parse input amount as integer or float, provided: {}",
|
||||
amount
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_amount_or_all<T>(amount: T) -> Result<(), String>
|
||||
where
|
||||
T: AsRef<str> + Display,
|
||||
{
|
||||
if amount.as_ref().parse::<u64>().is_ok()
|
||||
|| amount.as_ref().parse::<f64>().is_ok()
|
||||
|| amount.as_ref() == "ALL"
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!(
|
||||
"Unable to parse input amount as integer or float, provided: {}",
|
||||
amount
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_rfc3339_datetime<T>(value: T) -> Result<(), String>
|
||||
where
|
||||
T: AsRef<str> + Display,
|
||||
{
|
||||
DateTime::parse_from_rfc3339(value.as_ref())
|
||||
.map(|_| ())
|
||||
.map_err(|e| format!("{}", e))
|
||||
}
|
||||
|
||||
pub fn is_derivation<T>(value: T) -> Result<(), String>
|
||||
where
|
||||
T: AsRef<str> + Display,
|
||||
{
|
||||
let value = value.as_ref().replace('\'', "");
|
||||
let mut parts = value.split('/');
|
||||
let account = parts.next().unwrap();
|
||||
account
|
||||
.parse::<u32>()
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"Unable to parse derivation, provided: {}, err: {}",
|
||||
account, e
|
||||
)
|
||||
})
|
||||
.and_then(|_| {
|
||||
if let Some(change) = parts.next() {
|
||||
change.parse::<u32>().map_err(|e| {
|
||||
format!(
|
||||
"Unable to parse derivation, provided: {}, err: {}",
|
||||
change, e
|
||||
)
|
||||
})
|
||||
} else {
|
||||
Ok(0)
|
||||
}
|
||||
})
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
pub fn is_derived_address_seed<T>(value: T) -> Result<(), String>
|
||||
where
|
||||
T: AsRef<str> + Display,
|
||||
{
|
||||
let value = value.as_ref();
|
||||
if value.len() > MAX_SEED_LEN {
|
||||
Err(format!(
|
||||
"Address seed must not be longer than {} bytes",
|
||||
MAX_SEED_LEN
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_niceness_adjustment_valid<T>(value: T) -> Result<(), String>
|
||||
where
|
||||
T: AsRef<str> + Display,
|
||||
{
|
||||
let adjustment = value.as_ref().parse::<i8>().map_err(|err| {
|
||||
format!(
|
||||
"error parsing niceness adjustment value '{}': {}",
|
||||
value, err
|
||||
)
|
||||
})?;
|
||||
if solana_perf::thread::is_renice_allowed(adjustment) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(String::from(
|
||||
"niceness adjustment supported only on Linux; negative adjustment \
|
||||
(priority increase) requires root or CAP_SYS_NICE (see `man 7 capabilities` \
|
||||
for details)",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[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());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_niceness_adjustment_valid() {
|
||||
assert_eq!(is_niceness_adjustment_valid("0"), Ok(()));
|
||||
assert!(is_niceness_adjustment_valid("128").is_err());
|
||||
assert!(is_niceness_adjustment_valid("-129").is_err());
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,32 @@
|
|||
use thiserror::Error;
|
||||
|
||||
pub struct ArgConstant<'a> {
|
||||
pub long: &'a str,
|
||||
pub name: &'a str,
|
||||
pub help: &'a str,
|
||||
}
|
||||
|
||||
/// Error type for forwarding Errors out of `main()` of a `clap` app
|
||||
/// and still using the `Display` formatter
|
||||
#[derive(Error)]
|
||||
#[error("{0}")]
|
||||
pub struct DisplayError(Box<dyn std::error::Error>);
|
||||
impl DisplayError {
|
||||
pub fn new_as_boxed(inner: Box<dyn std::error::Error>) -> Box<Self> {
|
||||
DisplayError(inner).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for DisplayError {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(fmt, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub mod fee_payer;
|
||||
pub mod input_parsers;
|
||||
pub mod input_validators;
|
||||
pub mod keypair;
|
||||
pub mod memo;
|
||||
pub mod nonce;
|
||||
pub mod offline;
|
|
@ -0,0 +1,15 @@
|
|||
use {crate::ArgConstant, clap::Arg};
|
||||
|
||||
pub const MEMO_ARG: ArgConstant<'static> = ArgConstant {
|
||||
name: "memo",
|
||||
long: "--with-memo",
|
||||
help: "Specify a memo string to include in the transaction.",
|
||||
};
|
||||
|
||||
pub fn memo_arg<'a>() -> Arg<'a> {
|
||||
Arg::new(MEMO_ARG.name)
|
||||
.long(MEMO_ARG.long)
|
||||
.takes_value(true)
|
||||
.value_name("MEMO")
|
||||
.help(MEMO_ARG.help)
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
use {
|
||||
crate::{input_validators::*, offline::BLOCKHASH_ARG, ArgConstant},
|
||||
clap::{Arg, Command},
|
||||
};
|
||||
|
||||
pub const NONCE_ARG: ArgConstant<'static> = ArgConstant {
|
||||
name: "nonce",
|
||||
long: "nonce",
|
||||
help: "Provide the nonce account to use when creating a nonced \n\
|
||||
transaction. Nonced transactions are useful when a transaction \n\
|
||||
requires a lengthy signing process. Learn more about nonced \n\
|
||||
transactions at https://docs.solana.com/offline-signing/durable-nonce",
|
||||
};
|
||||
|
||||
pub const NONCE_AUTHORITY_ARG: ArgConstant<'static> = ArgConstant {
|
||||
name: "nonce_authority",
|
||||
long: "nonce-authority",
|
||||
help: "Provide the nonce authority keypair to use when signing a nonced transaction",
|
||||
};
|
||||
|
||||
fn nonce_arg<'a>() -> Arg<'a> {
|
||||
Arg::new(NONCE_ARG.name)
|
||||
.long(NONCE_ARG.long)
|
||||
.takes_value(true)
|
||||
.value_name("PUBKEY")
|
||||
.requires(BLOCKHASH_ARG.name)
|
||||
.validator(|s| is_valid_pubkey(s))
|
||||
.help(NONCE_ARG.help)
|
||||
}
|
||||
|
||||
pub fn nonce_authority_arg<'a>() -> Arg<'a> {
|
||||
Arg::new(NONCE_AUTHORITY_ARG.name)
|
||||
.long(NONCE_AUTHORITY_ARG.long)
|
||||
.takes_value(true)
|
||||
.value_name("KEYPAIR")
|
||||
.validator(|s| is_valid_signer(s))
|
||||
.help(NONCE_AUTHORITY_ARG.help)
|
||||
}
|
||||
|
||||
pub trait NonceArgs {
|
||||
fn nonce_args(self, global: bool) -> Self;
|
||||
}
|
||||
|
||||
impl NonceArgs for Command<'_> {
|
||||
fn nonce_args(self, global: bool) -> Self {
|
||||
self.arg(nonce_arg().global(global)).arg(
|
||||
nonce_authority_arg()
|
||||
.requires(NONCE_ARG.name)
|
||||
.global(global),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
use {
|
||||
crate::{input_validators::*, ArgConstant},
|
||||
clap::{Arg, Command},
|
||||
};
|
||||
|
||||
pub const BLOCKHASH_ARG: ArgConstant<'static> = ArgConstant {
|
||||
name: "blockhash",
|
||||
long: "blockhash",
|
||||
help: "Use the supplied blockhash",
|
||||
};
|
||||
|
||||
pub const SIGN_ONLY_ARG: ArgConstant<'static> = ArgConstant {
|
||||
name: "sign_only",
|
||||
long: "sign-only",
|
||||
help: "Sign the transaction offline",
|
||||
};
|
||||
|
||||
pub const SIGNER_ARG: ArgConstant<'static> = ArgConstant {
|
||||
name: "signer",
|
||||
long: "signer",
|
||||
help: "Provide a public-key/signature pair for the transaction",
|
||||
};
|
||||
|
||||
pub const DUMP_TRANSACTION_MESSAGE: ArgConstant<'static> = ArgConstant {
|
||||
name: "dump_transaction_message",
|
||||
long: "dump-transaction-message",
|
||||
help: "Display the base64 encoded binary transaction message in sign-only mode",
|
||||
};
|
||||
|
||||
pub fn blockhash_arg<'a>() -> Arg<'a> {
|
||||
Arg::new(BLOCKHASH_ARG.name)
|
||||
.long(BLOCKHASH_ARG.long)
|
||||
.takes_value(true)
|
||||
.value_name("BLOCKHASH")
|
||||
.validator(|s| is_hash(s))
|
||||
.help(BLOCKHASH_ARG.help)
|
||||
}
|
||||
|
||||
pub fn sign_only_arg<'a>() -> Arg<'a> {
|
||||
Arg::new(SIGN_ONLY_ARG.name)
|
||||
.long(SIGN_ONLY_ARG.long)
|
||||
.takes_value(false)
|
||||
.requires(BLOCKHASH_ARG.name)
|
||||
.help(SIGN_ONLY_ARG.help)
|
||||
}
|
||||
|
||||
fn signer_arg<'a>() -> Arg<'a> {
|
||||
Arg::new(SIGNER_ARG.name)
|
||||
.long(SIGNER_ARG.long)
|
||||
.takes_value(true)
|
||||
.value_name("PUBKEY=SIGNATURE")
|
||||
.validator(|s| is_pubkey_sig(s))
|
||||
.requires(BLOCKHASH_ARG.name)
|
||||
.multiple_occurrences(true)
|
||||
.multiple_values(true)
|
||||
.help(SIGNER_ARG.help)
|
||||
}
|
||||
|
||||
pub fn dump_transaction_message<'a>() -> Arg<'a> {
|
||||
Arg::new(DUMP_TRANSACTION_MESSAGE.name)
|
||||
.long(DUMP_TRANSACTION_MESSAGE.long)
|
||||
.takes_value(false)
|
||||
.requires(SIGN_ONLY_ARG.name)
|
||||
.help(DUMP_TRANSACTION_MESSAGE.help)
|
||||
}
|
||||
|
||||
pub trait ArgsConfig {
|
||||
fn blockhash_arg<'a>(&self, arg: Arg<'a>) -> Arg<'a> {
|
||||
arg
|
||||
}
|
||||
fn sign_only_arg<'a>(&self, arg: Arg<'a>) -> Arg<'a> {
|
||||
arg
|
||||
}
|
||||
fn signer_arg<'a>(&self, arg: Arg<'a>) -> Arg<'a> {
|
||||
arg
|
||||
}
|
||||
fn dump_transaction_message_arg<'a>(&self, arg: Arg<'a>) -> Arg<'a> {
|
||||
arg
|
||||
}
|
||||
}
|
||||
|
||||
pub trait OfflineArgs {
|
||||
fn offline_args(self) -> Self;
|
||||
fn offline_args_config(self, config: &dyn ArgsConfig) -> Self;
|
||||
}
|
||||
|
||||
impl OfflineArgs for Command<'_> {
|
||||
fn offline_args_config(self, config: &dyn ArgsConfig) -> Self {
|
||||
self.arg(config.blockhash_arg(blockhash_arg()))
|
||||
.arg(config.sign_only_arg(sign_only_arg()))
|
||||
.arg(config.signer_arg(signer_arg()))
|
||||
.arg(config.dump_transaction_message_arg(dump_transaction_message()))
|
||||
}
|
||||
fn offline_args(self) -> Self {
|
||||
struct NullArgsConfig {}
|
||||
impl ArgsConfig for NullArgsConfig {}
|
||||
self.offline_args_config(&NullArgsConfig {})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue