Skip to main content

cw_orch_daemon/queriers/
node.rs

1use std::{cmp::min, time::Duration};
2
3use crate::{
4    cosmos_modules, env::DaemonEnvVars, error::DaemonError, senders::query::QuerySender,
5    tx_resp::CosmTxResponse, DaemonBase,
6};
7
8use cosmrs::{
9    proto::cosmos::{
10        base::query::v1beta1::PageRequest,
11        tx::v1beta1::{OrderBy, SimulateResponse},
12    },
13    tendermint::{Block, Time},
14};
15use cosmwasm_std::BlockInfo;
16use cw_orch_core::{
17    environment::{NodeQuerier, Querier, QuerierGetter},
18    log::query_target,
19};
20use tokio::runtime::Handle;
21use tonic::transport::Channel;
22
23/// Querier for the Tendermint node.
24/// Supports queries for block and tx information
25/// All the async function are prefixed with `_`
26pub struct Node {
27    pub channel: Channel,
28    pub rt_handle: Option<Handle>,
29}
30
31impl Node {
32    pub fn new<Sender: QuerySender>(daemon: &DaemonBase<Sender>) -> Self {
33        Self {
34            channel: daemon.channel(),
35            rt_handle: Some(daemon.rt_handle.clone()),
36        }
37    }
38    pub fn new_async(channel: Channel) -> Self {
39        Self {
40            channel,
41            rt_handle: None,
42        }
43    }
44}
45
46impl<Sender: QuerySender> QuerierGetter<Node> for DaemonBase<Sender> {
47    fn querier(&self) -> Node {
48        Node::new(self)
49    }
50}
51
52impl Querier for Node {
53    type Error = DaemonError;
54}
55
56impl Node {
57    /// Returns node info
58    pub async fn _info(
59        &self,
60    ) -> Result<cosmos_modules::tendermint::GetNodeInfoResponse, DaemonError> {
61        let mut client =
62            cosmos_modules::tendermint::service_client::ServiceClient::new(self.channel.clone());
63
64        let resp = client
65            .get_node_info(cosmos_modules::tendermint::GetNodeInfoRequest {})
66            .await?
67            .into_inner();
68
69        Ok(resp)
70    }
71
72    /// Queries node syncing
73    pub async fn _syncing(&self) -> Result<bool, DaemonError> {
74        let mut client =
75            cosmos_modules::tendermint::service_client::ServiceClient::new(self.channel.clone());
76
77        let resp = client
78            .get_syncing(cosmos_modules::tendermint::GetSyncingRequest {})
79            .await?
80            .into_inner();
81
82        Ok(resp.syncing)
83    }
84
85    /// Returns latests block information
86    pub async fn _latest_block(&self) -> Result<Block, DaemonError> {
87        let mut client =
88            cosmos_modules::tendermint::service_client::ServiceClient::new(self.channel.clone());
89
90        let resp = client
91            .get_latest_block(cosmos_modules::tendermint::GetLatestBlockRequest {})
92            .await?
93            .into_inner();
94
95        Ok(Block::try_from(resp.block.unwrap())?)
96    }
97
98    /// Returns block information fetched by height
99    pub async fn _block_by_height(&self, height: u64) -> Result<Block, DaemonError> {
100        let mut client =
101            cosmos_modules::tendermint::service_client::ServiceClient::new(self.channel.clone());
102
103        let resp = client
104            .get_block_by_height(cosmos_modules::tendermint::GetBlockByHeightRequest {
105                height: height as i64,
106            })
107            .await?
108            .into_inner();
109
110        Ok(Block::try_from(resp.block.unwrap())?)
111    }
112
113    /// Return the average block time for the last 50 blocks or since inception
114    /// This is used to estimate the time when a tx will be included in a block
115    pub async fn _average_block_speed(
116        &self,
117        multiplier: Option<f32>,
118    ) -> Result<Duration, DaemonError> {
119        // get latest block time and height
120        let mut latest_block = self._latest_block().await?;
121        let latest_block_time = latest_block.header.time;
122        let mut latest_block_height = latest_block.header.height.value();
123
124        while latest_block_height <= 1 {
125            // wait to get some blocks
126            tokio::time::sleep(Duration::from_secs(1)).await;
127            latest_block = self._latest_block().await?;
128            latest_block_height = latest_block.header.height.value();
129        }
130
131        // let avg period
132        let avg_period = min(latest_block_height - 1, 50);
133
134        // get block time for block avg_period blocks ago
135        let block_avg_period_ago = self
136            ._block_by_height(latest_block_height - avg_period)
137            .await?;
138        let block_avg_period_ago_time = block_avg_period_ago.header.time;
139
140        // calculate average block time
141        let average_block_time = latest_block_time.duration_since(block_avg_period_ago_time)?;
142        let average_block_time = average_block_time.div_f64(avg_period as f64);
143
144        // multiply by multiplier if provided
145        let average_block_time = match multiplier {
146            Some(multiplier) => average_block_time.mul_f32(multiplier),
147            None => average_block_time,
148        };
149
150        Ok(average_block_time)
151    }
152
153    /// Returns latests validator set
154    pub async fn _latest_validator_set(
155        &self,
156        pagination: Option<PageRequest>,
157    ) -> Result<cosmos_modules::tendermint::GetLatestValidatorSetResponse, DaemonError> {
158        let mut client =
159            cosmos_modules::tendermint::service_client::ServiceClient::new(self.channel.clone());
160
161        let resp = client
162            .get_latest_validator_set(cosmos_modules::tendermint::GetLatestValidatorSetRequest {
163                pagination,
164            })
165            .await?
166            .into_inner();
167
168        Ok(resp)
169    }
170
171    /// Returns latests validator set fetched by height
172    pub async fn _validator_set_by_height(
173        &self,
174        height: i64,
175        pagination: Option<PageRequest>,
176    ) -> Result<cosmos_modules::tendermint::GetValidatorSetByHeightResponse, DaemonError> {
177        let mut client =
178            cosmos_modules::tendermint::service_client::ServiceClient::new(self.channel.clone());
179
180        let resp = client
181            .get_validator_set_by_height(
182                cosmos_modules::tendermint::GetValidatorSetByHeightRequest { height, pagination },
183            )
184            .await?
185            .into_inner();
186
187        Ok(resp)
188    }
189
190    /// Returns current block height
191    pub async fn _block_height(&self) -> Result<u64, DaemonError> {
192        let block = self._latest_block().await?;
193        Ok(block.header.height.value())
194    }
195
196    /// Returns the block timestamp (since unix epoch) in nanos
197    pub async fn _block_time(&self) -> Result<u128, DaemonError> {
198        let block = self._latest_block().await?;
199        Ok(block
200            .header
201            .time
202            .duration_since(Time::unix_epoch())?
203            .as_nanos())
204    }
205
206    /// Simulate TX
207    pub async fn _simulate_tx(&self, tx_bytes: Vec<u8>) -> Result<u64, DaemonError> {
208        let mut client =
209            cosmos_modules::tx::service_client::ServiceClient::new(self.channel.clone());
210        #[allow(deprecated)]
211        let resp: SimulateResponse = client
212            .simulate(cosmos_modules::tx::SimulateRequest { tx: None, tx_bytes })
213            .await?
214            .into_inner();
215        let gas_used = resp.gas_info.unwrap().gas_used;
216        Ok(gas_used)
217    }
218
219    /// Returns all the block info
220    pub async fn _block_info(&self) -> Result<cosmwasm_std::BlockInfo, DaemonError> {
221        let block = self._latest_block().await?;
222
223        block_to_block_info(block)
224    }
225
226    /// Find TX by hash
227    pub async fn _find_tx(&self, hash: String) -> Result<CosmTxResponse, DaemonError> {
228        self._find_tx_with_retries(hash, DaemonEnvVars::max_tx_query_retries())
229            .await
230    }
231
232    /// Find TX by hash with a given amount of retries
233    pub async fn _find_tx_with_retries(
234        &self,
235        hash: String,
236        retries: usize,
237    ) -> Result<CosmTxResponse, DaemonError> {
238        let mut client =
239            cosmos_modules::tx::service_client::ServiceClient::new(self.channel.clone());
240
241        let request = cosmos_modules::tx::GetTxRequest { hash: hash.clone() };
242        let mut block_speed = self._average_block_speed(Some(0.7)).await?;
243        let max_block_time = DaemonEnvVars::max_block_time();
244        if let Some(max_time) = max_block_time {
245            block_speed = block_speed.min(max_time);
246        } else {
247            let min_block_time = DaemonEnvVars::min_block_time();
248            block_speed = block_speed.max(min_block_time);
249        }
250
251        for _ in 0..retries {
252            match client.get_tx(request.clone()).await {
253                Ok(tx) => {
254                    let resp = tx.into_inner().tx_response.unwrap().into();
255                    log::debug!(target: &query_target(), "TX found: {:?}", resp);
256                    return Ok(resp);
257                }
258                Err(err) => {
259                    // increase wait time
260                    block_speed = block_speed.mul_f64(1.6);
261                    if let Some(max_time) = max_block_time {
262                        block_speed = block_speed.min(max_time)
263                    }
264                    log::debug!(target: &query_target(), "TX not found with error: {:?}", err);
265                    log::debug!(target: &query_target(), "Waiting {} milli-seconds", block_speed.as_millis());
266                    tokio::time::sleep(block_speed).await;
267                }
268            }
269        }
270
271        // return error if tx not found by now
272        Err(DaemonError::TXNotFound(hash, retries))
273    }
274
275    /// Find TX by events
276    pub async fn _find_tx_by_events(
277        &self,
278        events: Vec<String>,
279        page: Option<u64>,
280        order_by: Option<OrderBy>,
281    ) -> Result<Vec<CosmTxResponse>, DaemonError> {
282        self._find_tx_by_events_with_retries(
283            events,
284            page,
285            order_by,
286            false,
287            DaemonEnvVars::max_tx_query_retries(),
288        )
289        .await
290    }
291
292    /// Find Tx by events
293    /// This function will consider that no transactions found is an error
294    /// This either returns a non empty vector or errors
295    pub async fn _find_some_tx_by_events(
296        &self,
297        events: Vec<String>,
298        page: Option<u64>,
299        order_by: Option<OrderBy>,
300    ) -> Result<Vec<CosmTxResponse>, DaemonError> {
301        self._find_tx_by_events_with_retries(
302            events,
303            page,
304            order_by,
305            true,
306            DaemonEnvVars::max_tx_query_retries(),
307        )
308        .await
309    }
310
311    /// Find TX by events with  :
312    /// 1. Specify if an empty tx object is a valid response
313    /// 2. Specify a given amount of retries
314    pub async fn _find_tx_by_events_with_retries(
315        &self,
316        events: Vec<String>,
317        page: Option<u64>,
318        order_by: Option<OrderBy>,
319        retry_on_empty: bool,
320        retries: usize,
321    ) -> Result<Vec<CosmTxResponse>, DaemonError> {
322        let mut client = cosmrs::proto::cosmos::tx::v1beta1::service_client::ServiceClient::new(
323            self.channel.clone(),
324        );
325
326        #[allow(deprecated)]
327        let request = cosmrs::proto::cosmos::tx::v1beta1::GetTxsEventRequest {
328            events: events.clone(),
329            pagination: None,
330            order_by: order_by.unwrap_or(OrderBy::Desc).into(),
331            page: page.unwrap_or(0),
332            limit: 100,
333            query: events.join(" AND "),
334        };
335
336        for _ in 0..retries {
337            match client.get_txs_event(request.clone()).await {
338                Ok(tx) => {
339                    let resp = tx.into_inner().tx_responses;
340                    if retry_on_empty && resp.is_empty() {
341                        log::debug!(target: &query_target(), "No TX found with events {:?}", events);
342                        log::debug!(target: &query_target(), "Waiting 10s");
343                        tokio::time::sleep(Duration::from_secs(10)).await;
344                    } else {
345                        log::debug!(
346                            target: &query_target(),
347                            "TX found by events: {:?}",
348                            resp.iter().map(|t| t.txhash.clone())
349                        );
350                        return Ok(resp.iter().map(|r| r.clone().into()).collect());
351                    }
352                }
353                Err(err) => {
354                    log::debug!(target: &query_target(), "TX not found with error: {:?}", err);
355                    log::debug!(target: &query_target(), "Waiting 10s");
356                    tokio::time::sleep(Duration::from_secs(10)).await;
357                }
358            }
359        }
360        // return error if tx not found by now
361        Err(DaemonError::TXNotFound(
362            format!("with events {:?}", events),
363            DaemonEnvVars::max_tx_query_retries(),
364        ))
365    }
366}
367
368// Now we define traits
369
370impl NodeQuerier for Node {
371    type Response = CosmTxResponse;
372
373    fn latest_block(&self) -> Result<cosmwasm_std::BlockInfo, Self::Error> {
374        self.rt_handle
375            .as_ref()
376            .ok_or(DaemonError::QuerierNeedRuntime)?
377            .block_on(self._block_info())
378    }
379
380    fn block_by_height(&self, height: u64) -> Result<cosmwasm_std::BlockInfo, Self::Error> {
381        let block = self
382            .rt_handle
383            .as_ref()
384            .ok_or(DaemonError::QuerierNeedRuntime)?
385            .block_on(self._block_by_height(height))?;
386
387        block_to_block_info(block)
388    }
389
390    fn block_height(&self) -> Result<u64, Self::Error> {
391        self.rt_handle
392            .as_ref()
393            .ok_or(DaemonError::QuerierNeedRuntime)?
394            .block_on(self._block_height())
395    }
396
397    fn block_time(&self) -> Result<u128, Self::Error> {
398        self.rt_handle
399            .as_ref()
400            .ok_or(DaemonError::QuerierNeedRuntime)?
401            .block_on(self._block_time())
402    }
403
404    fn simulate_tx(&self, tx_bytes: Vec<u8>) -> Result<u64, Self::Error> {
405        self.rt_handle
406            .as_ref()
407            .ok_or(DaemonError::QuerierNeedRuntime)?
408            .block_on(self._simulate_tx(tx_bytes))
409    }
410
411    fn find_tx(&self, hash: String) -> Result<Self::Response, Self::Error> {
412        self.rt_handle
413            .as_ref()
414            .ok_or(DaemonError::QuerierNeedRuntime)?
415            .block_on(self._find_tx(hash))
416    }
417}
418
419fn block_to_block_info(block: Block) -> Result<BlockInfo, DaemonError> {
420    let since_epoch = block.header.time.duration_since(Time::unix_epoch())?;
421    let time = cosmwasm_std::Timestamp::from_nanos(since_epoch.as_nanos() as u64);
422    Ok(cosmwasm_std::BlockInfo {
423        height: block.header.height.value(),
424        time,
425        chain_id: block.header.chain_id.to_string(),
426    })
427}