cosm_utils/clients/
client.rs

1use crate::chain::coin::{Coin, Denom};
2use crate::chain::error::ChainError;
3use crate::chain::fee::{Fee, GasInfo};
4use crate::chain::msg::Msg;
5use crate::chain::request::TxOptions;
6use crate::chain::tx::RawTx;
7use crate::config::cfg::ChainConfig;
8use crate::modules::auth::error::AccountError;
9use crate::modules::auth::model::{Account, AccountResponse, Address};
10use crate::signing_key::key::SigningKey;
11use async_trait::async_trait;
12use cosmrs::proto::cosmos::auth::v1beta1::{
13    BaseAccount, QueryAccountRequest, QueryAccountResponse,
14};
15use cosmrs::proto::cosmos::tx::v1beta1::{SimulateRequest, SimulateResponse, TxRaw};
16use cosmrs::proto::traits::Message;
17use cosmrs::tendermint::Hash;
18use cosmrs::Any;
19
20use cosmrs::tendermint::abci::{Event, EventAttribute};
21use cosmrs::tx::{Body, SignerInfo};
22#[cfg(feature = "mockall")]
23use mockall::automock;
24
25use serde::Serialize;
26use tendermint_rpc::endpoint::tx;
27
28fn encode_msg<T: Message>(msg: T) -> Result<Vec<u8>, ChainError> {
29    let mut data = Vec::with_capacity(msg.encoded_len());
30    msg.encode(&mut data)
31        .map_err(ChainError::prost_proto_encoding)?;
32    Ok(data)
33}
34
35pub trait GetErr: Sized {
36    fn get_err(self) -> Result<Self, ChainError>;
37}
38
39pub trait GetValue {
40    fn get_value(&self) -> &[u8];
41}
42
43pub trait GetEvents {
44    fn get_events(&self) -> &[Event];
45
46    fn find_event_tags(&self, event_type: String, key_name: String) -> Vec<&EventAttribute> {
47        let mut events = vec![];
48        for event in self.get_events() {
49            if event.kind == event_type {
50                for attr in &event.attributes {
51                    if attr.key == key_name {
52                        events.push(attr);
53                    }
54                }
55            }
56        }
57        events
58    }
59}
60
61#[cfg_attr(feature = "mockall", automock)]
62#[async_trait]
63pub trait HashSearch: ClientAbciQuery {
64    async fn hash_search(&self, hash: &Hash) -> Result<tx::Response, ChainError>;
65}
66
67#[cfg_attr(feature = "mockall", automock)]
68#[async_trait]
69pub trait ClientTxCommit {
70    type Response: GetErr + GetEvents;
71    async fn broadcast_tx_commit(&self, raw_tx: &RawTx) -> Result<Self::Response, ChainError>;
72}
73
74#[cfg_attr(feature = "mockall", automock)]
75#[async_trait]
76pub trait ClientTxSync {
77    type Response: GetErr;
78    async fn broadcast_tx_sync(&self, raw_tx: &RawTx) -> Result<Self::Response, ChainError>;
79}
80
81#[cfg_attr(feature = "mockall", automock)]
82#[async_trait]
83pub trait ClientTxAsync {
84    type Response: GetErr;
85    async fn broadcast_tx_async(&self, raw_tx: &RawTx) -> Result<Self::Response, ChainError>;
86}
87
88#[cfg_attr(feature = "mockall", automock)]
89#[async_trait]
90pub trait ClientAbciQuery: Sized {
91    type Response: GetErr + GetValue;
92    async fn abci_query<V>(
93        &self,
94        path: Option<String>,
95        data: V,
96        height: Option<u32>,
97        prove: bool,
98    ) -> Result<Self::Response, ChainError>
99    where
100        V: Into<Vec<u8>> + Send;
101
102    async fn query<I, O>(&self, msg: I, path: &str) -> Result<O, ChainError>
103    where
104        Self: Sized,
105        I: Message + Default + 'static,
106        O: Message + Default + 'static,
107    {
108        let bytes = encode_msg(msg)?;
109
110        let res = self
111            .abci_query(Some(path.to_string()), bytes, None, false)
112            .await?;
113
114        let proto_res =
115            O::decode(res.get_err()?.get_value()).map_err(ChainError::prost_proto_decoding)?;
116
117        Ok(proto_res)
118    }
119
120    async fn auth_query_account(&self, address: Address) -> Result<AccountResponse, AccountError> {
121        let req = QueryAccountRequest {
122            address: address.into(),
123        };
124
125        let res = self
126            .query::<_, QueryAccountResponse>(req, "/cosmos.auth.v1beta1.Query/Account")
127            .await?;
128
129        let account = res.account.ok_or(AccountError::Address {
130            message: "Invalid account address".to_string(),
131        })?;
132
133        let base_account = BaseAccount::decode(account.value.as_slice())
134            .map_err(ChainError::prost_proto_decoding)?;
135
136        Ok(AccountResponse {
137            account: base_account.try_into()?,
138        })
139    }
140
141    #[allow(deprecated)]
142    async fn query_simulate_tx(&self, tx: &RawTx) -> Result<GasInfo, ChainError> {
143        let req = SimulateRequest {
144            tx: None,
145            tx_bytes: tx.to_bytes()?,
146        };
147
148        let bytes = encode_msg(req)?;
149
150        let res = self
151            .abci_query(
152                Some("/cosmos.tx.v1beta1.Service/Simulate".to_string()),
153                bytes,
154                None,
155                false,
156            )
157            .await?;
158
159        let sim_res = SimulateResponse::decode(res.get_err()?.get_value())
160            .map_err(ChainError::prost_proto_decoding)?;
161
162        let gas_info = sim_res.gas_info.ok_or(ChainError::Simulation {
163            result: sim_res.result.unwrap(),
164        })?;
165
166        Ok(gas_info.into())
167    }
168
169    // Sends tx with an empty public_key / signature, like they do in the cosmos-sdk:
170    // https://github.com/cosmos/cosmos-sdk/blob/main/client/tx/tx.go#L133
171    async fn tx_simulate<I>(
172        &self,
173        denom: &str,
174        gas_price: f64,
175        gas_adjustment: f64,
176        msgs: I,
177        account: &Account,
178    ) -> Result<Fee, ChainError>
179    where
180        I: IntoIterator<Item = Any> + Send,
181    {
182        let tx = Body::new(msgs, "cosm-client memo", 0u16);
183
184        let denom: Denom = denom.parse()?;
185
186        let fee = Fee::new(
187            Coin {
188                denom: denom.clone(),
189                amount: 0u128,
190            },
191            0u64,
192            None,
193            None,
194        );
195
196        let auth_info =
197            SignerInfo::single_direct(None, account.sequence).auth_info(fee.try_into()?);
198
199        let tx_raw = TxRaw {
200            body_bytes: tx.into_bytes().map_err(ChainError::proto_encoding)?,
201            auth_info_bytes: auth_info.into_bytes().map_err(ChainError::proto_encoding)?,
202            signatures: vec![vec![]],
203        };
204
205        let gas_info = self.query_simulate_tx(&tx_raw.into()).await?;
206
207        // TODO: clean up this gas conversion code to be clearer
208        let gas_limit = (gas_info.gas_used.value() as f64 * gas_adjustment).ceil();
209        let amount = Coin {
210            denom,
211            amount: ((gas_limit * gas_price).ceil() as u64).into(),
212        };
213
214        let fee = Fee::new(amount, gas_limit as u64, None, None);
215
216        Ok(fee)
217    }
218
219    async fn tx_sign<T>(
220        &self,
221        chain_cfg: &ChainConfig,
222        msgs: Vec<T>,
223        key: &SigningKey,
224        tx_options: &TxOptions,
225    ) -> Result<RawTx, AccountError>
226    where
227        T: Msg + Serialize + Send + Sync,
228        <T as Msg>::Err: Send + Sync,
229    {
230        let sender_addr = key
231            .to_addr(&chain_cfg.prefix, &chain_cfg.derivation_path)
232            .await?;
233
234        let timeout_height = tx_options.timeout_height.unwrap_or_default();
235
236        let account = if let Some(ref account) = tx_options.account {
237            account.clone()
238        } else {
239            self.auth_query_account(sender_addr).await?.account
240        };
241
242        let fee = if let Some(fee) = &tx_options.fee {
243            fee.clone()
244        } else {
245            self.tx_simulate(
246                &chain_cfg.denom,
247                chain_cfg.gas_price,
248                chain_cfg.gas_adjustment,
249                msgs.iter()
250                    .map(|m| m.to_any())
251                    .collect::<Result<Vec<_>, _>>()
252                    .map_err(|e| ChainError::ProtoEncoding {
253                        message: e.to_string(),
254                    })?,
255                &account,
256            )
257            .await?
258        };
259
260        let raw = key
261            .sign(
262                msgs,
263                timeout_height,
264                &tx_options.memo,
265                account,
266                fee,
267                &chain_cfg.chain_id,
268                &chain_cfg.derivation_path,
269            )
270            .await?;
271        Ok(raw)
272    }
273}