hermes_cli_components/impls/commands/queries/
client_status.rs

1use core::marker::PhantomData;
2
3use cgp::prelude::*;
4use hermes_error::traits::wrap::CanWrapError;
5use hermes_logging_components::traits::has_logger::HasLogger;
6use hermes_logging_components::traits::logger::CanLog;
7use hermes_logging_components::types::level::LevelInfo;
8use hermes_relayer_components::build::traits::builders::chain_builder::CanBuildChain;
9use hermes_relayer_components::chain::traits::queries::chain_status::CanQueryChainStatus;
10use hermes_relayer_components::chain::traits::queries::client_state::{
11    CanQueryClientState, CanQueryClientStateWithLatestHeight,
12};
13use hermes_relayer_components::chain::traits::queries::consensus_state::{
14    CanQueryConsensusState, CanQueryConsensusStateWithLatestHeight,
15};
16use hermes_relayer_components::chain::traits::types::client_state::{
17    HasClientStateFields, HasClientStateType,
18};
19use hermes_relayer_components::chain::traits::types::consensus_state::{
20    HasConsensusStateFields, HasConsensusStateType,
21};
22use hermes_relayer_components::chain::traits::types::ibc::HasIbcChainTypes;
23use hermes_relayer_components::multi::traits::chain_at::HasChainTypeAt;
24use hermes_relayer_components::multi::types::index::Index;
25use serde::Serialize;
26
27use crate::traits::build::CanLoadBuilder;
28use crate::traits::command::CommandRunner;
29use crate::traits::output::CanProduceOutput;
30use crate::traits::parse::CanParseArg;
31
32pub struct RunQueryClientStatusCommand;
33
34#[derive(Debug, clap::Parser, HasField)]
35pub struct QueryClientStatusArgs {
36    /// Identifier of the host chain
37    #[clap(
38        long = "chain",
39        required = true,
40        value_name = "CHAIN_ID",
41        help_heading = "REQUIRED"
42    )]
43    chain_id: String,
44
45    /// Identifier of the client on the host chain
46    #[clap(
47        long = "client",
48        required = true,
49        value_name = "CLIENT_ID",
50        help_heading = "REQUIRED"
51    )]
52    client_id: String,
53}
54
55#[derive(Debug, Serialize)]
56pub enum ClientStatus {
57    Frozen,
58    Expired,
59    Active,
60}
61
62impl<App, Args, Build, Chain, Counterparty> CommandRunner<App, Args> for RunQueryClientStatusCommand
63where
64    App: CanLoadBuilder<Builder = Build>
65        + CanProduceOutput<ClientStatus>
66        + HasLogger
67        // TODO: use AnyCounterparty
68        // + HasAnyCounterparty<AnyCounterparty = Counterparty>
69        + CanParseArg<Args, symbol!("chain_id"), Parsed = Chain::ChainId>
70        + CanParseArg<Args, symbol!("client_id"), Parsed = Chain::ClientId>
71        + CanRaiseError<Build::Error>
72        + CanRaiseError<Chain::Error>,
73    Build: CanBuildChain<0, Chain = Chain> + HasChainTypeAt<1, Chain = Counterparty>,
74    Chain: HasIbcChainTypes<Counterparty> + CanQueryClientStatus<Counterparty>,
75    Counterparty: HasIbcChainTypes<Chain>
76        + HasClientStateType<Chain>
77        + HasClientStateFields<Chain>
78        + HasConsensusStateType<Chain>
79        + HasConsensusStateFields<Chain>,
80    App::Logger: CanLog<LevelInfo>,
81    Args: Async,
82{
83    async fn run_command(app: &App, args: &Args) -> Result<App::Output, App::Error> {
84        let chain_id = app.parse_arg(args, PhantomData::<symbol!("chain_id")>)?;
85        let client_id = app.parse_arg(args, PhantomData::<symbol!("client_id")>)?;
86
87        let logger = app.logger();
88        let builder = app.load_builder().await?;
89
90        let chain = builder
91            .build_chain(Index::<0>, &chain_id)
92            .await
93            .map_err(App::raise_error)?;
94
95        let client_status = chain
96            .query_client_status(&client_id)
97            .await
98            .map_err(App::raise_error)?;
99
100        match client_status {
101            ClientStatus::Frozen => {
102                logger
103                    .log(&format!("Client `{}` is frozen", client_id), &LevelInfo)
104                    .await;
105            }
106            ClientStatus::Expired => {
107                logger
108                    .log(&format!("Client `{}` has expired", client_id), &LevelInfo)
109                    .await;
110            }
111            ClientStatus::Active => {
112                logger
113                    .log(&format!("Client `{}` is active", client_id), &LevelInfo)
114                    .await;
115            }
116        }
117
118        Ok(app.produce_output(client_status))
119    }
120}
121
122#[async_trait]
123pub trait CanQueryClientStatus<Counterparty>:
124    HasIbcChainTypes<Counterparty>
125    + HasErrorType
126    + CanQueryClientState<Counterparty>
127    + CanQueryClientStateWithLatestHeight<Counterparty>
128    + CanQueryChainStatus
129    + CanQueryConsensusState<Counterparty>
130where
131    Counterparty: HasIbcChainTypes<Self>
132        + HasClientStateType<Self>
133        + HasClientStateFields<Self>
134        + HasConsensusStateType<Self>
135        + HasConsensusStateFields<Self>,
136{
137    async fn query_client_status(
138        &self,
139        client_id: &Self::ClientId,
140    ) -> Result<ClientStatus, Self::Error>;
141}
142
143impl<Chain, Counterparty> CanQueryClientStatus<Counterparty> for Chain
144where
145    Chain: HasIbcChainTypes<Counterparty>
146        + HasErrorType
147        + CanQueryClientState<Counterparty>
148        + CanQueryClientStateWithLatestHeight<Counterparty>
149        + CanQueryChainStatus
150        + CanQueryConsensusState<Counterparty>
151        + CanWrapError<String>,
152    Counterparty: HasIbcChainTypes<Chain>
153        + HasClientStateType<Chain>
154        + HasClientStateFields<Chain>
155        + HasConsensusStateType<Chain>
156        + HasConsensusStateFields<Chain>,
157{
158    async fn query_client_status(
159        &self,
160        client_id: &Self::ClientId,
161    ) -> Result<ClientStatus, Self::Error> {
162        let client_state = self
163            .query_client_state_with_latest_height(client_id)
164            .await
165            .map_err(|e| {
166                Chain::wrap_error(
167                    format!("Failed to query client state for client `{client_id}`"),
168                    e,
169                )
170            })?;
171
172        if Counterparty::client_state_is_frozen(&client_state) {
173            return Ok(ClientStatus::Frozen);
174        }
175
176        let client_latest_height = Counterparty::client_state_latest_height(&client_state);
177
178        let latest_consensus_state = self
179            .query_consensus_state_with_latest_height(client_id, &client_latest_height)
180            .await
181            .map_err(|e| {
182                Chain::wrap_error(
183                    format!("Failed to query consensus state at height {client_latest_height}"),
184                    e,
185                )
186            })?;
187
188        let latest_consensus_state_timestamp =
189            Counterparty::consensus_state_timestamp(&latest_consensus_state);
190
191        let chain_status = self
192            .query_chain_status()
193            .await
194            .map_err(|e| Chain::wrap_error("Failed to query chain status".to_owned(), e))?;
195
196        let current_network_time = Self::chain_status_timestamp(&chain_status);
197
198        let elapsed =
199            Self::timestamp_duration_since(&latest_consensus_state_timestamp, current_network_time);
200
201        let has_expired = elapsed.map_or(false, |elapsed| {
202            Counterparty::client_state_has_expired(&client_state, elapsed)
203        });
204
205        if has_expired {
206            Ok(ClientStatus::Expired)
207        } else {
208            Ok(ClientStatus::Active)
209        }
210    }
211}