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)]
55 pub hd_path: Option<usize>,
56
57 #[command(flatten)]
58 pub network: network::Args,
59
60 #[arg(long, default_value = "false")]
62 pub fund: bool,
63
64 #[arg(long)]
66 pub overwrite: bool,
67}
68
69impl Cmd {
70 pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
71 let print = Print::new(global_args.quiet);
72
73 if self.config_locator.read_identity(&self.name).is_ok() {
74 if !self.overwrite {
75 return Err(Error::IdentityAlreadyExists(self.name.to_string()));
76 }
77
78 print.exclaimln(format!("Overwriting identity '{}'", &self.name.to_string()));
79 }
80
81 let secret = self.secret(&print)?;
82 let path = self.config_locator.write_identity(&self.name, &secret)?;
83 print.checkln(format!("Key saved with alias {} in {path:?}", self.name));
84
85 if self.fund {
86 self.fund(&secret, &print).await?;
87 }
88
89 Ok(())
90 }
91
92 async fn fund(&self, secret: &Secret, print: &Print) -> Result<(), Error> {
93 let addr = secret.public_key(self.hd_path)?;
94 let network = self.network.get(&self.config_locator)?;
95 let formatted_name = self.name.to_string();
96
97 match network.fund_address(&addr).await {
98 Ok(()) => print.checkln(format!(
99 "Account {} funded on {:?}",
100 formatted_name, network.network_passphrase
101 )),
102 Err(e) => {
103 tracing::trace!("Account funding error: {:?}", e);
104
105 print.errorln(format!(
106 "Unable to fund account {} on {:?}",
107 formatted_name, network.network_passphrase
108 ));
109 }
110 }
111
112 Ok(())
113 }
114
115 fn secret(&self, print: &Print) -> Result<Secret, Error> {
116 let seed_phrase = self.seed_phrase()?;
117 if self.secure_store {
118 let secret = secure_store::save_secret(print, &self.name, &seed_phrase)?;
119 Ok(secret.parse()?)
120 } else if self.as_secret {
121 let secret: Secret = seed_phrase.into();
122 Ok(secret.private_key(self.hd_path)?.into())
123 } else {
124 Ok(seed_phrase.into())
125 }
126 }
127
128 fn seed_phrase(&self) -> Result<SeedPhrase, Error> {
129 Ok(secret::seed_phrase_from_seed(self.seed.as_deref())?)
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use crate::config::{address::KeyName, key::Key, secret::Secret};
136
137 fn set_up_test() -> (super::locator::Args, super::Cmd) {
138 let temp_dir = tempfile::tempdir().unwrap();
139 let locator = super::locator::Args {
140 global: false,
141 config_dir: Some(temp_dir.path().to_path_buf()),
142 };
143
144 let cmd = super::Cmd {
145 name: KeyName("test_name".to_string()),
146 seed: None,
147 as_secret: false,
148 secure_store: false,
149 config_locator: locator.clone(),
150 hd_path: None,
151 network: super::network::Args::default(),
152 fund: false,
153 overwrite: false,
154 };
155
156 (locator, cmd)
157 }
158
159 fn global_args() -> super::global::Args {
160 super::global::Args {
161 quiet: true,
162 ..Default::default()
163 }
164 }
165
166 #[tokio::test]
167 async fn test_storing_secret_as_a_seed_phrase() {
168 let (test_locator, cmd) = set_up_test();
169 let global_args = global_args();
170
171 let result = cmd.run(&global_args).await;
172 assert!(result.is_ok());
173 let identity = test_locator.read_identity("test_name").unwrap();
174 assert!(matches!(identity, Key::Secret(Secret::SeedPhrase { .. })));
175 }
176
177 #[tokio::test]
178 async fn test_storing_secret_as_a_secret_key() {
179 let (test_locator, mut cmd) = set_up_test();
180 cmd.as_secret = true;
181 let global_args = global_args();
182
183 let result = cmd.run(&global_args).await;
184 assert!(result.is_ok());
185 let identity = test_locator.read_identity("test_name").unwrap();
186 assert!(matches!(identity, Key::Secret(Secret::SecretKey { .. })));
187 }
188
189 #[cfg(feature = "additional-libs")]
190 #[tokio::test]
191 async fn test_storing_secret_in_secure_store() {
192 use keyring::{mock, set_default_credential_builder};
193 set_default_credential_builder(mock::default_credential_builder());
194 let (test_locator, mut cmd) = set_up_test();
195 cmd.secure_store = true;
196 let global_args = global_args();
197
198 let result = cmd.run(&global_args).await;
199 assert!(result.is_ok());
200 let identity = test_locator.read_identity("test_name").unwrap();
201 assert!(matches!(identity, Key::Secret(Secret::SecureStore { .. })));
202 }
203
204 #[cfg(not(feature = "additional-libs"))]
205 #[tokio::test]
206 async fn test_storing_in_secure_store_returns_error_when_additional_libs_not_enabled() {
207 let (test_locator, mut cmd) = set_up_test();
208 cmd.secure_store = true;
209 let global_args = global_args();
210
211 let result = cmd.run(&global_args).await;
212 assert!(result.is_err());
213 assert_eq!(
214 result.unwrap_err().to_string(),
215 format!("Secure Store keys are not allowed: additional-libs feature must be enabled")
216 );
217
218 let identity_result = test_locator.read_identity("test_name");
219 assert!(identity_result.is_err());
220 assert_eq!(
221 identity_result.unwrap_err().to_string(),
222 format!("Failed to find config identity for test_name")
223 );
224 }
225}