solana_clap_utils/
input_validators.rs

1use {
2    crate::keypair::{parse_signer_source, SignerSourceKind, ASK_KEYWORD},
3    chrono::DateTime,
4    solana_clock::{Epoch, Slot},
5    solana_hash::Hash,
6    solana_keypair::read_keypair_file,
7    solana_pubkey::{Pubkey, MAX_SEED_LEN},
8    solana_signature::Signature,
9    std::{fmt::Display, ops::RangeBounds, str::FromStr},
10};
11
12fn is_parsable_generic<U, T>(string: T) -> Result<(), String>
13where
14    T: AsRef<str> + Display,
15    U: FromStr,
16    U::Err: Display,
17{
18    string
19        .as_ref()
20        .parse::<U>()
21        .map(|_| ())
22        .map_err(|err| format!("error parsing '{string}': {err}"))
23}
24
25// Return an error if string cannot be parsed as type T.
26// Takes a String to avoid second type parameter when used as a clap validator
27pub fn is_parsable<T>(string: String) -> Result<(), String>
28where
29    T: FromStr,
30    T::Err: Display,
31{
32    is_parsable_generic::<T, String>(string)
33}
34
35// Return an error if string cannot be parsed as numeric type T, and value not within specified
36// range
37pub fn is_within_range<T, R>(string: String, range: R) -> Result<(), String>
38where
39    T: FromStr + Copy + std::fmt::Debug + PartialOrd + std::ops::Add<Output = T> + From<usize>,
40    T::Err: Display,
41    R: RangeBounds<T> + std::fmt::Debug,
42{
43    match string.parse::<T>() {
44        Ok(input) => {
45            if !range.contains(&input) {
46                Err(format!("input '{input:?}' out of range {range:?}"))
47            } else {
48                Ok(())
49            }
50        }
51        Err(err) => Err(format!("error parsing '{string}': {err}")),
52    }
53}
54
55// Return an error if a pubkey cannot be parsed.
56pub fn is_pubkey<T>(string: T) -> Result<(), String>
57where
58    T: AsRef<str> + Display,
59{
60    is_parsable_generic::<Pubkey, _>(string)
61}
62
63// Return an error if a hash cannot be parsed.
64pub fn is_hash<T>(string: T) -> Result<(), String>
65where
66    T: AsRef<str> + Display,
67{
68    is_parsable_generic::<Hash, _>(string)
69}
70
71// Return an error if a keypair file cannot be parsed.
72pub fn is_keypair<T>(string: T) -> Result<(), String>
73where
74    T: AsRef<str> + Display,
75{
76    read_keypair_file(string.as_ref())
77        .map(|_| ())
78        .map_err(|err| format!("{err}"))
79}
80
81// Return an error if a keypair file cannot be parsed
82pub fn is_keypair_or_ask_keyword<T>(string: T) -> Result<(), String>
83where
84    T: AsRef<str> + Display,
85{
86    if string.as_ref() == ASK_KEYWORD {
87        return Ok(());
88    }
89    read_keypair_file(string.as_ref())
90        .map(|_| ())
91        .map_err(|err| format!("{err}"))
92}
93
94// Return an error if a `SignerSourceKind::Prompt` cannot be parsed
95pub fn is_prompt_signer_source<T>(string: T) -> Result<(), String>
96where
97    T: AsRef<str> + Display,
98{
99    if string.as_ref() == ASK_KEYWORD {
100        return Ok(());
101    }
102    match parse_signer_source(string.as_ref())
103        .map_err(|err| format!("{err}"))?
104        .kind
105    {
106        SignerSourceKind::Prompt => Ok(()),
107        _ => Err(format!(
108            "Unable to parse input as `prompt:` URI scheme or `ASK` keyword: {string}"
109        )),
110    }
111}
112
113// Return an error if string cannot be parsed as pubkey string or keypair file location
114pub fn is_pubkey_or_keypair<T>(string: T) -> Result<(), String>
115where
116    T: AsRef<str> + Display,
117{
118    is_pubkey(string.as_ref()).or_else(|_| is_keypair(string))
119}
120
121// Return an error if string cannot be parsed as a pubkey string, or a valid Signer that can
122// produce a pubkey()
123pub fn is_valid_pubkey<T>(string: T) -> Result<(), String>
124where
125    T: AsRef<str> + Display,
126{
127    match parse_signer_source(string.as_ref())
128        .map_err(|err| format!("{err}"))?
129        .kind
130    {
131        SignerSourceKind::Filepath(path) => is_keypair(path),
132        _ => Ok(()),
133    }
134}
135
136// Return an error if string cannot be parsed as a valid Signer. This is an alias of
137// `is_valid_pubkey`, and does accept pubkey strings, even though a Pubkey is not by itself
138// sufficient to sign a transaction.
139//
140// In the current offline-signing implementation, a pubkey is the valid input for a signer field
141// when paired with an offline `--signer` argument to provide a Presigner (pubkey + signature).
142// Clap validators can't check multiple fields at once, so the verification that a `--signer` is
143// also provided and correct happens in parsing, not in validation.
144pub fn is_valid_signer<T>(string: T) -> Result<(), String>
145where
146    T: AsRef<str> + Display,
147{
148    is_valid_pubkey(string)
149}
150
151// Return an error if string cannot be parsed as pubkey=signature string
152pub fn is_pubkey_sig<T>(string: T) -> Result<(), String>
153where
154    T: AsRef<str> + Display,
155{
156    let mut signer = string.as_ref().split('=');
157    match Pubkey::from_str(
158        signer
159            .next()
160            .ok_or_else(|| "Malformed signer string".to_string())?,
161    ) {
162        Ok(_) => {
163            match Signature::from_str(
164                signer
165                    .next()
166                    .ok_or_else(|| "Malformed signer string".to_string())?,
167            ) {
168                Ok(_) => Ok(()),
169                Err(err) => Err(format!("{err}")),
170            }
171        }
172        Err(err) => Err(format!("{err}")),
173    }
174}
175
176// Return an error if a url cannot be parsed.
177pub fn is_url<T>(string: T) -> Result<(), String>
178where
179    T: AsRef<str> + Display,
180{
181    match url::Url::parse(string.as_ref()) {
182        Ok(url) => {
183            if url.has_host() {
184                Ok(())
185            } else {
186                Err("no host provided".to_string())
187            }
188        }
189        Err(err) => Err(format!("{err}")),
190    }
191}
192
193pub fn is_url_or_moniker<T>(string: T) -> Result<(), String>
194where
195    T: AsRef<str> + Display,
196{
197    match url::Url::parse(&normalize_to_url_if_moniker(string.as_ref())) {
198        Ok(url) => {
199            if url.has_host() {
200                Ok(())
201            } else {
202                Err("no host provided".to_string())
203            }
204        }
205        Err(err) => Err(format!("{err}")),
206    }
207}
208
209pub fn normalize_to_url_if_moniker<T: AsRef<str>>(url_or_moniker: T) -> String {
210    match url_or_moniker.as_ref() {
211        "m" | "mainnet-beta" => "https://api.mainnet-beta.solana.com",
212        "t" | "testnet" => "https://api.testnet.solana.com",
213        "d" | "devnet" => "https://api.devnet.solana.com",
214        "l" | "localhost" => "http://localhost:8899",
215        url => url,
216    }
217    .to_string()
218}
219
220pub fn is_epoch<T>(epoch: T) -> Result<(), String>
221where
222    T: AsRef<str> + Display,
223{
224    is_parsable_generic::<Epoch, _>(epoch)
225}
226
227pub fn is_slot<T>(slot: T) -> Result<(), String>
228where
229    T: AsRef<str> + Display,
230{
231    is_parsable_generic::<Slot, _>(slot)
232}
233
234pub fn is_pow2<T>(bins: T) -> Result<(), String>
235where
236    T: AsRef<str> + Display,
237{
238    bins.as_ref()
239        .parse::<usize>()
240        .map_err(|e| format!("Unable to parse, provided: {bins}, err: {e}"))
241        .and_then(|v| {
242            if !v.is_power_of_two() {
243                Err(format!("Must be a power of 2: {v}"))
244            } else {
245                Ok(())
246            }
247        })
248}
249
250pub fn is_port<T>(port: T) -> Result<(), String>
251where
252    T: AsRef<str> + Display,
253{
254    is_parsable_generic::<u16, _>(port)
255}
256
257pub fn is_valid_percentage<T>(percentage: T) -> Result<(), String>
258where
259    T: AsRef<str> + Display,
260{
261    percentage
262        .as_ref()
263        .parse::<u8>()
264        .map_err(|e| format!("Unable to parse input percentage, provided: {percentage}, err: {e}"))
265        .and_then(|v| {
266            if v > 100 {
267                Err(format!(
268                    "Percentage must be in range of 0 to 100, provided: {v}"
269                ))
270            } else {
271                Ok(())
272            }
273        })
274}
275
276pub fn is_amount<T>(amount: T) -> Result<(), String>
277where
278    T: AsRef<str> + Display,
279{
280    if amount.as_ref().parse::<u64>().is_ok() || amount.as_ref().parse::<f64>().is_ok() {
281        Ok(())
282    } else {
283        Err(format!(
284            "Unable to parse input amount as integer or float, provided: {amount}"
285        ))
286    }
287}
288
289pub fn is_amount_or_all<T>(amount: T) -> Result<(), String>
290where
291    T: AsRef<str> + Display,
292{
293    if amount.as_ref().parse::<u64>().is_ok()
294        || amount.as_ref().parse::<f64>().is_ok()
295        || amount.as_ref() == "ALL"
296    {
297        Ok(())
298    } else {
299        Err(format!(
300            "Unable to parse input amount as integer or float, provided: {amount}"
301        ))
302    }
303}
304
305pub fn is_rfc3339_datetime<T>(value: T) -> Result<(), String>
306where
307    T: AsRef<str> + Display,
308{
309    DateTime::parse_from_rfc3339(value.as_ref())
310        .map(|_| ())
311        .map_err(|e| format!("{e}"))
312}
313
314pub fn is_derivation<T>(value: T) -> Result<(), String>
315where
316    T: AsRef<str> + Display,
317{
318    let value = value.as_ref().replace('\'', "");
319    let mut parts = value.split('/');
320    let account = parts.next().unwrap();
321    account
322        .parse::<u32>()
323        .map_err(|e| format!("Unable to parse derivation, provided: {account}, err: {e}"))
324        .and_then(|_| {
325            if let Some(change) = parts.next() {
326                change.parse::<u32>().map_err(|e| {
327                    format!("Unable to parse derivation, provided: {change}, err: {e}")
328                })
329            } else {
330                Ok(0)
331            }
332        })
333        .map(|_| ())
334}
335
336pub fn is_structured_seed<T>(value: T) -> Result<(), String>
337where
338    T: AsRef<str> + Display,
339{
340    let (prefix, value) = value
341        .as_ref()
342        .split_once(':')
343        .ok_or("Seed must contain ':' as delimiter")
344        .unwrap();
345    if prefix.is_empty() || value.is_empty() {
346        Err(String::from("Seed prefix or value is empty"))
347    } else {
348        match prefix {
349            "string" | "pubkey" | "hex" | "u8" => Ok(()),
350            _ => {
351                let len = prefix.len();
352                if len != 5 && len != 6 {
353                    Err(format!("Wrong prefix length {len} {prefix}:{value}"))
354                } else {
355                    let sign = &prefix[0..1];
356                    let type_size = &prefix[1..len.saturating_sub(2)];
357                    let byte_order = &prefix[len.saturating_sub(2)..len];
358                    if sign != "u" && sign != "i" {
359                        Err(format!("Wrong prefix sign {sign} {prefix}:{value}"))
360                    } else if type_size != "16"
361                        && type_size != "32"
362                        && type_size != "64"
363                        && type_size != "128"
364                    {
365                        Err(format!(
366                            "Wrong prefix type size {type_size} {prefix}:{value}"
367                        ))
368                    } else if byte_order != "le" && byte_order != "be" {
369                        Err(format!(
370                            "Wrong prefix byte order {byte_order} {prefix}:{value}"
371                        ))
372                    } else {
373                        Ok(())
374                    }
375                }
376            }
377        }
378    }
379}
380
381pub fn is_derived_address_seed<T>(value: T) -> Result<(), String>
382where
383    T: AsRef<str> + Display,
384{
385    let value = value.as_ref();
386    if value.len() > MAX_SEED_LEN {
387        Err(format!(
388            "Address seed must not be longer than {MAX_SEED_LEN} bytes"
389        ))
390    } else {
391        Ok(())
392    }
393}
394
395pub fn validate_maximum_full_snapshot_archives_to_retain<T>(value: T) -> Result<(), String>
396where
397    T: AsRef<str> + Display,
398{
399    let value = value.as_ref();
400    if value.eq("0") {
401        Err(String::from(
402            "--maximum-full-snapshot-archives-to-retain cannot be zero",
403        ))
404    } else {
405        Ok(())
406    }
407}
408
409pub fn validate_maximum_incremental_snapshot_archives_to_retain<T>(value: T) -> Result<(), String>
410where
411    T: AsRef<str> + Display,
412{
413    let value = value.as_ref();
414    if value.eq("0") {
415        Err(String::from(
416            "--maximum-incremental-snapshot-archives-to-retain cannot be zero",
417        ))
418    } else {
419        Ok(())
420    }
421}
422
423#[cfg(test)]
424mod tests {
425    use super::*;
426
427    #[test]
428    fn test_is_derivation() {
429        assert_eq!(is_derivation("2"), Ok(()));
430        assert_eq!(is_derivation("0"), Ok(()));
431        assert_eq!(is_derivation("65537"), Ok(()));
432        assert_eq!(is_derivation("0/2"), Ok(()));
433        assert_eq!(is_derivation("0'/2'"), Ok(()));
434        assert!(is_derivation("a").is_err());
435        assert!(is_derivation("4294967296").is_err());
436        assert!(is_derivation("a/b").is_err());
437        assert!(is_derivation("0/4294967296").is_err());
438    }
439}