solana_clap_v3_utils/
input_validators.rs

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