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 config_dir: Some(temp_dir.path().to_path_buf()),
141 };
142
143 let cmd = super::Cmd {
144 name: KeyName("test_name".to_string()),
145 seed: None,
146 as_secret: false,
147 secure_store: false,
148 config_locator: locator.clone(),
149 hd_path: None,
150 network: super::network::Args::default(),
151 fund: false,
152 overwrite: false,
153 };
154
155 (locator, cmd)
156 }
157
158 fn global_args() -> super::global::Args {
159 super::global::Args {
160 quiet: true,
161 ..Default::default()
162 }
163 }
164
165 #[tokio::test]
166 async fn test_storing_secret_as_a_seed_phrase() {
167 let (test_locator, cmd) = set_up_test();
168 let global_args = global_args();
169
170 let result = cmd.run(&global_args).await;
171 assert!(result.is_ok());
172 let identity = test_locator.read_identity("test_name").unwrap();
173 assert!(matches!(identity, Key::Secret(Secret::SeedPhrase { .. })));
174 }
175
176 #[tokio::test]
177 async fn test_storing_secret_as_a_secret_key() {
178 let (test_locator, mut cmd) = set_up_test();
179 cmd.as_secret = true;
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::SecretKey { .. })));
186 }
187
188 #[cfg(feature = "additional-libs")]
189 #[tokio::test]
190 async fn test_storing_secret_in_secure_store() {
191 use keyring::{mock, set_default_credential_builder};
192 set_default_credential_builder(mock::default_credential_builder());
193 let (test_locator, mut cmd) = set_up_test();
194 cmd.secure_store = 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::SecureStore { .. })));
201 }
202
203 #[cfg(not(feature = "additional-libs"))]
204 #[tokio::test]
205 async fn test_storing_in_secure_store_returns_error_when_additional_libs_not_enabled() {
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_err());
212 assert_eq!(
213 result.unwrap_err().to_string(),
214 format!("Secure Store keys are not allowed: additional-libs feature must be enabled")
215 );
216
217 let identity_result = test_locator.read_identity("test_name");
218 assert!(identity_result.is_err());
219 assert_eq!(
220 identity_result.unwrap_err().to_string(),
221 format!("Failed to find config identity for test_name")
222 );
223 }
224}