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