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