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