solana_clap_utils/
input_parsers.rs

1use {
2    crate::keypair::{
3        keypair_from_seed_phrase, pubkey_from_path, resolve_signer_from_path, signer_from_path,
4        ASK_KEYWORD, SKIP_SEED_PHRASE_VALIDATION_ARG,
5    },
6    chrono::DateTime,
7    clap::ArgMatches,
8    solana_bls_signatures::Pubkey as BLSPubkey,
9    solana_clock::UnixTimestamp,
10    solana_cluster_type::ClusterType,
11    solana_commitment_config::CommitmentConfig,
12    solana_keypair::{read_keypair_file, Keypair},
13    solana_native_token::LAMPORTS_PER_SOL,
14    solana_pubkey::Pubkey,
15    solana_remote_wallet::remote_wallet::RemoteWalletManager,
16    solana_signature::Signature,
17    solana_signer::Signer,
18    std::{io, num::ParseIntError, rc::Rc, str::FromStr},
19};
20
21// Sentinel value used to indicate to write to screen instead of file
22pub const STDOUT_OUTFILE_TOKEN: &str = "-";
23
24// Return parsed values from matches at `name`
25pub fn values_of<T>(matches: &ArgMatches<'_>, name: &str) -> Option<Vec<T>>
26where
27    T: std::str::FromStr,
28    <T as std::str::FromStr>::Err: std::fmt::Debug,
29{
30    matches
31        .values_of(name)
32        .map(|xs| xs.map(|x| x.parse::<T>().unwrap()).collect())
33}
34
35// Return a parsed value from matches at `name`
36pub fn value_of<T>(matches: &ArgMatches<'_>, name: &str) -> Option<T>
37where
38    T: std::str::FromStr,
39    <T as std::str::FromStr>::Err: std::fmt::Debug,
40{
41    if let Some(value) = matches.value_of(name) {
42        value.parse::<T>().ok()
43    } else {
44        None
45    }
46}
47
48pub fn unix_timestamp_from_rfc3339_datetime(
49    matches: &ArgMatches<'_>,
50    name: &str,
51) -> Option<UnixTimestamp> {
52    matches.value_of(name).and_then(|value| {
53        DateTime::parse_from_rfc3339(value)
54            .ok()
55            .map(|date_time| date_time.timestamp())
56    })
57}
58
59// Return the keypair for an argument with filename `name` or None if not present.
60pub fn keypair_of(matches: &ArgMatches<'_>, name: &str) -> Option<Keypair> {
61    if let Some(value) = matches.value_of(name) {
62        if value == ASK_KEYWORD {
63            let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name);
64            keypair_from_seed_phrase(name, skip_validation, true, None, true).ok()
65        } else {
66            read_keypair_file(value).ok()
67        }
68    } else {
69        None
70    }
71}
72
73pub fn keypairs_of(matches: &ArgMatches<'_>, name: &str) -> Option<Vec<Keypair>> {
74    matches.values_of(name).map(|values| {
75        values
76            .filter_map(|value| {
77                if value == ASK_KEYWORD {
78                    let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name);
79                    keypair_from_seed_phrase(name, skip_validation, true, None, true).ok()
80                } else {
81                    read_keypair_file(value).ok()
82                }
83            })
84            .collect()
85    })
86}
87
88// Return a pubkey for an argument that can itself be parsed into a pubkey,
89// or is a filename that can be read as a keypair
90pub fn pubkey_of(matches: &ArgMatches<'_>, name: &str) -> Option<Pubkey> {
91    value_of(matches, name).or_else(|| keypair_of(matches, name).map(|keypair| keypair.pubkey()))
92}
93
94pub fn pubkeys_of(matches: &ArgMatches<'_>, name: &str) -> Option<Vec<Pubkey>> {
95    matches.values_of(name).map(|values| {
96        values
97            .map(|value| {
98                value.parse::<Pubkey>().unwrap_or_else(|_| {
99                    read_keypair_file(value)
100                        .expect("read_keypair_file failed")
101                        .pubkey()
102                })
103            })
104            .collect()
105    })
106}
107
108pub fn bls_pubkeys_of(matches: &ArgMatches<'_>, name: &str) -> Option<Vec<BLSPubkey>> {
109    matches.values_of(name).map(|values| {
110        values
111            .map(|value| {
112                BLSPubkey::from_str(value).unwrap_or_else(|_| {
113                    panic!("Failed to parse BLS public key from value: {value}")
114                })
115            })
116            .collect()
117    })
118}
119
120// Return pubkey/signature pairs for a string of the form pubkey=signature
121pub fn pubkeys_sigs_of(matches: &ArgMatches<'_>, name: &str) -> Option<Vec<(Pubkey, Signature)>> {
122    matches.values_of(name).map(|values| {
123        values
124            .map(|pubkey_signer_string| {
125                let mut signer = pubkey_signer_string.split('=');
126                let key = Pubkey::from_str(signer.next().unwrap()).unwrap();
127                let sig = Signature::from_str(signer.next().unwrap()).unwrap();
128                (key, sig)
129            })
130            .collect()
131    })
132}
133
134// Return a signer from matches at `name`
135#[allow(clippy::type_complexity)]
136pub fn signer_of(
137    matches: &ArgMatches<'_>,
138    name: &str,
139    wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
140) -> Result<(Option<Box<dyn Signer>>, Option<Pubkey>), Box<dyn std::error::Error>> {
141    if let Some(location) = matches.value_of(name) {
142        let signer = signer_from_path(matches, location, name, wallet_manager)?;
143        let signer_pubkey = signer.pubkey();
144        Ok((Some(signer), Some(signer_pubkey)))
145    } else {
146        Ok((None, None))
147    }
148}
149
150pub fn pubkey_of_signer(
151    matches: &ArgMatches<'_>,
152    name: &str,
153    wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
154) -> Result<Option<Pubkey>, Box<dyn std::error::Error>> {
155    if let Some(location) = matches.value_of(name) {
156        Ok(Some(pubkey_from_path(
157            matches,
158            location,
159            name,
160            wallet_manager,
161        )?))
162    } else {
163        Ok(None)
164    }
165}
166
167pub fn pubkeys_of_multiple_signers(
168    matches: &ArgMatches<'_>,
169    name: &str,
170    wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
171) -> Result<Option<Vec<Pubkey>>, Box<dyn std::error::Error>> {
172    if let Some(pubkey_matches) = matches.values_of(name) {
173        let mut pubkeys: Vec<Pubkey> = vec![];
174        for signer in pubkey_matches {
175            pubkeys.push(pubkey_from_path(matches, signer, name, wallet_manager)?);
176        }
177        Ok(Some(pubkeys))
178    } else {
179        Ok(None)
180    }
181}
182
183pub fn resolve_signer(
184    matches: &ArgMatches<'_>,
185    name: &str,
186    wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
187) -> Result<Option<String>, Box<dyn std::error::Error>> {
188    resolve_signer_from_path(
189        matches,
190        matches.value_of(name).unwrap(),
191        name,
192        wallet_manager,
193    )
194}
195
196/// Convert a SOL amount string to lamports.
197///
198/// Accepts plain or decimal strings ("50", "0.03", ".5", "1.").
199/// Any decimal places beyond 9 are truncated.
200pub fn lamports_of_sol(matches: &ArgMatches<'_>, name: &str) -> Option<u64> {
201    matches.value_of(name).and_then(|value| {
202        if value == "." {
203            None
204        } else {
205            let (sol, lamports) = value.split_once('.').unwrap_or((value, ""));
206            let sol = if sol.is_empty() {
207                0
208            } else {
209                sol.parse::<u64>().ok()?
210            };
211            let lamports = if lamports.is_empty() {
212                0
213            } else {
214                format!("{lamports:0<9}")[..9].parse().ok()?
215            };
216            Some(
217                LAMPORTS_PER_SOL
218                    .saturating_mul(sol)
219                    .saturating_add(lamports),
220            )
221        }
222    })
223}
224
225pub fn cluster_type_of(matches: &ArgMatches<'_>, name: &str) -> Option<ClusterType> {
226    value_of(matches, name)
227}
228
229pub fn commitment_of(matches: &ArgMatches<'_>, name: &str) -> Option<CommitmentConfig> {
230    matches
231        .value_of(name)
232        .map(|value| CommitmentConfig::from_str(value).unwrap_or_default())
233}
234
235// Parse a cpu range in standard cpuset format, eg:
236//
237// 0-4,9
238// 0-2,7,12-14
239pub fn parse_cpu_ranges(data: &str) -> Result<Vec<usize>, io::Error> {
240    data.split(',')
241        .map(|range| {
242            let mut iter = range
243                .split('-')
244                .map(|s| s.parse::<usize>().map_err(|ParseIntError { .. }| range));
245            let start = iter.next().unwrap()?; // str::split always returns at least one element.
246            let end = match iter.next() {
247                None => start,
248                Some(end) => {
249                    if iter.next().is_some() {
250                        return Err(range);
251                    }
252                    end?
253                }
254            };
255            Ok(start..=end)
256        })
257        .try_fold(Vec::new(), |mut cpus, range| {
258            let range = range.map_err(|range| io::Error::new(io::ErrorKind::InvalidData, range))?;
259            cpus.extend(range);
260            Ok(cpus)
261        })
262}
263
264#[cfg(test)]
265mod tests {
266    use {
267        super::*,
268        clap::{App, Arg},
269        solana_bls_signatures::{keypair::Keypair as BLSKeypair, Pubkey as BLSPubkey},
270        solana_keypair::write_keypair_file,
271        std::fs,
272    };
273
274    fn app<'ab, 'v>() -> App<'ab, 'v> {
275        App::new("test")
276            .arg(
277                Arg::with_name("multiple")
278                    .long("multiple")
279                    .takes_value(true)
280                    .multiple(true),
281            )
282            .arg(Arg::with_name("single").takes_value(true).long("single"))
283            .arg(Arg::with_name("unit").takes_value(true).long("unit"))
284    }
285
286    fn tmp_file_path(name: &str, pubkey: &Pubkey) -> String {
287        use std::env;
288        let out_dir = env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_string());
289
290        format!("{out_dir}/tmp/{name}-{pubkey}")
291    }
292
293    #[test]
294    fn test_values_of() {
295        let matches = app().get_matches_from(vec!["test", "--multiple", "50", "--multiple", "39"]);
296        assert_eq!(values_of(&matches, "multiple"), Some(vec![50, 39]));
297        assert_eq!(values_of::<u64>(&matches, "single"), None);
298
299        let pubkey0 = solana_pubkey::new_rand();
300        let pubkey1 = solana_pubkey::new_rand();
301        let matches = app().get_matches_from(vec![
302            "test",
303            "--multiple",
304            &pubkey0.to_string(),
305            "--multiple",
306            &pubkey1.to_string(),
307        ]);
308        assert_eq!(
309            values_of(&matches, "multiple"),
310            Some(vec![pubkey0, pubkey1])
311        );
312    }
313
314    #[test]
315    fn test_value_of() {
316        let matches = app().get_matches_from(vec!["test", "--single", "50"]);
317        assert_eq!(value_of(&matches, "single"), Some(50));
318        assert_eq!(value_of::<u64>(&matches, "multiple"), None);
319
320        let pubkey = solana_pubkey::new_rand();
321        let matches = app().get_matches_from(vec!["test", "--single", &pubkey.to_string()]);
322        assert_eq!(value_of(&matches, "single"), Some(pubkey));
323    }
324
325    #[test]
326    fn test_keypair_of() {
327        let keypair = Keypair::new();
328        let outfile = tmp_file_path("test_keypair_of.json", &keypair.pubkey());
329        let _ = write_keypair_file(&keypair, &outfile).unwrap();
330
331        let matches = app().get_matches_from(vec!["test", "--single", &outfile]);
332        assert_eq!(
333            keypair_of(&matches, "single").unwrap().pubkey(),
334            keypair.pubkey()
335        );
336        assert!(keypair_of(&matches, "multiple").is_none());
337
338        let matches = app().get_matches_from(vec!["test", "--single", "random_keypair_file.json"]);
339        assert!(keypair_of(&matches, "single").is_none());
340
341        fs::remove_file(&outfile).unwrap();
342    }
343
344    #[test]
345    fn test_pubkey_of() {
346        let keypair = Keypair::new();
347        let outfile = tmp_file_path("test_pubkey_of.json", &keypair.pubkey());
348        let _ = write_keypair_file(&keypair, &outfile).unwrap();
349
350        let matches = app().get_matches_from(vec!["test", "--single", &outfile]);
351        assert_eq!(pubkey_of(&matches, "single"), Some(keypair.pubkey()));
352        assert_eq!(pubkey_of(&matches, "multiple"), None);
353
354        let matches =
355            app().get_matches_from(vec!["test", "--single", &keypair.pubkey().to_string()]);
356        assert_eq!(pubkey_of(&matches, "single"), Some(keypair.pubkey()));
357
358        let matches = app().get_matches_from(vec!["test", "--single", "random_keypair_file.json"]);
359        assert_eq!(pubkey_of(&matches, "single"), None);
360
361        fs::remove_file(&outfile).unwrap();
362    }
363
364    #[test]
365    fn test_pubkeys_of() {
366        let keypair = Keypair::new();
367        let outfile = tmp_file_path("test_pubkeys_of.json", &keypair.pubkey());
368        let _ = write_keypair_file(&keypair, &outfile).unwrap();
369
370        let matches = app().get_matches_from(vec![
371            "test",
372            "--multiple",
373            &keypair.pubkey().to_string(),
374            "--multiple",
375            &outfile,
376        ]);
377        assert_eq!(
378            pubkeys_of(&matches, "multiple"),
379            Some(vec![keypair.pubkey(), keypair.pubkey()])
380        );
381        fs::remove_file(&outfile).unwrap();
382    }
383
384    #[test]
385    fn test_pubkeys_sigs_of() {
386        let key1 = solana_pubkey::new_rand();
387        let key2 = solana_pubkey::new_rand();
388        let sig1 = Keypair::new().sign_message(&[0u8]);
389        let sig2 = Keypair::new().sign_message(&[1u8]);
390        let signer1 = format!("{key1}={sig1}");
391        let signer2 = format!("{key2}={sig2}");
392        let matches =
393            app().get_matches_from(vec!["test", "--multiple", &signer1, "--multiple", &signer2]);
394        assert_eq!(
395            pubkeys_sigs_of(&matches, "multiple"),
396            Some(vec![(key1, sig1), (key2, sig2)])
397        );
398    }
399
400    #[test]
401    #[ignore = "historical reference; shows float behavior fixed in pull #4988"]
402    fn test_lamports_of_sol_origin() {
403        use solana_native_token::sol_str_to_lamports;
404        pub fn lamports_of_sol(matches: &ArgMatches<'_>, name: &str) -> Option<u64> {
405            matches.value_of(name).and_then(sol_str_to_lamports)
406        }
407
408        let matches = app().get_matches_from(vec!["test", "--single", "50"]);
409        assert_eq!(lamports_of_sol(&matches, "single"), Some(50_000_000_000));
410        assert_eq!(lamports_of_sol(&matches, "multiple"), None);
411        let matches = app().get_matches_from(vec!["test", "--single", "1.5"]);
412        assert_eq!(lamports_of_sol(&matches, "single"), Some(1_500_000_000));
413        assert_eq!(lamports_of_sol(&matches, "multiple"), None);
414        let matches = app().get_matches_from(vec!["test", "--single", "0.03"]);
415        assert_eq!(lamports_of_sol(&matches, "single"), Some(30_000_000));
416        let matches = app().get_matches_from(vec!["test", "--single", ".03"]);
417        assert_eq!(lamports_of_sol(&matches, "single"), Some(30_000_000));
418        let matches = app().get_matches_from(vec!["test", "--single", "1."]);
419        assert_eq!(lamports_of_sol(&matches, "single"), Some(1_000_000_000));
420        let matches = app().get_matches_from(vec!["test", "--single", ".0"]);
421        assert_eq!(lamports_of_sol(&matches, "single"), Some(0));
422        let matches = app().get_matches_from(vec!["test", "--single", "."]);
423        assert_eq!(lamports_of_sol(&matches, "single"), None);
424        // NOT EQ
425        let matches = app().get_matches_from(vec!["test", "--single", "1.000000015"]);
426        assert_ne!(lamports_of_sol(&matches, "single"), Some(1_000_000_015));
427        let matches = app().get_matches_from(vec!["test", "--single", "0.0157"]);
428        assert_ne!(lamports_of_sol(&matches, "single"), Some(15_700_000));
429        let matches = app().get_matches_from(vec!["test", "--single", "0.5025"]);
430        assert_ne!(lamports_of_sol(&matches, "single"), Some(502_500_000));
431    }
432
433    #[test]
434    fn test_bls_pubkeys_of() {
435        let bls_pubkey1: BLSPubkey = BLSKeypair::new().public;
436        let bls_pubkey2: BLSPubkey = BLSKeypair::new().public;
437        let matches = app().get_matches_from(vec![
438            "test",
439            "--multiple",
440            &bls_pubkey1.to_string(),
441            "--multiple",
442            &bls_pubkey2.to_string(),
443        ]);
444        assert_eq!(
445            bls_pubkeys_of(&matches, "multiple"),
446            Some(vec![bls_pubkey1, bls_pubkey2])
447        );
448    }
449
450    #[test]
451    fn test_lamports_of_sol() {
452        let matches = app().get_matches_from(vec!["test", "--single", "50"]);
453        assert_eq!(lamports_of_sol(&matches, "single"), Some(50_000_000_000));
454        assert_eq!(lamports_of_sol(&matches, "multiple"), None);
455        let matches = app().get_matches_from(vec!["test", "--single", "1.5"]);
456        assert_eq!(lamports_of_sol(&matches, "single"), Some(1_500_000_000));
457        assert_eq!(lamports_of_sol(&matches, "multiple"), None);
458        let matches = app().get_matches_from(vec!["test", "--single", "0.03"]);
459        assert_eq!(lamports_of_sol(&matches, "single"), Some(30_000_000));
460        let matches = app().get_matches_from(vec!["test", "--single", ".03"]);
461        assert_eq!(lamports_of_sol(&matches, "single"), Some(30_000_000));
462        let matches = app().get_matches_from(vec!["test", "--single", "1."]);
463        assert_eq!(lamports_of_sol(&matches, "single"), Some(1_000_000_000));
464        let matches = app().get_matches_from(vec!["test", "--single", ".0"]);
465        assert_eq!(lamports_of_sol(&matches, "single"), Some(0));
466        let matches = app().get_matches_from(vec!["test", "--single", "."]);
467        assert_eq!(lamports_of_sol(&matches, "single"), None);
468        // EQ
469        let matches = app().get_matches_from(vec!["test", "--single", "1.000000015"]);
470        assert_eq!(lamports_of_sol(&matches, "single"), Some(1_000_000_015));
471        let matches = app().get_matches_from(vec!["test", "--single", "0.0157"]);
472        assert_eq!(lamports_of_sol(&matches, "single"), Some(15_700_000));
473        let matches = app().get_matches_from(vec!["test", "--single", "0.5025"]);
474        assert_eq!(lamports_of_sol(&matches, "single"), Some(502_500_000));
475        // Truncation of extra decimal places
476        let matches = app().get_matches_from(vec!["test", "--single", "0.1234567891"]);
477        assert_eq!(lamports_of_sol(&matches, "single"), Some(123_456_789));
478        let matches = app().get_matches_from(vec!["test", "--single", "0.1234567899"]);
479        assert_eq!(lamports_of_sol(&matches, "single"), Some(123_456_789));
480        let matches = app().get_matches_from(vec!["test", "--single", "1.000.4567899"]);
481        assert_eq!(lamports_of_sol(&matches, "single"), None);
482        let matches = app().get_matches_from(vec!["test", "--single", "6,998"]);
483        assert_eq!(lamports_of_sol(&matches, "single"), None);
484        let matches = app().get_matches_from(vec!["test", "--single", "6,998.00"]);
485        assert_eq!(lamports_of_sol(&matches, "single"), None);
486    }
487}