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:` — Read the keypair from a JSON keypair file. The path portion
568/// of the URI is the file path.
569///
570/// - `stdin:` — 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:` — 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` — 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` — 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:` — 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/// - `-` — Read the keypair from stdin. This is the same as the `stdin:`
622/// URI scheme.
623///
624/// - `ASK` — 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}