use crate::chain_registry::get_configs;
use abscissa_core::clap::Parser;
use abscissa_core::{Command, Runnable};
use itertools::Itertools;
use crate::conclude::Output;
use ibc_relayer::config::{store, ChainConfig, Config};
use std::collections::HashSet;
use std::path::PathBuf;
use tracing::{error, info, warn};
fn find_key(chain_config: &ChainConfig) -> Option<String> {
let keys = chain_config.list_keys().ok()?;
keys.into_iter().next().map(|(name, _)| name)
}
#[derive(Clone, Command, Debug, Parser, PartialEq, Eq)]
#[clap(
override_usage = "hermes config auto [OPTIONS] --output <PATH> --chain <CHAIN1_NAME:OPTIONAL_KEY_NAME> --chain <CHAIN2_NAME:OPTIONAL_KEY_NAME>"
)]
pub struct AutoCmd {
#[clap(
long = "output",
required = true,
value_name = "PATH",
help_heading = "REQUIRED",
help = "Path to the configuration file"
)]
path: PathBuf,
#[clap(
long = "chains",
alias = "chain",
required = true,
multiple = true,
value_name = "CHAIN_NAME:OPTIONAL_KEY_NAME",
help_heading = "REQUIRED",
help = "Names of the chains to include in the configuration, together with an optional key name. \
Either repeat this argument for every chain or pass a space-separated list of chains. \
Every chain must be found in the chain registry."
)]
chain_names: Vec<String>,
#[clap(
long = "commit",
value_name = "COMMIT_HASH",
help = "Commit hash from which the chain configs will be generated. If it's not set, the latest commit will be used."
)]
commit: Option<String>,
}
fn extract_chains_and_keys(chain_names: &[String]) -> Vec<(String, Option<String>)> {
let mut captured_names = chain_names
.iter()
.map(|chain_key| {
chain_key
.split_once(':')
.map(|(name, key)| (name.to_string(), Some(key.to_string())))
.unwrap_or_else(|| (chain_key.to_string(), None))
})
.collect::<Vec<_>>();
captured_names.sort_by(|a, b| a.0.cmp(&b.0));
captured_names
}
impl Runnable for AutoCmd {
fn run(&self) {
let runtime = tokio::runtime::Runtime::new().unwrap();
let names_and_keys = extract_chains_and_keys(&self.chain_names);
let chain_names = names_and_keys
.iter()
.map(|(n, _)| n)
.cloned()
.collect::<Vec<_>>();
let commit = self.commit.clone();
let config_results = runtime.block_on(get_configs(&chain_names, commit));
if let Err(e) = config_results {
let config = Config::default();
match store(&config, &self.path) {
Ok(_) => Output::error(format!(
"An error occurred while generating the chain config file: {}
A default config file has been written at '{}'",
e,
self.path.display(),
))
.exit(),
Err(e) => Output::error(format!(
"An error occurred while attempting to write the config file: {}",
e
))
.exit(),
}
};
let mut chain_configs: Vec<(String, ChainConfig)> = config_results
.unwrap()
.into_iter()
.filter_map(|(name, config)| match config {
Ok(config) => Some((name, config)),
Err(e) => {
error!("Failed to generate chain config for chain '{name}': {e}");
None
}
})
.collect();
let fetched_chains_set: HashSet<_> =
HashSet::from_iter(chain_configs.iter().map(|(name, _)| name).cloned());
let expected_chains_set: HashSet<_> = HashSet::from_iter(chain_names.iter().cloned());
let missing_chains_set: HashSet<_> = expected_chains_set
.difference(&fetched_chains_set)
.collect();
let configs_and_keys = chain_configs
.iter_mut()
.zip(names_and_keys.iter().map(|(_, keys)| keys).cloned());
for ((_name, chain_config), key_option) in configs_and_keys {
if let Some(key_name) = key_option {
info!("{}: uses key \"{}\"", &chain_config.id(), &key_name);
chain_config.set_key_name(key_name);
} else {
let chain_id = &chain_config.id();
let key = find_key(chain_config);
if let Some(key_name) = key {
info!("{}: uses key '{}'", &chain_id, &key_name);
chain_config.set_key_name(key_name);
} else {
warn!("No key found for chain: {}", chain_id);
}
}
}
let config = Config {
chains: chain_configs.into_iter().map(|(_, c)| c).collect(),
..Config::default()
};
match store(&config, &self.path) {
Ok(_) if missing_chains_set.is_empty() => {
Output::success_msg(format!(
"Config file written successfully at '{}'",
self.path.display()
))
.exit()
},
Ok(_) => {
Output::success_msg(format!(
"Config file written successfully at '{}'. \
However, configurations for the following chains were not able to be generated: {}",
self.path.display(),
missing_chains_set.iter().join(", "),
))
.exit()
},
Err(e) => Output::error(format!(
"An error occurred while attempting to write the config file: {}",
e
))
.exit(),
}
}
}
#[cfg(test)]
mod tests {
use super::AutoCmd;
use abscissa_core::clap::Parser;
use std::path::PathBuf;
#[test]
fn auto_config_without_commit() {
assert_eq!(
AutoCmd {
path: PathBuf::from("./example.toml"),
chain_names: vec!["chain1:key1".to_string(), "chain2".to_string()],
commit: None,
},
AutoCmd::parse_from([
"test",
"--output",
"./example.toml",
"--chains",
"chain1:key1",
"chain2",
])
)
}
#[test]
fn auto_config_with_commit() {
assert_eq!(
AutoCmd {
path: PathBuf::from("./example.toml"),
chain_names: vec!["chain1:key1".to_string(), "chain2".to_string()],
commit: Some("test_commit".to_string()),
},
AutoCmd::parse_from([
"test",
"--output",
"./example.toml",
"--chains",
"chain1:key1",
"chain2",
"--commit",
"test_commit"
])
)
}
}