soroban_cli/commands/keys/
generate.rs

1use 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    /// Name of identity
34    pub name: KeyName,
35
36    /// Do not fund address
37    #[cfg(feature = "version_lt_23")]
38    #[arg(long)]
39    pub no_fund: bool,
40
41    /// Optional seed to use when generating seed phrase.
42    /// Random otherwise.
43    #[arg(long)]
44    pub seed: Option<String>,
45
46    /// Output the generated identity as a secret key
47    #[arg(long, short = 's')]
48    pub as_secret: bool,
49
50    /// Save in OS-specific secure store
51    #[arg(long)]
52    pub secure_store: bool,
53
54    #[command(flatten)]
55    pub config_locator: locator::Args,
56
57    /// When generating a secret key, which `hd_path` should be used from the original `seed_phrase`.
58    #[arg(long)]
59    pub hd_path: Option<usize>,
60
61    #[command(flatten)]
62    pub network: network::Args,
63
64    /// Fund generated key pair
65    #[arg(long, default_value = "false")]
66    pub fund: bool,
67
68    /// Overwrite existing identity if it already exists.
69    #[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        #[cfg(feature = "version_lt_23")]
86        if !self.fund {
87            print.warnln(
88                "Behavior of `generate` will change in the \
89            future, and it will no longer fund by default. If you want to fund please \
90            provide `--fund` flag. If you don't need to fund your keys in the future, ignore this \
91            warning. It can be suppressed with -q flag.",
92            );
93        }
94        let secret = self.secret(&print)?;
95        let path = self.config_locator.write_identity(&self.name, &secret)?;
96        print.checkln(format!("Key saved with alias {} in {path:?}", self.name));
97
98        #[cfg(feature = "version_lt_23")]
99        if !self.no_fund {
100            self.fund(&secret, &print).await?;
101        }
102        #[cfg(feature = "version_gte_23")]
103        if self.fund {
104            self.fund(&secret, &print).await?;
105        }
106
107        Ok(())
108    }
109
110    async fn fund(&self, secret: &Secret, print: &Print) -> Result<(), Error> {
111        let addr = secret.public_key(self.hd_path)?;
112        let network = self.network.get(&self.config_locator)?;
113        network
114            .fund_address(&addr)
115            .await
116            .map_err(|e| {
117                tracing::warn!("fund_address failed: {e}");
118            })
119            .unwrap_or_default();
120        print.checkln(format!(
121            "Account {:?} funded on {:?}",
122            self.name, network.network_passphrase
123        ));
124        Ok(())
125    }
126
127    fn secret(&self, print: &Print) -> Result<Secret, Error> {
128        let seed_phrase = self.seed_phrase()?;
129        if self.secure_store {
130            Ok(secure_store::save_secret(print, &self.name, seed_phrase)?)
131        } else if self.as_secret {
132            let secret: Secret = seed_phrase.into();
133            Ok(secret.private_key(self.hd_path)?.into())
134        } else {
135            Ok(seed_phrase.into())
136        }
137    }
138
139    fn seed_phrase(&self) -> Result<SeedPhrase, Error> {
140        Ok(secret::seed_phrase_from_seed(self.seed.as_deref())?)
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use crate::config::{address::KeyName, key::Key, secret::Secret};
147    use keyring::{mock, set_default_credential_builder};
148
149    fn set_up_test() -> (super::locator::Args, super::Cmd) {
150        let temp_dir = tempfile::tempdir().unwrap();
151        let locator = super::locator::Args {
152            global: false,
153            config_dir: Some(temp_dir.path().to_path_buf()),
154        };
155
156        let cmd = super::Cmd {
157            name: KeyName("test_name".to_string()),
158            #[cfg(feature = "version_lt_23")]
159            no_fund: true,
160            seed: None,
161            as_secret: false,
162            secure_store: false,
163            config_locator: locator.clone(),
164            hd_path: None,
165            network: super::network::Args::default(),
166            fund: false,
167            overwrite: false,
168        };
169
170        (locator, cmd)
171    }
172
173    fn global_args() -> super::global::Args {
174        super::global::Args {
175            quiet: true,
176            ..Default::default()
177        }
178    }
179
180    #[tokio::test]
181    async fn test_storing_secret_as_a_seed_phrase() {
182        let (test_locator, cmd) = set_up_test();
183        let global_args = global_args();
184
185        let result = cmd.run(&global_args).await;
186        assert!(result.is_ok());
187        let identity = test_locator.read_identity("test_name").unwrap();
188        assert!(matches!(identity, Key::Secret(Secret::SeedPhrase { .. })));
189    }
190
191    #[tokio::test]
192    async fn test_storing_secret_as_a_secret_key() {
193        let (test_locator, mut cmd) = set_up_test();
194        cmd.as_secret = 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::SecretKey { .. })));
201    }
202
203    #[tokio::test]
204    async fn test_storing_secret_in_secure_store() {
205        set_default_credential_builder(mock::default_credential_builder());
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_ok());
212        let identity = test_locator.read_identity("test_name").unwrap();
213        assert!(matches!(identity, Key::Secret(Secret::SecureStore { .. })));
214    }
215}