solana_clap_utils/
keypair.rs

1//! Loading signers and keypairs from the command line.
2//!
3//! This module contains utilities for loading [Signer]s and [Keypair]s from
4//! standard signing sources, from the command line, as in the Solana CLI.
5//!
6//! The key function here is [`signer_from_path`], which loads a `Signer` from
7//! one of several possible sources by interpreting a "path" command line
8//! argument. Its documentation includes a description of all possible signing
9//! sources supported by the Solana CLI. Many other functions here are
10//! variations on, or delegate to, `signer_from_path`.
11
12use {
13    crate::{
14        input_parsers::{pubkeys_sigs_of, STDOUT_OUTFILE_TOKEN},
15        offline::{SIGNER_ARG, SIGN_ONLY_ARG},
16        ArgConstant,
17    },
18    bip39::{Language, Mnemonic, Seed},
19    clap::ArgMatches,
20    rpassword::prompt_password,
21    solana_derivation_path::{DerivationPath, DerivationPathError},
22    solana_hash::Hash,
23    solana_keypair::{
24        keypair_from_seed, keypair_from_seed_phrase_and_passphrase, read_keypair,
25        read_keypair_file, seed_derivable::keypair_from_seed_and_derivation_path, Keypair,
26    },
27    solana_message::Message,
28    solana_presigner::Presigner,
29    solana_pubkey::Pubkey,
30    solana_remote_wallet::{
31        locator::{Locator as RemoteWalletLocator, LocatorError as RemoteWalletLocatorError},
32        remote_keypair::generate_remote_keypair,
33        remote_wallet::{maybe_wallet_manager, RemoteWalletError, RemoteWalletManager},
34    },
35    solana_seed_phrase::generate_seed_from_seed_phrase_and_passphrase,
36    solana_signature::Signature,
37    solana_signer::{null_signer::NullSigner, Signer},
38    std::{
39        cell::RefCell,
40        convert::TryFrom,
41        error,
42        io::{stdin, stdout, Write},
43        ops::Deref,
44        process::exit,
45        rc::Rc,
46        str::FromStr,
47    },
48    thiserror::Error,
49};
50
51pub struct SignOnly {
52    pub blockhash: Hash,
53    pub message: Option<String>,
54    pub present_signers: Vec<(Pubkey, Signature)>,
55    pub absent_signers: Vec<Pubkey>,
56    pub bad_signers: Vec<Pubkey>,
57}
58
59impl SignOnly {
60    pub fn has_all_signers(&self) -> bool {
61        self.absent_signers.is_empty() && self.bad_signers.is_empty()
62    }
63
64    pub fn presigner_of(&self, pubkey: &Pubkey) -> Option<Presigner> {
65        presigner_from_pubkey_sigs(pubkey, &self.present_signers)
66    }
67}
68pub type CliSigners = Vec<Box<dyn Signer>>;
69pub type SignerIndex = usize;
70pub struct CliSignerInfo {
71    pub signers: CliSigners,
72}
73
74impl CliSignerInfo {
75    pub fn index_of(&self, pubkey: Option<Pubkey>) -> Option<usize> {
76        if let Some(pubkey) = pubkey {
77            self.signers
78                .iter()
79                .position(|signer| signer.pubkey() == pubkey)
80        } else {
81            Some(0)
82        }
83    }
84    pub fn index_of_or_none(&self, pubkey: Option<Pubkey>) -> Option<usize> {
85        if let Some(pubkey) = pubkey {
86            self.signers
87                .iter()
88                .position(|signer| signer.pubkey() == pubkey)
89        } else {
90            None
91        }
92    }
93    pub fn signers_for_message(&self, message: &Message) -> Vec<&dyn Signer> {
94        self.signers
95            .iter()
96            .filter_map(|k| {
97                if message.signer_keys().contains(&&k.pubkey()) {
98                    Some(k.as_ref())
99                } else {
100                    None
101                }
102            })
103            .collect()
104    }
105}
106
107/// A command line argument that loads a default signer in absence of other signers.
108///
109/// This type manages a default signing source which may be overridden by other
110/// signing sources via its [`generate_unique_signers`] method.
111///
112/// [`generate_unique_signers`]: DefaultSigner::generate_unique_signers
113///
114/// `path` is a signing source as documented by [`signer_from_path`], and
115/// `arg_name` is the name of its [clap] command line argument, which is passed
116/// to `signer_from_path` as its `keypair_name` argument.
117#[derive(Debug, Default)]
118pub struct DefaultSigner {
119    /// The name of the signers command line argument.
120    pub arg_name: String,
121    /// The signing source.
122    pub path: String,
123    is_path_checked: RefCell<bool>,
124}
125
126impl DefaultSigner {
127    /// Create a new `DefaultSigner`.
128    ///
129    /// `path` is a signing source as documented by [`signer_from_path`], and
130    /// `arg_name` is the name of its [clap] command line argument, which is
131    /// passed to `signer_from_path` as its `keypair_name` argument.
132    ///
133    /// [clap]: https://docs.rs/clap
134    ///
135    /// # Examples
136    ///
137    /// ```no_run
138    /// use clap::{App, Arg, value_t_or_exit};
139    /// use solana_clap_utils::keypair::DefaultSigner;
140    /// use solana_clap_utils::offline::OfflineArgs;
141    ///
142    /// let clap_app = App::new("my-program")
143    ///     // The argument we'll parse as a signer "path"
144    ///     .arg(Arg::with_name("keypair")
145    ///         .required(true)
146    ///         .help("The default signer"))
147    ///     .offline_args();
148    ///
149    /// let clap_matches = clap_app.get_matches();
150    /// let keypair_str = value_t_or_exit!(clap_matches, "keypair", String);
151    ///
152    /// let default_signer = DefaultSigner::new("keypair", &keypair_str);
153    /// # assert!(default_signer.arg_name.len() > 0);
154    /// assert_eq!(default_signer.path, keypair_str);
155    /// # Ok::<(), Box<dyn std::error::Error>>(())
156    /// ```
157    pub fn new<AN: AsRef<str>, P: AsRef<str>>(arg_name: AN, path: P) -> Self {
158        let arg_name = arg_name.as_ref().to_string();
159        let path = path.as_ref().to_string();
160        Self {
161            arg_name,
162            path,
163            ..Self::default()
164        }
165    }
166
167    fn path(&self) -> Result<&str, Box<dyn std::error::Error>> {
168        if !self.is_path_checked.borrow().deref() {
169            parse_signer_source(&self.path)
170                .and_then(|s| {
171                    if let SignerSourceKind::Filepath(path) = &s.kind {
172                        std::fs::metadata(path).map(|_| ()).map_err(|e| e.into())
173                    } else {
174                        Ok(())
175                    }
176                })
177                .map_err(|_| {
178                    std::io::Error::other(format!(
179                        "No default signer found, run \"solana-keygen new -o {}\" to create a new \
180                         one",
181                        self.path
182                    ))
183                })?;
184            *self.is_path_checked.borrow_mut() = true;
185        }
186        Ok(&self.path)
187    }
188
189    /// Generate a unique set of signers, possibly excluding this default signer.
190    ///
191    /// This function allows a command line application to have a default
192    /// signer, perhaps representing a default wallet, but to override that
193    /// signer and instead sign with one or more other signers.
194    ///
195    /// `bulk_signers` is a vector of signers, all of which are optional. If any
196    /// of those signers is `None`, then the default signer will be loaded; if
197    /// all of those signers are `Some`, then the default signer will not be
198    /// loaded.
199    ///
200    /// The returned value includes all of the `bulk_signers` that were not
201    /// `None`, and maybe the default signer, if it was loaded.
202    ///
203    /// # Examples
204    ///
205    /// ```no_run
206    /// use clap::{App, Arg, value_t_or_exit};
207    /// use solana_clap_utils::keypair::{DefaultSigner, signer_from_path};
208    /// use solana_clap_utils::offline::OfflineArgs;
209    /// use solana_signer::Signer;
210    ///
211    /// let clap_app = App::new("my-program")
212    ///     // The argument we'll parse as a signer "path"
213    ///     .arg(Arg::with_name("keypair")
214    ///         .required(true)
215    ///         .help("The default signer"))
216    ///     .arg(Arg::with_name("payer")
217    ///         .long("payer")
218    ///         .help("The account paying for the transaction"))
219    ///     .offline_args();
220    ///
221    /// let mut wallet_manager = None;
222    ///
223    /// let clap_matches = clap_app.get_matches();
224    /// let keypair_str = value_t_or_exit!(clap_matches, "keypair", String);
225    /// let maybe_payer = clap_matches.value_of("payer");
226    ///
227    /// let default_signer = DefaultSigner::new("keypair", &keypair_str);
228    /// let maybe_payer_signer = maybe_payer.map(|payer| {
229    ///     signer_from_path(&clap_matches, payer, "payer", &mut wallet_manager)
230    /// }).transpose()?;
231    /// let bulk_signers = vec![maybe_payer_signer];
232    ///
233    /// let unique_signers = default_signer.generate_unique_signers(
234    ///     bulk_signers,
235    ///     &clap_matches,
236    ///     &mut wallet_manager,
237    /// )?;
238    /// # Ok::<(), Box<dyn std::error::Error>>(())
239    /// ```
240    pub fn generate_unique_signers(
241        &self,
242        bulk_signers: Vec<Option<Box<dyn Signer>>>,
243        matches: &ArgMatches<'_>,
244        wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
245    ) -> Result<CliSignerInfo, Box<dyn error::Error>> {
246        let mut unique_signers = vec![];
247
248        // Determine if the default signer is needed
249        if bulk_signers.iter().any(|signer| signer.is_none()) {
250            let default_signer = self.signer_from_path(matches, wallet_manager)?;
251            unique_signers.push(default_signer);
252        }
253
254        for signer in bulk_signers.into_iter().flatten() {
255            if !unique_signers.iter().any(|s| s == &signer) {
256                unique_signers.push(signer);
257            }
258        }
259        Ok(CliSignerInfo {
260            signers: unique_signers,
261        })
262    }
263
264    /// Loads the default [Signer] from one of several possible sources.
265    ///
266    /// The `path` is not strictly a file system path, but is interpreted as
267    /// various types of _signing source_, depending on its format, one of which
268    /// is a path to a keypair file. Some sources may require user interaction
269    /// in the course of calling this function.
270    ///
271    /// This simply delegates to the [`signer_from_path`] free function, passing
272    /// it the `DefaultSigner`s `path` and `arg_name` fields as the `path` and
273    /// `keypair_name` arguments.
274    ///
275    /// See the [`signer_from_path`] free function for full documentation of how
276    /// this function interprets its arguments.
277    ///
278    /// # Examples
279    ///
280    /// ```no_run
281    /// use clap::{App, Arg, value_t_or_exit};
282    /// use solana_clap_utils::keypair::DefaultSigner;
283    /// use solana_clap_utils::offline::OfflineArgs;
284    ///
285    /// let clap_app = App::new("my-program")
286    ///     // The argument we'll parse as a signer "path"
287    ///     .arg(Arg::with_name("keypair")
288    ///         .required(true)
289    ///         .help("The default signer"))
290    ///     .offline_args();
291    ///
292    /// let clap_matches = clap_app.get_matches();
293    /// let keypair_str = value_t_or_exit!(clap_matches, "keypair", String);
294    /// let default_signer = DefaultSigner::new("keypair", &keypair_str);
295    /// let mut wallet_manager = None;
296    ///
297    /// let signer = default_signer.signer_from_path(
298    ///     &clap_matches,
299    ///     &mut wallet_manager,
300    /// )?;
301    /// # Ok::<(), Box<dyn std::error::Error>>(())
302    /// ```
303    pub fn signer_from_path(
304        &self,
305        matches: &ArgMatches,
306        wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
307    ) -> Result<Box<dyn Signer>, Box<dyn std::error::Error>> {
308        signer_from_path(matches, self.path()?, &self.arg_name, wallet_manager)
309    }
310
311    /// Loads the default [Signer] from one of several possible sources.
312    ///
313    /// The `path` is not strictly a file system path, but is interpreted as
314    /// various types of _signing source_, depending on its format, one of which
315    /// is a path to a keypair file. Some sources may require user interaction
316    /// in the course of calling this function.
317    ///
318    /// This simply delegates to the [`signer_from_path_with_config`] free
319    /// function, passing it the `DefaultSigner`s `path` and `arg_name` fields
320    /// as the `path` and `keypair_name` arguments.
321    ///
322    /// See the [`signer_from_path`] free function for full documentation of how
323    /// this function interprets its arguments.
324    ///
325    /// # Examples
326    ///
327    /// ```no_run
328    /// use clap::{App, Arg, value_t_or_exit};
329    /// use solana_clap_utils::keypair::{SignerFromPathConfig, DefaultSigner};
330    /// use solana_clap_utils::offline::OfflineArgs;
331    ///
332    /// let clap_app = App::new("my-program")
333    ///     // The argument we'll parse as a signer "path"
334    ///     .arg(Arg::with_name("keypair")
335    ///         .required(true)
336    ///         .help("The default signer"))
337    ///     .offline_args();
338    ///
339    /// let clap_matches = clap_app.get_matches();
340    /// let keypair_str = value_t_or_exit!(clap_matches, "keypair", String);
341    /// let default_signer = DefaultSigner::new("keypair", &keypair_str);
342    /// let mut wallet_manager = None;
343    ///
344    /// // Allow pubkey signers without accompanying signatures
345    /// let config = SignerFromPathConfig {
346    ///     allow_null_signer: true,
347    /// };
348    ///
349    /// let signer = default_signer.signer_from_path_with_config(
350    ///     &clap_matches,
351    ///     &mut wallet_manager,
352    ///     &config,
353    /// )?;
354    /// # Ok::<(), Box<dyn std::error::Error>>(())
355    /// ```
356    pub fn signer_from_path_with_config(
357        &self,
358        matches: &ArgMatches,
359        wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
360        config: &SignerFromPathConfig,
361    ) -> Result<Box<dyn Signer>, Box<dyn std::error::Error>> {
362        signer_from_path_with_config(
363            matches,
364            self.path()?,
365            &self.arg_name,
366            wallet_manager,
367            config,
368        )
369    }
370}
371
372#[derive(Debug)]
373pub(crate) struct SignerSource {
374    pub kind: SignerSourceKind,
375    pub derivation_path: Option<DerivationPath>,
376    pub legacy: bool,
377}
378
379impl SignerSource {
380    fn new(kind: SignerSourceKind) -> Self {
381        Self {
382            kind,
383            derivation_path: None,
384            legacy: false,
385        }
386    }
387
388    fn new_legacy(kind: SignerSourceKind) -> Self {
389        Self {
390            kind,
391            derivation_path: None,
392            legacy: true,
393        }
394    }
395}
396
397const SIGNER_SOURCE_PROMPT: &str = "prompt";
398const SIGNER_SOURCE_FILEPATH: &str = "file";
399const SIGNER_SOURCE_USB: &str = "usb";
400const SIGNER_SOURCE_STDIN: &str = "stdin";
401const SIGNER_SOURCE_PUBKEY: &str = "pubkey";
402
403pub(crate) enum SignerSourceKind {
404    Prompt,
405    Filepath(String),
406    Usb(RemoteWalletLocator),
407    Stdin,
408    Pubkey(Pubkey),
409}
410
411impl AsRef<str> for SignerSourceKind {
412    fn as_ref(&self) -> &str {
413        match self {
414            Self::Prompt => SIGNER_SOURCE_PROMPT,
415            Self::Filepath(_) => SIGNER_SOURCE_FILEPATH,
416            Self::Usb(_) => SIGNER_SOURCE_USB,
417            Self::Stdin => SIGNER_SOURCE_STDIN,
418            Self::Pubkey(_) => SIGNER_SOURCE_PUBKEY,
419        }
420    }
421}
422
423impl std::fmt::Debug for SignerSourceKind {
424    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
425        let s: &str = self.as_ref();
426        write!(f, "{s}")
427    }
428}
429
430#[derive(Debug, Error)]
431pub(crate) enum SignerSourceError {
432    #[error("unrecognized signer source")]
433    UnrecognizedSource,
434    #[error(transparent)]
435    RemoteWalletLocatorError(#[from] RemoteWalletLocatorError),
436    #[error(transparent)]
437    DerivationPathError(#[from] DerivationPathError),
438    #[error(transparent)]
439    IoError(#[from] std::io::Error),
440}
441
442pub(crate) fn parse_signer_source<S: AsRef<str>>(
443    source: S,
444) -> Result<SignerSource, SignerSourceError> {
445    let source = source.as_ref();
446    let source = {
447        #[cfg(target_family = "windows")]
448        {
449            // trim matched single-quotes since cmd.exe won't
450            let mut source = source;
451            while let Some(trimmed) = source.strip_prefix('\'') {
452                source = if let Some(trimmed) = trimmed.strip_suffix('\'') {
453                    trimmed
454                } else {
455                    break;
456                }
457            }
458            source.replace('\\', "/")
459        }
460        #[cfg(not(target_family = "windows"))]
461        {
462            source.to_string()
463        }
464    };
465    match uriparse::URIReference::try_from(source.as_str()) {
466        Err(_) => Err(SignerSourceError::UnrecognizedSource),
467        Ok(uri) => {
468            if let Some(scheme) = uri.scheme() {
469                let scheme = scheme.as_str().to_ascii_lowercase();
470                match scheme.as_str() {
471                    SIGNER_SOURCE_PROMPT => Ok(SignerSource {
472                        kind: SignerSourceKind::Prompt,
473                        derivation_path: DerivationPath::from_uri_any_query(&uri)?,
474                        legacy: false,
475                    }),
476                    SIGNER_SOURCE_FILEPATH => Ok(SignerSource::new(SignerSourceKind::Filepath(
477                        uri.path().to_string(),
478                    ))),
479                    SIGNER_SOURCE_USB => Ok(SignerSource {
480                        kind: SignerSourceKind::Usb(RemoteWalletLocator::new_from_uri(&uri)?),
481                        derivation_path: DerivationPath::from_uri_key_query(&uri)?,
482                        legacy: false,
483                    }),
484                    SIGNER_SOURCE_STDIN => Ok(SignerSource::new(SignerSourceKind::Stdin)),
485                    _ => {
486                        #[cfg(target_family = "windows")]
487                        // On Windows, an absolute path's drive letter will be parsed as the URI
488                        // scheme. Assume a filepath source in case of a single character shceme.
489                        if scheme.len() == 1 {
490                            return Ok(SignerSource::new(SignerSourceKind::Filepath(source)));
491                        }
492                        Err(SignerSourceError::UnrecognizedSource)
493                    }
494                }
495            } else {
496                match source.as_str() {
497                    STDOUT_OUTFILE_TOKEN => Ok(SignerSource::new(SignerSourceKind::Stdin)),
498                    ASK_KEYWORD => Ok(SignerSource::new_legacy(SignerSourceKind::Prompt)),
499                    _ => match Pubkey::from_str(source.as_str()) {
500                        Ok(pubkey) => Ok(SignerSource::new(SignerSourceKind::Pubkey(pubkey))),
501                        Err(_) => std::fs::metadata(source.as_str())
502                            .map(|_| SignerSource::new(SignerSourceKind::Filepath(source)))
503                            .map_err(|err| err.into()),
504                    },
505                }
506            }
507        }
508    }
509}
510
511pub fn presigner_from_pubkey_sigs(
512    pubkey: &Pubkey,
513    signers: &[(Pubkey, Signature)],
514) -> Option<Presigner> {
515    signers.iter().find_map(|(signer, sig)| {
516        if *signer == *pubkey {
517            Some(Presigner::new(signer, sig))
518        } else {
519            None
520        }
521    })
522}
523
524#[derive(Debug, Default)]
525pub struct SignerFromPathConfig {
526    pub allow_null_signer: bool,
527}
528
529/// Loads a [Signer] from one of several possible sources.
530///
531/// The `path` is not strictly a file system path, but is interpreted as various
532/// types of _signing source_, depending on its format, one of which is a path
533/// to a keypair file. Some sources may require user interaction in the course
534/// of calling this function.
535///
536/// The result of this function is a boxed object of the [Signer] trait. To load
537/// a concrete [Keypair], use the [keypair_from_path] function, though note that
538/// it does not support all signer sources.
539///
540/// The `matches` argument is the same set of parsed [clap] matches from which
541/// `path` was parsed. It is used to parse various additional command line
542/// arguments, depending on which signing source is requested, as described
543/// below in "Signing sources".
544///
545/// [clap]: https//docs.rs/clap
546///
547/// The `keypair_name` argument is the "name" of the signer, and is typically
548/// the name of the clap argument from which the `path` argument was parsed,
549/// like "keypair", "from", or "fee-payer". It is used solely for interactively
550/// prompting the user, either when entering seed phrases or selecting from
551/// multiple hardware wallets.
552///
553/// The `wallet_manager` is used for establishing connections to a hardware
554/// device such as Ledger. If `wallet_manager` is a reference to `None`, and a
555/// hardware signer is requested, then this function will attempt to create a
556/// wallet manager, assigning it to the mutable `wallet_manager` reference. This
557/// argument is typically a reference to `None`.
558///
559/// # Signing sources
560///
561/// The `path` argument can simply be a path to a keypair file, but it may also
562/// be interpreted in several other ways, in the following order.
563///
564/// Firstly, the `path` argument may be interpreted as a [URI], with the URI
565/// scheme indicating where to load the signer from. If it parses as a URI, then
566/// the following schemes are supported:
567///
568/// - `file:` &mdash; Read the keypair from a JSON keypair file. The path portion
569///   of the URI is the file path.
570///
571/// - `stdin:` &mdash; Read the keypair from stdin, in the JSON format used by
572///   the keypair file.
573///
574///   Non-scheme parts of the URI are ignored.
575///
576/// - `prompt:` &mdash; The user will be prompted at the command line
577///   for their seed phrase and passphrase.
578///
579///   In this URI the [query string][qs] may contain zero or one of the
580///   following key/value pairs that determine the [BIP44 derivation path][dp]
581///   of the private key from the seed:
582///
583///   - `key` &mdash; In this case the value is either one or two numerical
584///     indexes separated by a slash, which represent the "account", and
585///     "change" components of the BIP44 derivation path. Example: `key=0/0`.
586///
587///   - `full-path` &mdash; In this case the value is a full derivation path,
588///     and the user is responsible for ensuring it is correct. Example:
589///     `full-path=m/44/501/0/0/0`.
590///
591///   If neither is provided, then the default derivation path is used.
592///
593///   Note that when specifying derivation paths, this routine will convert all
594///   indexes into ["hardened"] indexes, even if written as "normal" indexes.
595///
596///   Other components of the URI besides the scheme and query string are ignored.
597///
598///   If the "skip_seed_phrase_validation" argument, as defined in
599///   [SKIP_SEED_PHRASE_VALIDATION_ARG] is found in `matches`, then the keypair
600///   seed will be generated directly from the seed phrase, without parsing or
601///   validating it as a BIP39 seed phrase. This allows the use of non-BIP39 seed
602///   phrases.
603///
604/// - `usb:` &mdash; Use a USB hardware device as the signer. In this case, the
605///   URI host indicates the device type, and is required. The only currently valid host
606///   value is "ledger".
607///
608///   Optionally, the first segment of the URI path indicates the base-58
609///   encoded pubkey of the wallet, and the "account" and "change" indices of
610///   the derivation path can be specified with the `key=` query parameter, as
611///   with the `prompt:` URI.
612///
613///   Examples:
614///
615///   - `usb://ledger`
616///   - `usb://ledger?key=0/0`
617///   - `usb://ledger/9rPVSygg3brqghvdZ6wsL2i5YNQTGhXGdJzF65YxaCQd`
618///   - `usb://ledger/9rPVSygg3brqghvdZ6wsL2i5YNQTGhXGdJzF65YxaCQd?key=0/0`
619///
620/// Next the `path` argument may be one of the following strings:
621///
622/// - `-` &mdash; Read the keypair from stdin. This is the same as the `stdin:`
623///   URI scheme.
624///
625/// - `ASK` &mdash; The user will be prompted at the command line for their seed
626///   phrase and passphrase. _This uses a legacy key derivation method and should
627///   usually be avoided in favor of `prompt:`._
628///
629/// Next, if the `path` argument parses as a base-58 public key, then the signer
630/// is created without a private key, but with presigned signatures, each parsed
631/// from the additional command line arguments, provided by the `matches`
632/// argument.
633///
634/// In this case, the remaining command line arguments are searched for clap
635/// arguments named "signer", as defined by [SIGNER_ARG], and each is parsed as
636/// a key-value pair of the form "pubkey=signature", where `pubkey` is the same
637/// base-58 public key, and `signature` is a serialized signature produced by
638/// the corresponding keypair. One of the "signer" signatures must be for the
639/// pubkey specified in `path` or this function will return an error; unless the
640/// "sign_only" clap argument, as defined by [SIGN_ONLY_ARG], is present in
641/// `matches`, in which case the signer will be created with no associated
642/// signatures.
643///
644/// Finally, if `path`, interpreted as a file path, represents a file on disk,
645/// then the signer is created by reading that file as a JSON-serialized
646/// keypair. This is the same as the `file:` URI scheme.
647///
648/// [qs]: https://en.wikipedia.org/wiki/Query_string
649/// [dp]: https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
650/// [URI]: https://en.wikipedia.org/wiki/Uniform_Resource_Identifier
651/// ["hardened"]: https://wiki.trezor.io/Hardened_and_non-hardened_derivation
652///
653/// # Examples
654///
655/// This shows a reasonable way to set up clap to parse all possible signer
656/// sources. Note the use of the [`OfflineArgs::offline_args`] method to add
657/// correct clap definitions of the `--signer` and `--sign-only` arguments, as
658/// required by the base-58 pubkey offline signing method.
659///
660/// [`OfflineArgs::offline_args`]: crate::offline::OfflineArgs::offline_args
661///
662/// ```no_run
663/// use clap::{App, Arg, value_t_or_exit};
664/// use solana_clap_utils::keypair::signer_from_path;
665/// use solana_clap_utils::offline::OfflineArgs;
666///
667/// let clap_app = App::new("my-program")
668///     // The argument we'll parse as a signer "path"
669///     .arg(Arg::with_name("keypair")
670///         .required(true)
671///         .help("The default signer"))
672///     .offline_args();
673///
674/// let clap_matches = clap_app.get_matches();
675/// let keypair_str = value_t_or_exit!(clap_matches, "keypair", String);
676/// let mut wallet_manager = None;
677/// let signer = signer_from_path(
678///     &clap_matches,
679///     &keypair_str,
680///     "keypair",
681///     &mut wallet_manager,
682/// )?;
683/// # Ok::<(), Box<dyn std::error::Error>>(())
684/// ```
685pub fn signer_from_path(
686    matches: &ArgMatches,
687    path: &str,
688    keypair_name: &str,
689    wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
690) -> Result<Box<dyn Signer>, Box<dyn error::Error>> {
691    let config = SignerFromPathConfig::default();
692    signer_from_path_with_config(matches, path, keypair_name, wallet_manager, &config)
693}
694
695/// Loads a [Signer] from one of several possible sources.
696///
697/// The `path` is not strictly a file system path, but is interpreted as various
698/// types of _signing source_, depending on its format, one of which is a path
699/// to a keypair file. Some sources may require user interaction in the course
700/// of calling this function.
701///
702/// This is the same as [`signer_from_path`] except that it additionaolly
703/// accepts a [`SignerFromPathConfig`] argument.
704///
705/// If the `allow_null_signer` field of `config` is `true`, then pubkey signers
706/// are allowed to have zero associated signatures via additional "signer"
707/// command line arguments. It the same effect as if the "sign_only" clap
708/// argument is present.
709///
710/// See [`signer_from_path`] for full documentation of how this function
711/// interprets its arguments.
712///
713/// # Examples
714///
715/// This shows a reasonable way to set up clap to parse all possible signer
716/// sources. Note the use of the [`OfflineArgs::offline_args`] method to add
717/// correct clap definitions of the `--signer` and `--sign-only` arguments, as
718/// required by the base-58 pubkey offline signing method.
719///
720/// [`OfflineArgs::offline_args`]: crate::offline::OfflineArgs::offline_args
721///
722/// ```no_run
723/// use clap::{App, Arg, value_t_or_exit};
724/// use solana_clap_utils::keypair::{signer_from_path_with_config, SignerFromPathConfig};
725/// use solana_clap_utils::offline::OfflineArgs;
726///
727/// let clap_app = App::new("my-program")
728///     // The argument we'll parse as a signer "path"
729///     .arg(Arg::with_name("keypair")
730///         .required(true)
731///         .help("The default signer"))
732///     .offline_args();
733///
734/// let clap_matches = clap_app.get_matches();
735/// let keypair_str = value_t_or_exit!(clap_matches, "keypair", String);
736/// let mut wallet_manager = None;
737///
738/// // Allow pubkey signers without accompanying signatures
739/// let config = SignerFromPathConfig {
740///     allow_null_signer: true,
741/// };
742///
743/// let signer = signer_from_path_with_config(
744///     &clap_matches,
745///     &keypair_str,
746///     "keypair",
747///     &mut wallet_manager,
748///     &config,
749/// )?;
750/// # Ok::<(), Box<dyn std::error::Error>>(())
751/// ```
752pub fn signer_from_path_with_config(
753    matches: &ArgMatches,
754    path: &str,
755    keypair_name: &str,
756    wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
757    config: &SignerFromPathConfig,
758) -> Result<Box<dyn Signer>, Box<dyn error::Error>> {
759    let SignerSource {
760        kind,
761        derivation_path,
762        legacy,
763    } = parse_signer_source(path)?;
764    match kind {
765        SignerSourceKind::Prompt => {
766            let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name);
767            Ok(Box::new(keypair_from_seed_phrase(
768                keypair_name,
769                skip_validation,
770                false,
771                derivation_path,
772                legacy,
773            )?))
774        }
775        SignerSourceKind::Filepath(path) => match read_keypair_file(&path) {
776            Err(e) => Err(std::io::Error::other(format!(
777                "could not read keypair file \"{path}\". Run \"solana-keygen new\" to create a \
778                 keypair file: {e}"
779            ))
780            .into()),
781            Ok(file) => Ok(Box::new(file)),
782        },
783        SignerSourceKind::Stdin => {
784            let mut stdin = std::io::stdin();
785            Ok(Box::new(read_keypair(&mut stdin)?))
786        }
787        SignerSourceKind::Usb(locator) => {
788            if wallet_manager.is_none() {
789                *wallet_manager = maybe_wallet_manager()?;
790            }
791            if let Some(wallet_manager) = wallet_manager {
792                Ok(Box::new(generate_remote_keypair(
793                    locator,
794                    derivation_path.unwrap_or_default(),
795                    wallet_manager,
796                    matches.is_present("confirm_key"),
797                    keypair_name,
798                )?))
799            } else {
800                Err(RemoteWalletError::NoDeviceFound.into())
801            }
802        }
803        SignerSourceKind::Pubkey(pubkey) => {
804            let presigner = pubkeys_sigs_of(matches, SIGNER_ARG.name)
805                .as_ref()
806                .and_then(|presigners| presigner_from_pubkey_sigs(&pubkey, presigners));
807            if let Some(presigner) = presigner {
808                Ok(Box::new(presigner))
809            } else if config.allow_null_signer || matches.is_present(SIGN_ONLY_ARG.name) {
810                Ok(Box::new(NullSigner::new(&pubkey)))
811            } else {
812                Err(std::io::Error::other(format!(
813                    "missing signature for supplied pubkey: {pubkey}"
814                ))
815                .into())
816            }
817        }
818    }
819}
820
821/// Loads the pubkey of a [Signer] from one of several possible sources.
822///
823/// The `path` is not strictly a file system path, but is interpreted as various
824/// types of _signing source_, depending on its format, one of which is a path
825/// to a keypair file. Some sources may require user interaction in the course
826/// of calling this function.
827///
828/// The only difference between this function and [`signer_from_path`] is in the
829/// case of a "pubkey" path: this function does not require that accompanying
830/// command line arguments contain an offline signature.
831///
832/// See [`signer_from_path`] for full documentation of how this function
833/// interprets its arguments.
834///
835/// # Examples
836///
837/// ```no_run
838/// use clap::{App, Arg, value_t_or_exit};
839/// use solana_clap_utils::keypair::pubkey_from_path;
840///
841/// let clap_app = App::new("my-program")
842///     // The argument we'll parse as a signer "path"
843///     .arg(Arg::with_name("keypair")
844///         .required(true)
845///         .help("The default signer"));
846///
847/// let clap_matches = clap_app.get_matches();
848/// let keypair_str = value_t_or_exit!(clap_matches, "keypair", String);
849/// let mut wallet_manager = None;
850/// let pubkey = pubkey_from_path(
851///     &clap_matches,
852///     &keypair_str,
853///     "keypair",
854///     &mut wallet_manager,
855/// )?;
856/// # Ok::<(), Box<dyn std::error::Error>>(())
857/// ```
858pub fn pubkey_from_path(
859    matches: &ArgMatches,
860    path: &str,
861    keypair_name: &str,
862    wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
863) -> Result<Pubkey, Box<dyn error::Error>> {
864    let SignerSource { kind, .. } = parse_signer_source(path)?;
865    match kind {
866        SignerSourceKind::Pubkey(pubkey) => Ok(pubkey),
867        _ => Ok(signer_from_path(matches, path, keypair_name, wallet_manager)?.pubkey()),
868    }
869}
870
871pub fn resolve_signer_from_path(
872    matches: &ArgMatches,
873    path: &str,
874    keypair_name: &str,
875    wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
876) -> Result<Option<String>, Box<dyn error::Error>> {
877    let SignerSource {
878        kind,
879        derivation_path,
880        legacy,
881    } = parse_signer_source(path)?;
882    match kind {
883        SignerSourceKind::Prompt => {
884            let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name);
885            // This method validates the seed phrase, but returns `None` because there is no path
886            // on disk or to a device
887            keypair_from_seed_phrase(
888                keypair_name,
889                skip_validation,
890                false,
891                derivation_path,
892                legacy,
893            )
894            .map(|_| None)
895        }
896        SignerSourceKind::Filepath(path) => match read_keypair_file(&path) {
897            Err(e) => Err(std::io::Error::other(format!(
898                "could not read keypair file \"{path}\". Run \"solana-keygen new\" to create a \
899                 keypair file: {e}"
900            ))
901            .into()),
902            Ok(_) => Ok(Some(path.to_string())),
903        },
904        SignerSourceKind::Stdin => {
905            let mut stdin = std::io::stdin();
906            // This method validates the keypair from stdin, but returns `None` because there is no
907            // path on disk or to a device
908            read_keypair(&mut stdin).map(|_| None)
909        }
910        SignerSourceKind::Usb(locator) => {
911            if wallet_manager.is_none() {
912                *wallet_manager = maybe_wallet_manager()?;
913            }
914            if let Some(wallet_manager) = wallet_manager {
915                let path = generate_remote_keypair(
916                    locator,
917                    derivation_path.unwrap_or_default(),
918                    wallet_manager,
919                    matches.is_present("confirm_key"),
920                    keypair_name,
921                )
922                .map(|keypair| keypair.path)?;
923                Ok(Some(path))
924            } else {
925                Err(RemoteWalletError::NoDeviceFound.into())
926            }
927        }
928        _ => Ok(Some(path.to_string())),
929    }
930}
931
932// Keyword used to indicate that the user should be prompted for a keypair seed phrase
933pub const ASK_KEYWORD: &str = "ASK";
934
935pub const SKIP_SEED_PHRASE_VALIDATION_ARG: ArgConstant<'static> = ArgConstant {
936    long: "skip-seed-phrase-validation",
937    name: "skip_seed_phrase_validation",
938    help: "Skip validation of seed phrases. Use this if your phrase does not use the BIP39 \
939           official English word list",
940};
941
942/// Prompts user for a passphrase and then asks for confirmirmation to check for mistakes
943pub fn prompt_passphrase(prompt: &str) -> Result<String, Box<dyn error::Error>> {
944    let passphrase = prompt_password(prompt)?;
945    if !passphrase.is_empty() {
946        let confirmed = rpassword::prompt_password("Enter same passphrase again: ")?;
947        if confirmed != passphrase {
948            return Err("Passphrases did not match".into());
949        }
950    }
951    Ok(passphrase)
952}
953
954/// Loads a [Keypair] from one of several possible sources.
955///
956/// The `path` is not strictly a file system path, but is interpreted as various
957/// types of _signing source_, depending on its format, one of which is a path
958/// to a keypair file. Some sources may require user interaction in the course
959/// of calling this function.
960///
961/// This is the same as [`signer_from_path`] except that it only supports
962/// signing sources that can result in a [Keypair]: prompt for seed phrase,
963/// keypair file, and stdin.
964///
965/// If `confirm_pubkey` is `true` then after deriving the pubkey, the user will
966/// be prompted to confirm that the pubkey is as expected.
967///
968/// See [`signer_from_path`] for full documentation of how this function
969/// interprets its arguments.
970///
971/// # Examples
972///
973/// ```no_run
974/// use clap::{App, Arg, value_t_or_exit};
975/// use solana_clap_utils::keypair::keypair_from_path;
976///
977/// let clap_app = App::new("my-program")
978///     // The argument we'll parse as a signer "path"
979///     .arg(Arg::with_name("keypair")
980///         .required(true)
981///         .help("The default signer"));
982///
983/// let clap_matches = clap_app.get_matches();
984/// let keypair_str = value_t_or_exit!(clap_matches, "keypair", String);
985///
986/// let signer = keypair_from_path(
987///     &clap_matches,
988///     &keypair_str,
989///     "keypair",
990///     false,
991/// )?;
992/// # Ok::<(), Box<dyn std::error::Error>>(())
993/// ```
994pub fn keypair_from_path(
995    matches: &ArgMatches,
996    path: &str,
997    keypair_name: &str,
998    confirm_pubkey: bool,
999) -> Result<Keypair, Box<dyn error::Error>> {
1000    let SignerSource {
1001        kind,
1002        derivation_path,
1003        legacy,
1004    } = parse_signer_source(path)?;
1005    match kind {
1006        SignerSourceKind::Prompt => {
1007            let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name);
1008            Ok(keypair_from_seed_phrase(
1009                keypair_name,
1010                skip_validation,
1011                confirm_pubkey,
1012                derivation_path,
1013                legacy,
1014            )?)
1015        }
1016        SignerSourceKind::Filepath(path) => match read_keypair_file(&path) {
1017            Err(e) => Err(std::io::Error::other(format!(
1018                "could not read keypair file \"{path}\". Run \"solana-keygen new\" to create a \
1019                 keypair file: {e}"
1020            ))
1021            .into()),
1022            Ok(file) => Ok(file),
1023        },
1024        SignerSourceKind::Stdin => {
1025            let mut stdin = std::io::stdin();
1026            Ok(read_keypair(&mut stdin)?)
1027        }
1028        _ => Err(std::io::Error::other(format!(
1029            "signer of type `{kind:?}` does not support Keypair output"
1030        ))
1031        .into()),
1032    }
1033}
1034
1035/// Reads user input from stdin to retrieve a seed phrase and passphrase for keypair derivation.
1036///
1037/// Optionally skips validation of seed phrase. Optionally confirms recovered
1038/// public key.
1039pub fn keypair_from_seed_phrase(
1040    keypair_name: &str,
1041    skip_validation: bool,
1042    confirm_pubkey: bool,
1043    derivation_path: Option<DerivationPath>,
1044    legacy: bool,
1045) -> Result<Keypair, Box<dyn error::Error>> {
1046    let seed_phrase = prompt_password(format!("[{keypair_name}] seed phrase: "))?;
1047    let seed_phrase = seed_phrase.trim();
1048    let passphrase_prompt = format!(
1049        "[{keypair_name}] If this seed phrase has an associated passphrase, enter it now. \
1050         Otherwise, press ENTER to continue: ",
1051    );
1052
1053    let keypair = if skip_validation {
1054        let passphrase = prompt_passphrase(&passphrase_prompt)?;
1055        if legacy {
1056            keypair_from_seed_phrase_and_passphrase(seed_phrase, &passphrase)?
1057        } else {
1058            let seed = generate_seed_from_seed_phrase_and_passphrase(seed_phrase, &passphrase);
1059            keypair_from_seed_and_derivation_path(&seed, derivation_path)?
1060        }
1061    } else {
1062        let sanitized = sanitize_seed_phrase(seed_phrase);
1063        let parse_language_fn = || {
1064            for language in &[
1065                Language::English,
1066                Language::ChineseSimplified,
1067                Language::ChineseTraditional,
1068                Language::Japanese,
1069                Language::Spanish,
1070                Language::Korean,
1071                Language::French,
1072                Language::Italian,
1073            ] {
1074                if let Ok(mnemonic) = Mnemonic::from_phrase(&sanitized, *language) {
1075                    return Ok(mnemonic);
1076                }
1077            }
1078            Err("Can't get mnemonic from seed phrases")
1079        };
1080        let mnemonic = parse_language_fn()?;
1081        let passphrase = prompt_passphrase(&passphrase_prompt)?;
1082        let seed = Seed::new(&mnemonic, &passphrase);
1083        if legacy {
1084            keypair_from_seed(seed.as_bytes())?
1085        } else {
1086            keypair_from_seed_and_derivation_path(seed.as_bytes(), derivation_path)?
1087        }
1088    };
1089
1090    if confirm_pubkey {
1091        let pubkey = keypair.pubkey();
1092        print!("Recovered pubkey `{pubkey:?}`. Continue? (y/n): ");
1093        let _ignored = stdout().flush();
1094        let mut input = String::new();
1095        stdin().read_line(&mut input).expect("Unexpected input");
1096        if input.to_lowercase().trim() != "y" {
1097            println!("Exiting");
1098            exit(1);
1099        }
1100    }
1101
1102    Ok(keypair)
1103}
1104
1105fn sanitize_seed_phrase(seed_phrase: &str) -> String {
1106    seed_phrase
1107        .split_whitespace()
1108        .collect::<Vec<&str>>()
1109        .join(" ")
1110}
1111
1112#[cfg(test)]
1113mod tests {
1114    use {
1115        super::*,
1116        crate::offline::OfflineArgs,
1117        assert_matches::assert_matches,
1118        clap::{value_t_or_exit, App, Arg},
1119        solana_keypair::write_keypair_file,
1120        solana_remote_wallet::{locator::Manufacturer, remote_wallet::initialize_wallet_manager},
1121        solana_system_interface::instruction::transfer,
1122        tempfile::{NamedTempFile, TempDir},
1123    };
1124
1125    #[test]
1126    fn test_sanitize_seed_phrase() {
1127        let seed_phrase = " Mary   had\ta\u{2009}little  \n\t lamb";
1128        assert_eq!(
1129            "Mary had a little lamb".to_owned(),
1130            sanitize_seed_phrase(seed_phrase)
1131        );
1132    }
1133
1134    #[test]
1135    fn test_signer_info_signers_for_message() {
1136        let source = Keypair::new();
1137        let fee_payer = Keypair::new();
1138        let nonsigner1 = Keypair::new();
1139        let nonsigner2 = Keypair::new();
1140        let recipient = Pubkey::new_unique();
1141        let message = Message::new(
1142            &[transfer(&source.pubkey(), &recipient, 42)],
1143            Some(&fee_payer.pubkey()),
1144        );
1145        let signers = vec![
1146            Box::new(fee_payer) as Box<dyn Signer>,
1147            Box::new(source) as Box<dyn Signer>,
1148            Box::new(nonsigner1) as Box<dyn Signer>,
1149            Box::new(nonsigner2) as Box<dyn Signer>,
1150        ];
1151        let signer_info = CliSignerInfo { signers };
1152        let msg_signers = signer_info.signers_for_message(&message);
1153        let signer_pubkeys = msg_signers.iter().map(|s| s.pubkey()).collect::<Vec<_>>();
1154        let expect = vec![
1155            signer_info.signers[0].pubkey(),
1156            signer_info.signers[1].pubkey(),
1157        ];
1158        assert_eq!(signer_pubkeys, expect);
1159    }
1160
1161    #[test]
1162    fn test_parse_signer_source() {
1163        assert_matches!(
1164            parse_signer_source(STDOUT_OUTFILE_TOKEN).unwrap(),
1165            SignerSource {
1166                kind: SignerSourceKind::Stdin,
1167                derivation_path: None,
1168                legacy: false,
1169            }
1170        );
1171        let stdin = "stdin:".to_string();
1172        assert_matches!(
1173            parse_signer_source(stdin).unwrap(),
1174            SignerSource {
1175                kind: SignerSourceKind::Stdin,
1176                derivation_path: None,
1177                legacy: false,
1178            }
1179        );
1180        assert_matches!(
1181            parse_signer_source(ASK_KEYWORD).unwrap(),
1182            SignerSource {
1183                kind: SignerSourceKind::Prompt,
1184                derivation_path: None,
1185                legacy: true,
1186            }
1187        );
1188        let pubkey = Pubkey::new_unique();
1189        assert!(
1190            matches!(parse_signer_source(pubkey.to_string()).unwrap(), SignerSource {
1191                kind: SignerSourceKind::Pubkey(p),
1192                derivation_path: None,
1193                legacy: false,
1194            }
1195            if p == pubkey)
1196        );
1197
1198        // Set up absolute and relative path strs
1199        let file0 = NamedTempFile::new().unwrap();
1200        let path = file0.path();
1201        assert!(path.is_absolute());
1202        let absolute_path_str = path.to_str().unwrap();
1203
1204        let file1 = NamedTempFile::new_in(std::env::current_dir().unwrap()).unwrap();
1205        let path = file1.path().file_name().unwrap().to_str().unwrap();
1206        let path = std::path::Path::new(path);
1207        assert!(path.is_relative());
1208        let relative_path_str = path.to_str().unwrap();
1209
1210        assert!(
1211            matches!(parse_signer_source(absolute_path_str).unwrap(), SignerSource {
1212                kind: SignerSourceKind::Filepath(p),
1213                derivation_path: None,
1214                legacy: false,
1215            } if p == absolute_path_str)
1216        );
1217        assert!(
1218            matches!(parse_signer_source(relative_path_str).unwrap(), SignerSource {
1219                kind: SignerSourceKind::Filepath(p),
1220                derivation_path: None,
1221                legacy: false,
1222            } if p == relative_path_str)
1223        );
1224
1225        let usb = "usb://ledger".to_string();
1226        let expected_locator = RemoteWalletLocator {
1227            manufacturer: Manufacturer::Ledger,
1228            pubkey: None,
1229        };
1230        assert_matches!(parse_signer_source(usb).unwrap(), SignerSource {
1231                kind: SignerSourceKind::Usb(u),
1232                derivation_path: None,
1233                legacy: false,
1234            } if u == expected_locator);
1235        let usb = "usb://ledger?key=0/0".to_string();
1236        let expected_locator = RemoteWalletLocator {
1237            manufacturer: Manufacturer::Ledger,
1238            pubkey: None,
1239        };
1240        let expected_derivation_path = Some(DerivationPath::new_bip44(Some(0), Some(0)));
1241        assert_matches!(parse_signer_source(usb).unwrap(), SignerSource {
1242                kind: SignerSourceKind::Usb(u),
1243                derivation_path: d,
1244                legacy: false,
1245            } if u == expected_locator && d == expected_derivation_path);
1246        // Catchall into SignerSource::Filepath fails
1247        let junk = "sometextthatisnotapubkeyorfile".to_string();
1248        assert!(Pubkey::from_str(&junk).is_err());
1249        assert_matches!(
1250            parse_signer_source(&junk),
1251            Err(SignerSourceError::IoError(_))
1252        );
1253
1254        let prompt = "prompt:".to_string();
1255        assert_matches!(
1256            parse_signer_source(prompt).unwrap(),
1257            SignerSource {
1258                kind: SignerSourceKind::Prompt,
1259                derivation_path: None,
1260                legacy: false,
1261            }
1262        );
1263        assert!(
1264            matches!(parse_signer_source(format!("file:{absolute_path_str}")).unwrap(), SignerSource {
1265                kind: SignerSourceKind::Filepath(p),
1266                derivation_path: None,
1267                legacy: false,
1268            } if p == absolute_path_str)
1269        );
1270        assert!(
1271            matches!(parse_signer_source(format!("file:{relative_path_str}")).unwrap(), SignerSource {
1272                kind: SignerSourceKind::Filepath(p),
1273                derivation_path: None,
1274                legacy: false,
1275            } if p == relative_path_str)
1276        );
1277    }
1278
1279    #[test]
1280    fn signer_from_path_with_file() -> Result<(), Box<dyn std::error::Error>> {
1281        let dir = TempDir::new()?;
1282        let dir = dir.path();
1283        let keypair_path = dir.join("id.json");
1284        let keypair_path_str = keypair_path.to_str().expect("utf-8");
1285
1286        let keypair = Keypair::new();
1287        write_keypair_file(&keypair, &keypair_path)?;
1288
1289        let args = vec!["program", keypair_path_str];
1290
1291        let clap_app = App::new("my-program")
1292            .arg(
1293                Arg::with_name("keypair")
1294                    .required(true)
1295                    .help("The signing keypair"),
1296            )
1297            .offline_args();
1298
1299        let clap_matches = clap_app.get_matches_from(args);
1300        let keypair_str = value_t_or_exit!(clap_matches, "keypair", String);
1301
1302        let wallet_manager = initialize_wallet_manager()?;
1303
1304        let signer = signer_from_path(
1305            &clap_matches,
1306            &keypair_str,
1307            "signer",
1308            &mut Some(wallet_manager),
1309        )?;
1310
1311        assert_eq!(keypair.pubkey(), signer.pubkey());
1312
1313        Ok(())
1314    }
1315}