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