diff --git a/Cargo.lock b/Cargo.lock index 32dfc0725..24aa8dc7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2056,6 +2056,7 @@ dependencies = [ "ibc-relayer", "ibc-relayer-types", "oneline-eyre", + "serde_json", "tracing", ] @@ -2068,6 +2069,8 @@ dependencies = [ "hermes-cosmos-relayer", "hermes-relayer-runtime", "oneline-eyre", + "serde", + "serde_json", "tokio", "tracing", "tracing-subscriber", @@ -6320,6 +6323,16 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.18" @@ -6330,12 +6343,15 @@ dependencies = [ "nu-ansi-term", "once_cell", "regex", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] diff --git a/crates/cli/cli-framework/Cargo.toml b/crates/cli/cli-framework/Cargo.toml index 023189582..0c463a553 100644 --- a/crates/cli/cli-framework/Cargo.toml +++ b/crates/cli/cli-framework/Cargo.toml @@ -20,5 +20,7 @@ cgp-core = { workspace = true } clap = { workspace = true } tokio = { workspace = true, features = ["full"] } tracing = { workspace = true } -tracing-subscriber = { workspace = true, features = ["env-filter"] } +tracing-subscriber = { workspace = true, features = ["env-filter", "json"] } oneline-eyre = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } diff --git a/crates/cli/cli-framework/src/application/boot.rs b/crates/cli/cli-framework/src/application/boot.rs index 79e42a502..56160c860 100644 --- a/crates/cli/cli-framework/src/application/boot.rs +++ b/crates/cli/cli-framework/src/application/boot.rs @@ -7,20 +7,24 @@ use hermes_relayer_runtime::types::runtime::HermesRuntime; use crate::application::log::{enable_ansi, install_logger}; use crate::application::Application; use crate::config::Config; +use crate::output; use crate::Result; pub fn boot() -> Result<()> where A: Application, { - let with_color = enable_ansi(); - install_logger(with_color); - oneline_eyre::install()?; let app = A::parse_from_env(); let config_path = app.config_path(); + let with_color = enable_ansi(); + let with_json = app.json_output(); + install_logger(with_color, with_json); + + output::set_json(with_json); + let config = A::Config::load_from_path(config_path).map_err(|e| eyre!("failed to load config: {e}"))?; @@ -32,8 +36,17 @@ where let rt = HermesRuntime::new(Arc::new(rt)); rt.runtime - .block_on(app.run(rt.clone(), config)) + .block_on(run(rt.clone(), app, config)) .map_err(|e| eyre!("Hermes command exited with an error: {e}"))?; Ok(()) } + +pub async fn run(rt: HermesRuntime, app: A, config: A::Config) -> Result<()> +where + A: Application, +{ + let output = app.run(rt, config).await?; + output.print(); + Ok(()) +} diff --git a/crates/cli/cli-framework/src/application/log.rs b/crates/cli/cli-framework/src/application/log.rs index 0e7023e47..e0832d64c 100644 --- a/crates/cli/cli-framework/src/application/log.rs +++ b/crates/cli/cli-framework/src/application/log.rs @@ -2,25 +2,29 @@ Install the [`tracing_subscriber`] logger handlers so that logs will be displayed during test. */ -pub fn install_logger(with_color: bool) { +pub fn install_logger(with_color: bool, with_json: bool) { use tracing::level_filters::LevelFilter; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::EnvFilter; + use tracing_subscriber::{fmt, registry}; // Use log level INFO by default if RUST_LOG is not set. let env_filter = EnvFilter::builder() .with_default_directive(LevelFilter::INFO.into()) .from_env_lossy(); - let layer = tracing_subscriber::fmt::layer() - .with_ansi(with_color) - .with_target(false); + if with_json { + let fmt_layer = fmt::layer().with_target(false).json(); + registry().with(env_filter).with(fmt_layer).init(); + } else { + let fmt_layer = fmt::layer() + .with_ansi(with_color) + .with_target(false) + .compact(); - tracing_subscriber::registry() - .with(env_filter) - .with(layer) - .init(); + registry().with(env_filter).with(fmt_layer).init(); + }; } /// Check if both stdout and stderr are proper terminal (tty), diff --git a/crates/cli/cli-framework/src/application/mod.rs b/crates/cli/cli-framework/src/application/mod.rs index 03ddf4dab..4ce9b47fd 100644 --- a/crates/cli/cli-framework/src/application/mod.rs +++ b/crates/cli/cli-framework/src/application/mod.rs @@ -10,6 +10,7 @@ use hermes_relayer_runtime::types::runtime::HermesRuntime; use crate::command::Runnable; use crate::config::Config; +use crate::output::Output; use crate::Result; #[async_trait] @@ -21,5 +22,7 @@ pub trait Application: Sized { fn config_path(&self) -> &Path; - async fn run(&self, runtime: HermesRuntime, config: Self::Config) -> Result<()>; + fn json_output(&self) -> bool; + + async fn run(&self, runtime: HermesRuntime, config: Self::Config) -> Result; } diff --git a/crates/cli/cli-framework/src/command.rs b/crates/cli/cli-framework/src/command.rs index d3cac0479..03a129d5e 100644 --- a/crates/cli/cli-framework/src/command.rs +++ b/crates/cli/cli-framework/src/command.rs @@ -1,9 +1,10 @@ use cgp_core::async_trait; use hermes_cosmos_relayer::contexts::builder::CosmosBuilder; +use crate::output::Output; use crate::Result; #[async_trait] pub trait Runnable { - async fn run(&self, builder: CosmosBuilder) -> Result<()>; + async fn run(&self, builder: CosmosBuilder) -> Result; } diff --git a/crates/cli/cli-framework/src/lib.rs b/crates/cli/cli-framework/src/lib.rs index 5c567bf5b..458847261 100644 --- a/crates/cli/cli-framework/src/lib.rs +++ b/crates/cli/cli-framework/src/lib.rs @@ -3,5 +3,6 @@ pub mod application; pub mod command; pub mod config; +pub mod output; pub type Result = oneline_eyre::Result; diff --git a/crates/cli/cli-framework/src/output.rs b/crates/cli/cli-framework/src/output.rs new file mode 100644 index 000000000..4d44d77bd --- /dev/null +++ b/crates/cli/cli-framework/src/output.rs @@ -0,0 +1,252 @@ +//! Custom-made solution to output a JSON return message and ensure a return code +//! from a CLI command. The main use-case for this module is to provide a consistent output for +//! queries and transactions. +//! +//! The examples below rely on crate-private methods (for this reason, doctests are ignored). +//! They are intended for contributors to crate `relayer-cli`, and _not_ for users of this binary. +//! +//! ## Examples on how to use the quick-access constructors: +//! +//! - Exit from a query/tx with a `String` error: +//! +//! ```ignore +//! let e = String::from("error message"); +//! Output::error(e).exit(); +//! // or as an alternative: +//! Output::error(json!("error occurred")).exit(); +//! ``` +//! +//! - Exit from a query/tx with an error of type `anomaly`: +//! In the case where the error is a complex type such as anomaly (including backtraces), it is +//! better to simplify the output and only write out the chain of error sources, which we can +//! achieve with `format!("{}", e)`. The complete solution is as follows: +//! +//! ```ignore +//! let e: Error = Kind::Query.into(); +//! Output::error(format!("{}", e)).exit(); +//! ``` +//! +//! #### Note: +//! The resulting output that this approach generates is determined by the 'error' annotation given +//! to the error object `Kind::Query`. If this error object comprises any positional arguments, +//! e.g. as achieved by `Query(String, String)`, then it is important to cover these arguments +//! in the `error` annotation, for instance: +//! ```ignore +//! #[derive(Debug, Error)] +//! pub enum Kind { +//! #[error("failed with underlying causes: {0}, {1}")] +//! Query(String, String), +//! // ... +//! } +//! ``` +//! +//! - Exit from a query/tx with success: +//! +//! ```ignore +//! let cs = ChannelEnd::default(); +//! Output::success(cs).exit(); +//! ``` +//! +//! - Exit from a query/tx with success and multiple objects in the result: +//! +//! ```ignore +//! let h = Height::default(); +//! let end = ConnectionEnd::default(); +//! Output::success(h).with_result(end).exit(); +//! ``` + +use core::fmt; +use std::sync::OnceLock; + +use serde::Serialize; +use tracing::{error, info}; + +static JSON: OnceLock = OnceLock::new(); + +/// Functional-style method to exit a program. +/// +/// ## Note: See `Output::exit()` for the preferred method of exiting a relayer command. +pub fn exit_with(out: Output) -> ! { + let status = out.status; + + out.print(); + + // The return code + if status == Status::Error { + std::process::exit(1); + } else { + std::process::exit(0); + } +} + +/// Return whether or not JSON output is enabled. +pub fn json() -> bool { + *JSON.get_or_init(|| false) +} + +/// Set whether or not JSON output is enabled. +pub fn set_json(enabled: bool) { + JSON.set(enabled).expect("failed to set JSON mode") +} + +/// Exits the program. Useful when a type produces an error which can no longer be propagated, and +/// the program must exit instead. +/// +/// ## Example of use +/// - Without this function: +/// ```ignore +/// let res = ForeignClient::new(chains.src.clone(), chains.dst.clone()); +/// let client = match res { +/// Ok(client) => client, +/// Err(e) => Output::error(format!("{}", e)).exit(), +/// }; +/// ``` +/// - With support from `exit_with_unrecoverable_error`: +/// ```ignore +/// let client_a = ForeignClient::new(chains.src.clone(), chains.dst.clone()) +/// .unwrap_or_else(exit_with_unrecoverable_error); +/// ``` +pub fn exit_with_unrecoverable_error(err: E) -> T { + Output::error(format!("{err}")).exit() +} + +/// The result to display before quitting, can either be a JSON value, some plain text, +/// a value to print with its Debug instance, or nothing. +#[derive(Debug)] +pub enum Result { + Json(serde_json::Value), + Text(String), + Nothing, +} + +impl fmt::Display for Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Result::Json(v) => write!(f, "{}", serde_json::to_string_pretty(v).unwrap()), + Result::Text(t) => write!(f, "{t}"), + Result::Nothing => Ok(()), + } + } +} + +/// A CLI output with support for JSON serialization. The only mandatory field is the `status`, +/// which typically signals a success (UNIX process return code `0`) or an error (code `1`). An +/// optional `result` can be added to an output. +/// +pub struct Output { + /// The return status + pub status: Status, + + /// The result of a command, such as the output from a query or transaction. + pub result: Result, +} + +impl Output { + /// Constructs a new `Output` with the provided `status` and an empty `result`. + pub fn new(status: Status) -> Self { + Output { + status, + result: Result::Nothing, + } + } + + /// Constructor that returns a new `Output` having a `Success` status and empty `result`. + pub fn with_success() -> Self { + Output::new(Status::Success) + } + + /// Constructor that returns a new `Output` having an `Error` status and empty `result`. + pub fn with_error() -> Self { + Output::new(Status::Error) + } + + /// Builder-style method for attaching a result to an output object. + pub fn with_result(mut self, result: R) -> Self + where + R: Serialize + 'static, + { + self.result = Result::Json(serde_json::to_value(result).unwrap()); + self + } + + /// Builder-style method for attaching a plain text message to an output object. + pub fn with_msg(mut self, msg: impl ToString) -> Self { + self.result = Result::Text(msg.to_string()); + self + } + + /// Quick-access constructor for an output signalling a success `status` and tagged with the + /// input `result`. + pub fn success(result: R) -> Self + where + R: Serialize + 'static, + { + Output::with_success().with_result(result) + } + + /// Quick-access constructor for an output message signalling a error `status`. + pub fn error(msg: impl ToString) -> Self { + Output::with_error().with_msg(msg) + } + + /// Quick-access constructor for an output signalling a success `status` and tagged with the + /// input `result`. + pub fn success_msg(msg: impl ToString) -> Self { + Output::with_success().with_msg(msg) + } + + pub fn print(self) { + if json() { + println!("{}", serde_json::to_string(&self.into_json()).unwrap()); + } else { + match self.status { + Status::Success => info!("{}", self.result), + Status::Error => error!("{}", self.result), + } + } + } + + /// Exits from the process with the current output. Convenience wrapper over `exit_with`. + pub fn exit(self) -> ! { + exit_with(self); + } + + /// Convert this output value to a JSON value + pub fn into_json(self) -> serde_json::Value { + let mut map = serde_json::Map::new(); + + map.insert( + "status".to_string(), + serde_json::to_value(self.status).unwrap(), + ); + + let value = match self.result { + Result::Json(v) => v, + Result::Text(v) => serde_json::Value::String(v), + Result::Nothing => serde_json::Value::String("no output".to_string()), + }; + + map.insert("result".to_string(), value); + + serde_json::Value::Object(map) + } +} + +/// Represents the exit status of any CLI command +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize)] +pub enum Status { + #[serde(rename(serialize = "success"))] + Success, + + #[serde(rename(serialize = "error"))] + Error, +} + +impl fmt::Display for Status { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Status::Success => write!(f, "Success"), + Status::Error => write!(f, "Error"), + } + } +} diff --git a/crates/cli/cli/Cargo.toml b/crates/cli/cli/Cargo.toml index 464a30aec..f9abd5571 100644 --- a/crates/cli/cli/Cargo.toml +++ b/crates/cli/cli/Cargo.toml @@ -31,3 +31,4 @@ clap = { workspace = true, features = ["derive"] } oneline-eyre = { workspace = true } humantime = { workspace = true } tracing = { workspace = true } +serde_json = { workspace = true } diff --git a/crates/cli/cli/bin/hermes.rs b/crates/cli/cli/bin/hermes.rs index 445cfd359..daec4426e 100644 --- a/crates/cli/cli/bin/hermes.rs +++ b/crates/cli/cli/bin/hermes.rs @@ -1,3 +1,5 @@ +#![recursion_limit = "256"] + use hermes_cli::application::HermesCli; use hermes_cli::Result; use hermes_cli_framework::application::boot; diff --git a/crates/cli/cli/src/application/mod.rs b/crates/cli/cli/src/application/mod.rs index 9cf1627ef..dea7ec34d 100644 --- a/crates/cli/cli/src/application/mod.rs +++ b/crates/cli/cli/src/application/mod.rs @@ -2,6 +2,7 @@ use std::path::{Path, PathBuf}; use hermes_cli_framework::application::Application; use hermes_cli_framework::command::Runnable; +use hermes_cli_framework::output::Output; use hermes_cosmos_relayer::contexts::builder::CosmosBuilder; use hermes_relayer_runtime::types::runtime::HermesRuntime; @@ -14,6 +15,9 @@ pub struct HermesCli { #[clap(short = 'c', long = "config", default_value = "config.toml")] pub config_path: PathBuf, + #[clap(long)] + pub json: bool, + #[clap(subcommand)] pub command: HermesCommand, } @@ -26,11 +30,15 @@ impl Application for HermesCli { &self.config_path } + fn json_output(&self) -> bool { + self.json + } + fn parse_from_env() -> Self { clap::Parser::parse() } - async fn run(&self, runtime: HermesRuntime, config: Self::Config) -> Result<()> { + async fn run(&self, runtime: HermesRuntime, config: Self::Config) -> Result { let builder = CosmosBuilder::new( config.config, runtime, diff --git a/crates/cli/cli/src/commands/channel/create.rs b/crates/cli/cli/src/commands/channel/create.rs index 7bcb88867..20d0e7d29 100644 --- a/crates/cli/cli/src/commands/channel/create.rs +++ b/crates/cli/cli/src/commands/channel/create.rs @@ -1,4 +1,5 @@ use hermes_cli_framework::command::Runnable; +use hermes_cli_framework::output::Output; use hermes_cosmos_client_components::types::channel::CosmosInitChannelOptions; use hermes_cosmos_relayer::contexts::builder::CosmosBuilder; use hermes_relayer_components::build::components::relay::build_from_chain::BuildRelayFromChains; @@ -94,7 +95,7 @@ pub struct ChannelCreate { } impl Runnable for ChannelCreate { - async fn run(&self, builder: CosmosBuilder) -> Result<()> { + async fn run(&self, builder: CosmosBuilder) -> Result { let relay = BuildRelayFromChains::build_relay( &builder, RelayAToBTarget, @@ -133,6 +134,6 @@ impl Runnable for ChannelCreate { self.chain_id_a, self.chain_id_b, ); - Ok(()) + Ok(Output::success_msg("Done")) } } diff --git a/crates/cli/cli/src/commands/channel/mod.rs b/crates/cli/cli/src/commands/channel/mod.rs index dac32d4dd..e648c8b5a 100644 --- a/crates/cli/cli/src/commands/channel/mod.rs +++ b/crates/cli/cli/src/commands/channel/mod.rs @@ -2,6 +2,7 @@ pub mod create; pub use create::ChannelCreate; use hermes_cli_framework::command::Runnable; +use hermes_cli_framework::output::Output; use hermes_cosmos_relayer::contexts::builder::CosmosBuilder; use crate::Result; @@ -12,8 +13,8 @@ pub enum ChannelCommands { Create(ChannelCreate), } -impl ChannelCommands { - pub async fn run(&self, builder: CosmosBuilder) -> Result<()> { +impl Runnable for ChannelCommands { + async fn run(&self, builder: CosmosBuilder) -> Result { match self { Self::Create(cmd) => cmd.run(builder).await, } diff --git a/crates/cli/cli/src/commands/client/create.rs b/crates/cli/cli/src/commands/client/create.rs index 3ec303008..bce4202e1 100644 --- a/crates/cli/cli/src/commands/client/create.rs +++ b/crates/cli/cli/src/commands/client/create.rs @@ -1,4 +1,5 @@ use hermes_cli_framework::command::Runnable; +use hermes_cli_framework::output::Output; use hermes_cosmos_relayer::contexts::builder::CosmosBuilder; use hermes_cosmos_relayer::contexts::relay::CosmosRelay; use hermes_relayer_components::relay::traits::client_creator::CanCreateClient; @@ -64,7 +65,7 @@ pub struct ClientCreate { } impl Runnable for ClientCreate { - async fn run(&self, builder: CosmosBuilder) -> Result<()> { + async fn run(&self, builder: CosmosBuilder) -> Result { let host_chain_config = builder .config @@ -116,7 +117,7 @@ impl Runnable for ClientCreate { self.host_chain_id, ); - Ok(()) + Ok(Output::success_msg("Done")) } } diff --git a/crates/cli/cli/src/commands/client/mod.rs b/crates/cli/cli/src/commands/client/mod.rs index 965f8991c..4d1f1674d 100644 --- a/crates/cli/cli/src/commands/client/mod.rs +++ b/crates/cli/cli/src/commands/client/mod.rs @@ -2,6 +2,7 @@ mod create; pub use create::ClientCreate; use hermes_cli_framework::command::Runnable; +use hermes_cli_framework::output::Output; use hermes_cosmos_relayer::contexts::builder::CosmosBuilder; use crate::Result; @@ -12,8 +13,8 @@ pub enum ClientCommands { Create(ClientCreate), } -impl ClientCommands { - pub async fn run(&self, builder: CosmosBuilder) -> Result<()> { +impl Runnable for ClientCommands { + async fn run(&self, builder: CosmosBuilder) -> Result { match self { Self::Create(cmd) => cmd.run(builder).await, } diff --git a/crates/cli/cli/src/commands/connection/create.rs b/crates/cli/cli/src/commands/connection/create.rs index c277e2b55..5491c02cf 100644 --- a/crates/cli/cli/src/commands/connection/create.rs +++ b/crates/cli/cli/src/commands/connection/create.rs @@ -1,6 +1,7 @@ use std::time::Duration; use hermes_cli_framework::command::Runnable; +use hermes_cli_framework::output::Output; use hermes_cosmos_client_components::types::connection::CosmosInitConnectionOptions; use hermes_cosmos_relayer::contexts::builder::CosmosBuilder; use hermes_relayer_components::build::components::relay::build_from_chain::BuildRelayFromChains; @@ -54,7 +55,7 @@ pub struct ConnectionCreate { } impl Runnable for ConnectionCreate { - async fn run(&self, builder: CosmosBuilder) -> Result<()> { + async fn run(&self, builder: CosmosBuilder) -> Result { let relay = BuildRelayFromChains::build_relay( &builder, RelayAToBTarget, @@ -91,6 +92,6 @@ impl Runnable for ConnectionCreate { self.chain_id_a, self.client_id_a, self.chain_id_b, self.client_id_b ); - Ok(()) + Ok(Output::success_msg("Done")) } } diff --git a/crates/cli/cli/src/commands/connection/mod.rs b/crates/cli/cli/src/commands/connection/mod.rs index e6d1e000b..6e76d99cd 100644 --- a/crates/cli/cli/src/commands/connection/mod.rs +++ b/crates/cli/cli/src/commands/connection/mod.rs @@ -2,6 +2,7 @@ mod create; pub use create::ConnectionCreate; use hermes_cli_framework::command::Runnable; +use hermes_cli_framework::output::Output; use hermes_cosmos_relayer::contexts::builder::CosmosBuilder; use crate::Result; @@ -12,8 +13,8 @@ pub enum ConnectionCommands { Create(ConnectionCreate), } -impl ConnectionCommands { - pub async fn run(&self, builder: CosmosBuilder) -> Result<()> { +impl Runnable for ConnectionCommands { + async fn run(&self, builder: CosmosBuilder) -> Result { match self { Self::Create(cmd) => cmd.run(builder).await, } diff --git a/crates/cli/cli/src/commands/mod.rs b/crates/cli/cli/src/commands/mod.rs index 48b1d32a4..3dc449b65 100644 --- a/crates/cli/cli/src/commands/mod.rs +++ b/crates/cli/cli/src/commands/mod.rs @@ -1,4 +1,5 @@ use hermes_cli_framework::command::Runnable; +use hermes_cli_framework::output::Output; use hermes_cosmos_relayer::contexts::builder::CosmosBuilder; use crate::Result; @@ -26,13 +27,13 @@ pub enum HermesCommand { #[clap(subcommand)] Channel(channel::ChannelCommands), - /// Queries + /// Query information about IBC objects #[clap(subcommand)] Query(query::QueryCommands), } impl Runnable for HermesCommand { - async fn run(&self, builder: CosmosBuilder) -> Result<()> { + async fn run(&self, builder: CosmosBuilder) -> Result { match self { Self::Start(cmd) => cmd.run(builder).await, Self::Client(cmd) => cmd.run(builder).await, diff --git a/crates/cli/cli/src/commands/query/client/mod.rs b/crates/cli/cli/src/commands/query/client/mod.rs new file mode 100644 index 000000000..8c35d4627 --- /dev/null +++ b/crates/cli/cli/src/commands/query/client/mod.rs @@ -0,0 +1,22 @@ +mod state; +use hermes_cli_framework::output::Output; +pub use state::QueryClientState; + +use hermes_cli_framework::command::Runnable; +use hermes_cosmos_relayer::contexts::builder::CosmosBuilder; + +use crate::Result; + +#[derive(Debug, clap::Subcommand)] +pub enum ClientCommands { + /// Query the state of a client + State(QueryClientState), +} + +impl Runnable for ClientCommands { + async fn run(&self, builder: CosmosBuilder) -> Result { + match self { + Self::State(cmd) => cmd.run(builder).await, + } + } +} diff --git a/crates/cli/cli/src/commands/query/client/state.rs b/crates/cli/cli/src/commands/query/client/state.rs new file mode 100644 index 000000000..70c298de4 --- /dev/null +++ b/crates/cli/cli/src/commands/query/client/state.rs @@ -0,0 +1,79 @@ +use hermes_cli_framework::command::Runnable; +use hermes_cli_framework::output::Output; +use hermes_cosmos_client_components::traits::chain_handle::HasBlockingChainHandle; +use hermes_cosmos_relayer::contexts::builder::CosmosBuilder; +use hermes_cosmos_relayer::types::error::BaseError; +use ibc_relayer::chain::handle::ChainHandle; +use ibc_relayer::chain::requests::{IncludeProof, QueryClientStateRequest, QueryHeight}; +use ibc_relayer_types::core::ics02_client::height::Height; +use ibc_relayer_types::core::ics24_host::identifier::{ChainId, ClientId}; +use oneline_eyre::eyre::Context; +use tracing::info; + +use crate::Result; + +#[derive(Debug, clap::Parser)] +pub struct QueryClientState { + /// Identifier of the host chain + #[clap( + long = "chain", + required = true, + value_name = "CHAIN_ID", + help_heading = "REQUIRED" + )] + chain_id: ChainId, + + /// Identifier of the client on the host chain + #[clap( + long = "client", + required = true, + value_name = "CLIENT_ID", + help_heading = "REQUIRED" + )] + client_id: ClientId, + + #[clap( + long = "height", + value_name = "HEIGHT", + help = "The height at which to query the client state. If not specified, the latest height is used." + )] + height: Option, +} + +impl Runnable for QueryClientState { + async fn run(&self, builder: CosmosBuilder) -> Result { + let chain = builder.build_chain(&self.chain_id).await?; + + let height = self.height.map_or(QueryHeight::Latest, |height| { + QueryHeight::Specific(Height::new(self.chain_id.version(), height).unwrap()) + }); + + let client_id = self.client_id.clone(); + + let client_state = chain + .with_blocking_chain_handle(move |handle| { + let (client_state, _) = handle + .query_client_state( + QueryClientStateRequest { client_id, height }, + IncludeProof::No, + ) + .map_err(BaseError::relayer)?; + + Ok(client_state) + }) + .await + .wrap_err_with(|| { + format!( + "Failed to query client state for client `{}` on chain `{}`", + self.client_id, self.chain_id + ) + })?; + + info!( + "Found client state for client `{}` on chain `{}`:", + self.client_id, self.chain_id + ); + + Ok(Output::success(client_state)) + } +} diff --git a/crates/cli/cli/src/commands/query/connections.rs b/crates/cli/cli/src/commands/query/connections.rs index 1d758e429..1a18c6f3f 100644 --- a/crates/cli/cli/src/commands/query/connections.rs +++ b/crates/cli/cli/src/commands/query/connections.rs @@ -1,3 +1,4 @@ +use hermes_cli_framework::output::Output; use oneline_eyre::eyre::eyre; use tracing::info; use tracing::warn; @@ -39,13 +40,13 @@ pub struct QueryConnections { } impl Runnable for QueryConnections { - async fn run(&self, builder: CosmosBuilder) -> Result<()> { + async fn run(&self, builder: CosmosBuilder) -> Result { let chain = builder.build_chain(&self.chain_id).await?; let chain_id = self.chain_id.clone(); let counterparty_chain_id = self.counterparty_chain_id.clone(); let verbose = self.verbose; - chain + let connections = chain .with_blocking_chain_handle(move |chain_handle| { let mut connections = chain_handle.query_connections(QueryConnectionsRequest { pagination: None }).unwrap(); @@ -88,11 +89,11 @@ impl Runnable for QueryConnections { } }); - Ok(()) + Ok(connections) }) .await .map_err(|e| eyre!("Failed to query connections for host chain: {e}"))?; - Ok(()) + Ok(Output::success(connections)) } } diff --git a/crates/cli/cli/src/commands/query/mod.rs b/crates/cli/cli/src/commands/query/mod.rs index 4da8237ad..5143497d8 100644 --- a/crates/cli/cli/src/commands/query/mod.rs +++ b/crates/cli/cli/src/commands/query/mod.rs @@ -1,20 +1,29 @@ +mod client; +pub use client::ClientCommands; + mod connections; pub use connections::QueryConnections; use hermes_cli_framework::command::Runnable; +use hermes_cli_framework::output::Output; use hermes_cosmos_relayer::contexts::builder::CosmosBuilder; use crate::Result; #[derive(Debug, clap::Subcommand)] pub enum QueryCommands { + /// Query information about IBC clients + #[clap(subcommand)] + Client(ClientCommands), + /// Query all connections Connections(QueryConnections), } -impl QueryCommands { - pub async fn run(&self, builder: CosmosBuilder) -> Result<()> { +impl Runnable for QueryCommands { + async fn run(&self, builder: CosmosBuilder) -> Result { match self { + Self::Client(cmd) => cmd.run(builder).await, Self::Connections(cmd) => cmd.run(builder).await, } } diff --git a/crates/cli/cli/src/commands/start.rs b/crates/cli/cli/src/commands/start.rs index 6371f5c8a..9674b4b5d 100644 --- a/crates/cli/cli/src/commands/start.rs +++ b/crates/cli/cli/src/commands/start.rs @@ -1,5 +1,6 @@ use cgp_core::CanRun; use hermes_cli_framework::command::Runnable; +use hermes_cli_framework::output::Output; use hermes_cosmos_relayer::contexts::builder::CosmosBuilder; use hermes_relayer_components::build::traits::components::birelay_builder::CanBuildBiRelay; use ibc_relayer_types::core::ics24_host::identifier::{ChainId, ClientId}; @@ -48,7 +49,7 @@ pub struct Start { } impl Runnable for Start { - async fn run(&self, builder: CosmosBuilder) -> Result<()> { + async fn run(&self, builder: CosmosBuilder) -> Result { info!("Starting relayer..."); let birelay = builder @@ -71,6 +72,6 @@ impl Runnable for Start { .await .map_err(|e| eyre!("Relayed exited because of error: {e}"))?; - Ok(()) + Ok(Output::success_msg("Relayer exited successfully.")) } }