soroban_cli/commands/keys/
generate.rs1use sep5::SeedPhrase;
2
3use super::super::config::{
4 locator, network,
5 secret::{self, Secret},
6};
7
8use crate::{commands::global, config::address::KeyName, print::Print, signer::secure_store};
9
10#[derive(thiserror::Error, Debug)]
11pub enum Error {
12 #[error(transparent)]
13 Config(#[from] locator::Error),
14
15 #[error(transparent)]
16 Secret(#[from] secret::Error),
17
18 #[error(transparent)]
19 Network(#[from] network::Error),
20
21 #[error("An identity with the name '{0}' already exists")]
22 IdentityAlreadyExists(String),
23
24 #[error(transparent)]
25 SecureStore(#[from] secure_store::Error),
26}
27
28#[derive(Debug, clap::Parser, Clone)]
29#[group(skip)]
30#[allow(clippy::struct_excessive_bools)]
31pub struct Cmd {
32 pub name: KeyName,
34
35 #[arg(long)]
38 pub seed: Option<String>,
39
40 #[arg(long, short = 's')]
42 pub as_secret: bool,
43
44 #[arg(long)]
48 pub secure_store: bool,
49
50 #[command(flatten)]
51 pub config_locator: locator::Args,
52
53 #[arg(long)]
58 pub hd_path: Option<u32>,
59
60 #[command(flatten)]
61 pub network: network::Args,
62
63 #[arg(long, default_value = "false")]
65 pub fund: bool,
66
67 #[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 let secret = self.secret(&print)?;
86 let path = self.config_locator.write_identity(&self.name, &secret)?;
87 print.checkln(format!("Key saved with alias {} in {path:?}", self.name));
88
89 if self.fund {
90 self.fund(&secret, &print).await?;
91 }
92
93 Ok(())
94 }
95
96 async fn fund(&self, secret: &Secret, print: &Print) -> Result<(), Error> {
97 let addr = secret.public_key(self.hd_path)?;
98 let network = self.network.get(&self.config_locator)?;
99 let formatted_name = self.name.to_string();
100
101 match network.fund_address(&addr).await {
102 Ok(()) => print.checkln(format!(
103 "Account {} funded on {:?}",
104 formatted_name, network.network_passphrase
105 )),
106 Err(e) => {
107 tracing::trace!("Account funding error: {:?}", e);
108
109 print.errorln(format!(
110 "Unable to fund account {} on {:?}",
111 formatted_name, network.network_passphrase
112 ));
113 }
114 }
115
116 Ok(())
117 }
118
119 fn secret(&self, print: &Print) -> Result<Secret, Error> {
120 let seed_phrase = self.seed_phrase()?;
121 if self.secure_store {
122 Ok(secure_store::save_secret(
123 print,
124 &self.name,
125 &seed_phrase,
126 self.hd_path,
127 self.overwrite,
128 )?)
129 } else if self.as_secret {
130 let secret: Secret = seed_phrase.into();
131 Ok(secret.private_key(self.hd_path)?.into())
132 } else {
133 Ok(Secret::SeedPhrase {
134 seed_phrase: seed_phrase.seed_phrase.into_phrase(),
135 hd_path: self.hd_path,
136 })
137 }
138 }
139
140 fn seed_phrase(&self) -> Result<SeedPhrase, Error> {
141 Ok(secret::seed_phrase_from_seed(self.seed.as_deref())?)
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use crate::config::{address::KeyName, key::Key, secret::Secret};
148
149 fn set_up_test() -> (super::locator::Args, super::Cmd, tempfile::TempDir) {
150 let temp_dir = tempfile::tempdir().unwrap();
151 let locator = super::locator::Args {
152 config_dir: Some(temp_dir.path().to_path_buf()),
153 };
154
155 let cmd = super::Cmd {
156 name: KeyName("test_name".to_string()),
157 seed: None,
158 as_secret: false,
159 secure_store: false,
160 config_locator: locator.clone(),
161 hd_path: None,
162 network: super::network::Args::default(),
163 fund: false,
164 overwrite: false,
165 };
166
167 (locator, cmd, temp_dir)
168 }
169
170 fn global_args() -> super::global::Args {
171 super::global::Args {
172 quiet: true,
173 ..Default::default()
174 }
175 }
176
177 #[tokio::test]
178 async fn test_storing_secret_as_a_seed_phrase() {
179 let (test_locator, cmd, _temp_dir) = set_up_test();
180 let global_args = global_args();
181
182 let result = cmd.run(&global_args).await;
183 assert!(result.is_ok());
184 let identity = test_locator.read_identity("test_name").unwrap();
185 assert!(matches!(identity, Key::Secret(Secret::SeedPhrase { .. })));
186 }
187
188 #[tokio::test]
189 async fn test_generate_seed_phrase_persists_hd_path() {
190 let (test_locator, mut cmd, _temp_dir) = set_up_test();
191 cmd.hd_path = Some(7);
192 let global_args = global_args();
193
194 cmd.run(&global_args).await.unwrap();
195
196 let identity = test_locator.read_identity("test_name").unwrap();
197 match identity {
198 Key::Secret(Secret::SeedPhrase { hd_path, .. }) => {
199 assert_eq!(hd_path, Some(7));
200 }
201 other => panic!("expected SeedPhrase variant, got {other:?}"),
202 }
203 }
204
205 #[tokio::test]
206 async fn test_storing_secret_as_a_secret_key() {
207 let (test_locator, mut cmd, _temp_dir) = set_up_test();
208 cmd.as_secret = true;
209 let global_args = global_args();
210
211 let result = cmd.run(&global_args).await;
212 assert!(result.is_ok());
213 let identity = test_locator.read_identity("test_name").unwrap();
214 assert!(matches!(identity, Key::Secret(Secret::SecretKey { .. })));
215 }
216
217 #[cfg(feature = "additional-libs")]
218 #[tokio::test]
219 async fn test_storing_secret_in_secure_store() {
220 use keyring::{mock, set_default_credential_builder};
221 set_default_credential_builder(mock::default_credential_builder());
222 let (test_locator, mut cmd, _temp_dir) = set_up_test();
223 cmd.secure_store = true;
224 let global_args = global_args();
225
226 let result = cmd.run(&global_args).await;
227 assert!(result.is_ok());
228 let identity = test_locator.read_identity("test_name").unwrap();
229 assert!(matches!(identity, Key::Secret(Secret::SecureStore { .. })));
230 }
231
232 #[cfg(feature = "additional-libs")]
233 #[tokio::test]
234 async fn test_generate_secure_store_caches_public_key_on_disk() {
235 use keyring::{mock, set_default_credential_builder};
236 set_default_credential_builder(mock::default_credential_builder());
237 let (test_locator, mut cmd, _temp_dir) = set_up_test();
238 cmd.secure_store = true;
239 let global_args = global_args();
240
241 cmd.run(&global_args).await.unwrap();
242
243 let identity = test_locator.read_identity("test_name").unwrap();
244 match identity {
245 Key::Secret(Secret::SecureStore {
246 public_key,
247 hd_path,
248 ..
249 }) => {
250 assert!(
251 public_key.is_some(),
252 "public_key should be cached on disk after `keys generate --secure-store`"
253 );
254 assert_eq!(hd_path, None);
255 }
256 other => panic!("expected SecureStore variant, got {other:?}"),
257 }
258 }
259
260 #[cfg(not(feature = "additional-libs"))]
261 #[tokio::test]
262 async fn test_storing_in_secure_store_returns_error_when_additional_libs_not_enabled() {
263 let (test_locator, mut cmd, _temp_dir) = set_up_test();
264 cmd.secure_store = true;
265 let global_args = global_args();
266
267 let result = cmd.run(&global_args).await;
268 assert!(result.is_err());
269 assert_eq!(
270 result.unwrap_err().to_string(),
271 format!("Secure Store keys are not allowed: additional-libs feature must be enabled")
272 );
273
274 let identity_result = test_locator.read_identity("test_name");
275 assert!(identity_result.is_err());
276 assert_eq!(
277 identity_result.unwrap_err().to_string(),
278 format!("Failed to find config identity for test_name")
279 );
280 }
281}