hermes_cli/commands/keys/
add.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3use std::str::FromStr;
4
5use hdpath::StandardHDPath;
6use hermes_cli_components::traits::build::CanLoadBuilder;
7use hermes_cli_framework::command::CommandRunner;
8use hermes_cli_framework::output::Output;
9use ibc_relayer::chain::cosmos::config::CosmosSdkConfig;
10use ibc_relayer::keyring::{
11    AnySigningKeyPair, KeyRing, Secp256k1KeyPair, SigningKeyPair, SigningKeyPairSized, Store,
12};
13use ibc_relayer_types::core::ics24_host::identifier::ChainId;
14use oneline_eyre::eyre;
15use oneline_eyre::eyre::{eyre, WrapErr};
16use tracing::warn;
17
18use crate::contexts::app::HermesApp;
19
20/// The data structure that represents the arguments when invoking the `keys add` CLI command.
21///
22/// The command has one argument and two exclusive flags:
23///
24/// The command to add a key from a file:
25///
26/// `keys add [OPTIONS] --chain <CHAIN_ID> --key-file <KEY_FILE>`
27///
28/// The command to restore a key from a file containing its mnemonic:
29///
30/// `keys add [OPTIONS] --chain <CHAIN_ID> --mnemonic-file <MNEMONIC_FILE>`
31///
32/// On *nix platforms, both flags also accept `/dev/stdin` as a value, which will read the key or the mnemonic from stdin.
33///
34/// The `--key-file` and `--mnemonic-file` flags cannot both be provided at the same time, this will cause a terminating error.
35///
36/// If successful the key will be created or restored, depending on which flag was given.
37#[derive(Debug, clap::Parser)]
38#[clap(override_usage = "Add a key from a Comet keyring file:
39        hermes keys add [OPTIONS] --chain <CHAIN_ID> --key-file <KEY_FILE>
40
41    Add a key from a file containing its mnemonic:
42        hermes keys add [OPTIONS] --chain <CHAIN_ID> --mnemonic-file <MNEMONIC_FILE>
43
44    On *nix platforms, both flags also accept `/dev/stdin` as a value, which will read the key or the mnemonic from stdin.")]
45pub struct KeysAddCmd {
46    #[clap(
47        long = "chain",
48        required = true,
49        help_heading = "FLAGS",
50        help = "Identifier of the chain"
51    )]
52    chain_id: ChainId,
53
54    #[clap(
55        long = "key-file",
56        required = true,
57        value_name = "KEY_FILE",
58        help_heading = "FLAGS",
59        help = "Path to the key file, or /dev/stdin to read the content from stdin",
60        group = "add-restore"
61    )]
62    key_file: Option<PathBuf>,
63
64    #[clap(
65        long = "mnemonic-file",
66        required = true,
67        value_name = "MNEMONIC_FILE",
68        help_heading = "FLAGS",
69        help = "Path to file containing the mnemonic to restore the key from, or /dev/stdin to read the mnemonic from stdin",
70        group = "add-restore"
71    )]
72    mnemonic_file: Option<PathBuf>,
73
74    #[clap(
75        long = "key-name",
76        value_name = "KEY_NAME",
77        help = "Name of the key (defaults to the `key_name` defined in the config)"
78    )]
79    key_name: Option<String>,
80
81    #[clap(
82        long = "hd-path",
83        value_name = "HD_PATH",
84        help = "Derivation path for this key",
85        default_value = "m/44'/118'/0'/0/0"
86    )]
87    hd_path: String,
88
89    #[clap(
90        long = "overwrite",
91        help = "Overwrite the key if there is already one with the same key name"
92    )]
93    overwrite: bool,
94}
95
96impl KeysAddCmd {
97    fn options(&self, chain_config: &CosmosSdkConfig) -> eyre::Result<KeysAddOptions> {
98        let name = self
99            .key_name
100            .clone()
101            .unwrap_or_else(|| chain_config.key_name.to_string());
102
103        let hd_path = StandardHDPath::from_str(&self.hd_path)
104            .map_err(|_| eyre!("invalid derivation path: {}", self.hd_path))?;
105
106        Ok(KeysAddOptions {
107            config: chain_config.clone(),
108            name,
109            hd_path,
110        })
111    }
112}
113
114#[derive(Clone, Debug)]
115pub struct KeysAddOptions {
116    pub name: String,
117    pub config: CosmosSdkConfig,
118    pub hd_path: StandardHDPath,
119}
120
121pub fn add_key(
122    config: &CosmosSdkConfig,
123    key_name: &str,
124    file: &Path,
125    hd_path: &StandardHDPath,
126    overwrite: bool,
127) -> eyre::Result<AnySigningKeyPair> {
128    let mut keyring = KeyRing::new_secp256k1(
129        Store::Test,
130        &config.account_prefix,
131        &config.id,
132        &config.key_store_folder,
133    )?;
134
135    check_key_exists(&keyring, key_name, overwrite);
136
137    let key_contents = fs::read_to_string(file).wrap_err("error reading the key file")?;
138    let key_pair = Secp256k1KeyPair::from_seed_file(&key_contents, hd_path)?;
139
140    keyring.add_key(key_name, key_pair.clone())?;
141
142    Ok(key_pair.into())
143}
144
145pub fn restore_key(
146    mnemonic: &Path,
147    key_name: &str,
148    hdpath: &StandardHDPath,
149    config: &CosmosSdkConfig,
150    overwrite: bool,
151) -> eyre::Result<AnySigningKeyPair> {
152    let mnemonic_content =
153        fs::read_to_string(mnemonic).wrap_err("error reading the mnemonic file")?;
154
155    let mut keyring = KeyRing::new_secp256k1(
156        Store::Test,
157        &config.account_prefix,
158        &config.id,
159        &config.key_store_folder,
160    )?;
161
162    check_key_exists(&keyring, key_name, overwrite);
163
164    let key_pair = Secp256k1KeyPair::from_mnemonic(
165        &mnemonic_content,
166        hdpath,
167        &config.address_type,
168        keyring.account_prefix(),
169    )?;
170
171    keyring.add_key(key_name, key_pair.clone())?;
172
173    Ok(key_pair.into())
174}
175
176/// Check if the key with the given key name already exists.
177/// If it already exists and overwrite is false, abort the command with an error.
178/// If overwrite is true, output a warning message informing the key will be overwritten.
179fn check_key_exists<S: SigningKeyPairSized>(keyring: &KeyRing<S>, key_name: &str, overwrite: bool) {
180    if keyring.get_key(key_name).is_ok() {
181        if overwrite {
182            warn!("key {} will be overwritten", key_name);
183        } else {
184            Output::error(format!("key with name '{key_name}' already exists")).exit();
185        }
186    }
187}
188
189impl CommandRunner<HermesApp> for KeysAddCmd {
190    async fn run(&self, app: &HermesApp) -> hermes_cli_framework::Result<Output> {
191        let builder = app.load_builder().await?;
192
193        let chain_config = builder
194            .config_map
195            .get(&self.chain_id)
196            .ok_or_else(|| eyre!("no chain configuration found for chain `{}`", self.chain_id))?;
197
198        let opts = match self.options(chain_config) {
199            Err(err) => Output::error(err).exit(),
200            Ok(result) => result,
201        };
202
203        // Check if --key-file or --mnemonic-file was given as input.
204        match (self.key_file.clone(), self.mnemonic_file.clone()) {
205            (Some(key_file), _) => {
206                let key = add_key(
207                    &opts.config,
208                    &opts.name,
209                    &key_file,
210                    &opts.hd_path,
211                    self.overwrite,
212                );
213                match key {
214                    Ok(key) => Output::success_msg(format!(
215                        "added key '{}' ({}) on chain `{}`",
216                        opts.name,
217                        key.account(),
218                        opts.config.id,
219                    ))
220                    .exit(),
221                    Err(e) => Output::error(format!(
222                        "an error occurred adding the key on chain `{}` from file {:?}: {}",
223                        self.chain_id, key_file, e
224                    ))
225                    .exit(),
226                }
227            }
228            (_, Some(mnemonic_file)) => {
229                let key = restore_key(
230                    &mnemonic_file,
231                    &opts.name,
232                    &opts.hd_path,
233                    &opts.config,
234                    self.overwrite,
235                );
236
237                match key {
238                    Ok(key) => Output::success_msg(format!(
239                        "restored key '{}' ({}) on chain `{}`",
240                        opts.name,
241                        key.account(),
242                        opts.config.id
243                    ))
244                    .exit(),
245                    Err(e) => Output::error(format!(
246                        "failed to restore the key on chain `{}` from file {:?}: {}",
247                        self.chain_id, mnemonic_file, e
248                    ))
249                    .exit(),
250                }
251            }
252            // This case should never trigger.
253            // The 'required' parameter for the flags will trigger an error if both flags have not been given.
254            // And the 'group' parameter for the flags will trigger an error if both flags are given.
255            _ => Output::error(
256                "exactly one of --mnemonic-file and --key-file must be given".to_string(),
257            )
258            .exit(),
259        }
260    }
261}