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