soroban_cli/commands/keys/
generate.rs1use clap::{arg, command};
2use sep5::SeedPhrase;
3
4use super::super::config::{
5 locator, network,
6 secret::{self, Secret},
7};
8
9use crate::{commands::global, config::address::KeyName, print::Print, signer::secure_store};
10
11#[derive(thiserror::Error, Debug)]
12pub enum Error {
13 #[error(transparent)]
14 Config(#[from] locator::Error),
15
16 #[error(transparent)]
17 Secret(#[from] secret::Error),
18
19 #[error(transparent)]
20 Network(#[from] network::Error),
21
22 #[error("An identity with the name '{0}' already exists")]
23 IdentityAlreadyExists(String),
24
25 #[error(transparent)]
26 SecureStore(#[from] secure_store::Error),
27}
28
29#[derive(Debug, clap::Parser, Clone)]
30#[group(skip)]
31#[allow(clippy::struct_excessive_bools)]
32pub struct Cmd {
33 pub name: KeyName,
35
36 #[cfg(feature = "version_lt_23")]
38 #[arg(long)]
39 pub no_fund: bool,
40
41 #[arg(long)]
44 pub seed: Option<String>,
45
46 #[arg(long, short = 's')]
48 pub as_secret: bool,
49
50 #[arg(long)]
52 pub secure_store: bool,
53
54 #[command(flatten)]
55 pub config_locator: locator::Args,
56
57 #[arg(long)]
59 pub hd_path: Option<usize>,
60
61 #[command(flatten)]
62 pub network: network::Args,
63
64 #[arg(long, default_value = "false")]
66 pub fund: bool,
67
68 #[arg(long)]
70 pub overwrite: bool,
71}
72
73impl Cmd {
74 pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
75 let print = Print::new(global_args.quiet);
76
77 if self.config_locator.read_identity(&self.name).is_ok() {
78 if !self.overwrite {
79 return Err(Error::IdentityAlreadyExists(self.name.to_string()));
80 }
81
82 print.exclaimln(format!("Overwriting identity '{}'", &self.name.to_string()));
83 }
84
85 #[cfg(feature = "version_lt_23")]
86 if !self.fund {
87 print.warnln(
88 "Behavior of `generate` will change in the \
89 future, and it will no longer fund by default. If you want to fund please \
90 provide `--fund` flag. If you don't need to fund your keys in the future, ignore this \
91 warning. It can be suppressed with -q flag.",
92 );
93 }
94 let secret = self.secret(&print)?;
95 let path = self.config_locator.write_identity(&self.name, &secret)?;
96 print.checkln(format!("Key saved with alias {} in {path:?}", self.name));
97
98 #[cfg(feature = "version_lt_23")]
99 if !self.no_fund {
100 self.fund(&secret, &print).await?;
101 }
102 #[cfg(feature = "version_gte_23")]
103 if self.fund {
104 self.fund(&secret, &print).await?;
105 }
106
107 Ok(())
108 }
109
110 async fn fund(&self, secret: &Secret, print: &Print) -> Result<(), Error> {
111 let addr = secret.public_key(self.hd_path)?;
112 let network = self.network.get(&self.config_locator)?;
113 network
114 .fund_address(&addr)
115 .await
116 .map_err(|e| {
117 tracing::warn!("fund_address failed: {e}");
118 })
119 .unwrap_or_default();
120 print.checkln(format!(
121 "Account {:?} funded on {:?}",
122 self.name, network.network_passphrase
123 ));
124 Ok(())
125 }
126
127 fn secret(&self, print: &Print) -> Result<Secret, Error> {
128 let seed_phrase = self.seed_phrase()?;
129 if self.secure_store {
130 Ok(secure_store::save_secret(print, &self.name, seed_phrase)?)
131 } else if self.as_secret {
132 let secret: Secret = seed_phrase.into();
133 Ok(secret.private_key(self.hd_path)?.into())
134 } else {
135 Ok(seed_phrase.into())
136 }
137 }
138
139 fn seed_phrase(&self) -> Result<SeedPhrase, Error> {
140 Ok(secret::seed_phrase_from_seed(self.seed.as_deref())?)
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use crate::config::{address::KeyName, key::Key, secret::Secret};
147 use keyring::{mock, set_default_credential_builder};
148
149 fn set_up_test() -> (super::locator::Args, super::Cmd) {
150 let temp_dir = tempfile::tempdir().unwrap();
151 let locator = super::locator::Args {
152 global: false,
153 config_dir: Some(temp_dir.path().to_path_buf()),
154 };
155
156 let cmd = super::Cmd {
157 name: KeyName("test_name".to_string()),
158 #[cfg(feature = "version_lt_23")]
159 no_fund: true,
160 seed: None,
161 as_secret: false,
162 secure_store: false,
163 config_locator: locator.clone(),
164 hd_path: None,
165 network: super::network::Args::default(),
166 fund: false,
167 overwrite: false,
168 };
169
170 (locator, cmd)
171 }
172
173 fn global_args() -> super::global::Args {
174 super::global::Args {
175 quiet: true,
176 ..Default::default()
177 }
178 }
179
180 #[tokio::test]
181 async fn test_storing_secret_as_a_seed_phrase() {
182 let (test_locator, cmd) = set_up_test();
183 let global_args = global_args();
184
185 let result = cmd.run(&global_args).await;
186 assert!(result.is_ok());
187 let identity = test_locator.read_identity("test_name").unwrap();
188 assert!(matches!(identity, Key::Secret(Secret::SeedPhrase { .. })));
189 }
190
191 #[tokio::test]
192 async fn test_storing_secret_as_a_secret_key() {
193 let (test_locator, mut cmd) = set_up_test();
194 cmd.as_secret = true;
195 let global_args = global_args();
196
197 let result = cmd.run(&global_args).await;
198 assert!(result.is_ok());
199 let identity = test_locator.read_identity("test_name").unwrap();
200 assert!(matches!(identity, Key::Secret(Secret::SecretKey { .. })));
201 }
202
203 #[tokio::test]
204 async fn test_storing_secret_in_secure_store() {
205 set_default_credential_builder(mock::default_credential_builder());
206 let (test_locator, mut cmd) = set_up_test();
207 cmd.secure_store = true;
208 let global_args = global_args();
209
210 let result = cmd.run(&global_args).await;
211 assert!(result.is_ok());
212 let identity = test_locator.read_identity("test_name").unwrap();
213 assert!(matches!(identity, Key::Secret(Secret::SecureStore { .. })));
214 }
215}