Skip to main content

soroban_cli/commands/keys/
generate.rs

1use 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    /// Name of identity
33    pub name: KeyName,
34
35    /// Optional seed to use when generating seed phrase.
36    /// Random otherwise.
37    #[arg(long)]
38    pub seed: Option<String>,
39
40    /// Output the generated identity as a secret key
41    #[arg(long, short = 's')]
42    pub as_secret: bool,
43
44    /// Save the new key in your OS's credential secure store.
45    ///
46    /// On Mac this uses Keychain, on Windows it is Secure Store Service, and on *nix platforms it uses a combination of the kernel keyutils and DBus-based Secret Service.
47    #[arg(long)]
48    pub secure_store: bool,
49
50    #[command(flatten)]
51    pub config_locator: locator::Args,
52
53    /// When generating a secret key, which `hd_path` should be used from the original `seed_phrase`.
54    #[arg(long)]
55    pub hd_path: Option<usize>,
56
57    #[command(flatten)]
58    pub network: network::Args,
59
60    /// Fund generated key pair
61    #[arg(long, default_value = "false")]
62    pub fund: bool,
63
64    /// Overwrite existing identity if it already exists.
65    #[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}