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, ops::RangeBounds, str::FromStr}, }; fn is_parsable_generic(string: T) -> Result<(), String> where T: AsRef + Display, U: FromStr, U::Err: Display, { string .as_ref() .parse::() .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(string: String) -> Result<(), String> where T: FromStr, T::Err: Display, { is_parsable_generic::(string) } // Return an error if string cannot be parsed as numeric type T, and value not within specified // range pub fn is_within_range(string: String, range: R) -> Result<(), String> where T: FromStr + Copy + std::fmt::Debug + PartialOrd + std::ops::Add + From, T::Err: Display, R: RangeBounds + std::fmt::Debug, { match string.parse::() { Ok(input) => { if !range.contains(&input) { Err(format!("input '{input:?}' out of range {range:?}")) } else { Ok(()) } } Err(err) => Err(format!("error parsing '{string}': {err}")), } } // Return an error if a pubkey cannot be parsed. pub fn is_pubkey(string: T) -> Result<(), String> where T: AsRef + Display, { is_parsable_generic::(string) } // Return an error if a hash cannot be parsed. pub fn is_hash(string: T) -> Result<(), String> where T: AsRef + Display, { is_parsable_generic::(string) } // Return an error if a keypair file cannot be parsed. pub fn is_keypair(string: T) -> Result<(), String> where T: AsRef + 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(string: T) -> Result<(), String> where T: AsRef + 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(string: T) -> Result<(), String> where T: AsRef + 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(string: T) -> Result<(), String> where T: AsRef + 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(string: T) -> Result<(), String> where T: AsRef + 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(string: T) -> Result<(), String> where T: AsRef + Display, { is_valid_pubkey(string) } // Return an error if string cannot be parsed as pubkey=signature string pub fn is_pubkey_sig(string: T) -> Result<(), String> where T: AsRef + 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(string: T) -> Result<(), String> where T: AsRef + 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(string: T) -> Result<(), String> where T: AsRef + 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>(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(epoch: T) -> Result<(), String> where T: AsRef + Display, { is_parsable_generic::(epoch) } pub fn is_slot(slot: T) -> Result<(), String> where T: AsRef + Display, { is_parsable_generic::(slot) } pub fn is_pow2(bins: T) -> Result<(), String> where T: AsRef + Display, { bins.as_ref() .parse::() .map_err(|e| format!("Unable to parse, provided: {bins}, err: {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(port: T) -> Result<(), String> where T: AsRef + Display, { is_parsable_generic::(port) } pub fn is_valid_percentage(percentage: T) -> Result<(), String> where T: AsRef + Display, { percentage .as_ref() .parse::() .map_err(|e| format!("Unable to parse input percentage, provided: {percentage}, err: {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(amount: T) -> Result<(), String> where T: AsRef + Display, { if amount.as_ref().parse::().is_ok() || amount.as_ref().parse::().is_ok() { Ok(()) } else { Err(format!( "Unable to parse input amount as integer or float, provided: {amount}" )) } } pub fn is_amount_or_all(amount: T) -> Result<(), String> where T: AsRef + Display, { if amount.as_ref().parse::().is_ok() || amount.as_ref().parse::().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(value: T) -> Result<(), String> where T: AsRef + Display, { DateTime::parse_from_rfc3339(value.as_ref()) .map(|_| ()) .map_err(|e| format!("{e}")) } pub fn is_derivation(value: T) -> Result<(), String> where T: AsRef + Display, { let value = value.as_ref().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) } }) .map(|_| ()) } pub fn is_derived_address_seed(value: T) -> Result<(), String> where T: AsRef + Display, { let value = value.as_ref(); if value.len() > MAX_SEED_LEN { Err(format!( "Address seed must not be longer than {MAX_SEED_LEN} bytes" )) } else { Ok(()) } } pub fn is_niceness_adjustment_valid(value: T) -> Result<(), String> where T: AsRef + Display, { let adjustment = value .as_ref() .parse::() .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)", )) } } pub fn validate_maximum_full_snapshot_archives_to_retain(value: T) -> Result<(), String> where T: AsRef + Display, { let value = value.as_ref(); if value.eq("0") { Err(String::from( "--maximum-full-snapshot-archives-to-retain cannot be zero", )) } else { Ok(()) } } pub fn validate_maximum_incremental_snapshot_archives_to_retain(value: T) -> Result<(), String> where T: AsRef + Display, { let value = value.as_ref(); if value.eq("0") { Err(String::from( "--maximum-incremental-snapshot-archives-to-retain cannot be zero", )) } else { 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()); } #[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()); } }