use std::fs;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use hdpath::StandardHDPath;
use hermes_cli_components::traits::build::CanLoadBuilder;
use hermes_cli_framework::command::CommandRunner;
use hermes_cli_framework::output::Output;
use ibc_relayer::chain::cosmos::config::CosmosSdkConfig;
use ibc_relayer::keyring::{
AnySigningKeyPair, KeyRing, Secp256k1KeyPair, SigningKeyPair, SigningKeyPairSized, Store,
};
use ibc_relayer_types::core::ics24_host::identifier::ChainId;
use oneline_eyre::eyre;
use oneline_eyre::eyre::{eyre, WrapErr};
use tracing::warn;
use crate::contexts::app::HermesApp;
#[derive(Debug, clap::Parser)]
#[clap(override_usage = "Add a key from a Comet keyring file:
hermes keys add [OPTIONS] --chain <CHAIN_ID> --key-file <KEY_FILE>
Add a key from a file containing its mnemonic:
hermes keys add [OPTIONS] --chain <CHAIN_ID> --mnemonic-file <MNEMONIC_FILE>
On *nix platforms, both flags also accept `/dev/stdin` as a value, which will read the key or the mnemonic from stdin.")]
pub struct KeysAddCmd {
#[clap(
long = "chain",
required = true,
help_heading = "FLAGS",
help = "Identifier of the chain"
)]
chain_id: ChainId,
#[clap(
long = "key-file",
required = true,
value_name = "KEY_FILE",
help_heading = "FLAGS",
help = "Path to the key file, or /dev/stdin to read the content from stdin",
group = "add-restore"
)]
key_file: Option<PathBuf>,
#[clap(
long = "mnemonic-file",
required = true,
value_name = "MNEMONIC_FILE",
help_heading = "FLAGS",
help = "Path to file containing the mnemonic to restore the key from, or /dev/stdin to read the mnemonic from stdin",
group = "add-restore"
)]
mnemonic_file: Option<PathBuf>,
#[clap(
long = "key-name",
value_name = "KEY_NAME",
help = "Name of the key (defaults to the `key_name` defined in the config)"
)]
key_name: Option<String>,
#[clap(
long = "hd-path",
value_name = "HD_PATH",
help = "Derivation path for this key",
default_value = "m/44'/118'/0'/0/0"
)]
hd_path: String,
#[clap(
long = "overwrite",
help = "Overwrite the key if there is already one with the same key name"
)]
overwrite: bool,
}
impl KeysAddCmd {
fn options(&self, chain_config: &CosmosSdkConfig) -> eyre::Result<KeysAddOptions> {
let name = self
.key_name
.clone()
.unwrap_or_else(|| chain_config.key_name.to_string());
let hd_path = StandardHDPath::from_str(&self.hd_path)
.map_err(|_| eyre!("invalid derivation path: {}", self.hd_path))?;
Ok(KeysAddOptions {
config: chain_config.clone(),
name,
hd_path,
})
}
}
#[derive(Clone, Debug)]
pub struct KeysAddOptions {
pub name: String,
pub config: CosmosSdkConfig,
pub hd_path: StandardHDPath,
}
pub fn add_key(
config: &CosmosSdkConfig,
key_name: &str,
file: &Path,
hd_path: &StandardHDPath,
overwrite: bool,
) -> eyre::Result<AnySigningKeyPair> {
let mut keyring = KeyRing::new_secp256k1(
Store::Test,
&config.account_prefix,
&config.id,
&config.key_store_folder,
)?;
check_key_exists(&keyring, key_name, overwrite);
let key_contents = fs::read_to_string(file).wrap_err("error reading the key file")?;
let key_pair = Secp256k1KeyPair::from_seed_file(&key_contents, hd_path)?;
keyring.add_key(key_name, key_pair.clone())?;
Ok(key_pair.into())
}
pub fn restore_key(
mnemonic: &Path,
key_name: &str,
hdpath: &StandardHDPath,
config: &CosmosSdkConfig,
overwrite: bool,
) -> eyre::Result<AnySigningKeyPair> {
let mnemonic_content =
fs::read_to_string(mnemonic).wrap_err("error reading the mnemonic file")?;
let mut keyring = KeyRing::new_secp256k1(
Store::Test,
&config.account_prefix,
&config.id,
&config.key_store_folder,
)?;
check_key_exists(&keyring, key_name, overwrite);
let key_pair = Secp256k1KeyPair::from_mnemonic(
&mnemonic_content,
hdpath,
&config.address_type,
keyring.account_prefix(),
)?;
keyring.add_key(key_name, key_pair.clone())?;
Ok(key_pair.into())
}
fn check_key_exists<S: SigningKeyPairSized>(keyring: &KeyRing<S>, key_name: &str, overwrite: bool) {
if keyring.get_key(key_name).is_ok() {
if overwrite {
warn!("key {} will be overwritten", key_name);
} else {
Output::error(format!("key with name '{key_name}' already exists")).exit();
}
}
}
impl CommandRunner<HermesApp> for KeysAddCmd {
async fn run(&self, app: &HermesApp) -> hermes_cli_framework::Result<Output> {
let builder = app.load_builder().await?;
let chain_config = builder
.config_map
.get(&self.chain_id)
.ok_or_else(|| eyre!("no chain configuration found for chain `{}`", self.chain_id))?;
let opts = match self.options(chain_config) {
Err(err) => Output::error(err).exit(),
Ok(result) => result,
};
match (self.key_file.clone(), self.mnemonic_file.clone()) {
(Some(key_file), _) => {
let key = add_key(
&opts.config,
&opts.name,
&key_file,
&opts.hd_path,
self.overwrite,
);
match key {
Ok(key) => Output::success_msg(format!(
"added key '{}' ({}) on chain `{}`",
opts.name,
key.account(),
opts.config.id,
))
.exit(),
Err(e) => Output::error(format!(
"an error occurred adding the key on chain `{}` from file {:?}: {}",
self.chain_id, key_file, e
))
.exit(),
}
}
(_, Some(mnemonic_file)) => {
let key = restore_key(
&mnemonic_file,
&opts.name,
&opts.hd_path,
&opts.config,
self.overwrite,
);
match key {
Ok(key) => Output::success_msg(format!(
"restored key '{}' ({}) on chain `{}`",
opts.name,
key.account(),
opts.config.id
))
.exit(),
Err(e) => Output::error(format!(
"failed to restore the key on chain `{}` from file {:?}: {}",
self.chain_id, mnemonic_file, e
))
.exit(),
}
}
_ => Output::error(
"exactly one of --mnemonic-file and --key-file must be given".to_string(),
)
.exit(),
}
}
}